前言
在objc里我们创建对象的时候会经常的用到alloc和init,那么objc在调用alloc的时候它的底层到底是进行了什么样的处理呐?
在开始前先运行下面这个demo
Person *p = [Person alloc];
p.age = 20;
Person *p1 = [p init];
Person *p2 = [p init];
NSLog(@"\n%@\n%@\n%@\n %p\n %p\n %p\n ",p,p1,p2, &p,&p1,&p2);
NSLog(@"%d %d",p1.age,p2.age);
其运行结果如下:
很明显可以看到p,p1,p2一样,而&p,&p1,&p2不一样,为什么会这样呐?
读者可以参考下面图配合这段文字进行理解:
我们通过Person alloc创建出的p,其实是在堆区创建了一个p的内存空间0x600003588500,而&p是栈区的指针,它指向0x600003588500,p init创建了同样指向0x600003588500的指针&p1和&p2, 所以可以看到可以看到p,p1,p2的属性age都是一样的,他们是同时指向一个地址0x600003588500。
\
1、汇编分析
开始汇编分析前,给读者提下几个汇编的指令:
- b,bl 跳转指令表示函数的调用
- ret 函数返回; 注释
- ;来代表注释
还有一点知识:cpu分为寄存器、运算器、控制器,
cpu先从内存里读数据保存到寄存器中再进行运算,这样效率会高
1.1汇编分析过程
先在xcode工程打开汇编模式,Debug ->Debug Workflow ->Always Show Disassembly,如图:
设置好汇编模式后,在这打个断点,编译就会进入汇编指令
这里很熟悉地看到一个bl指令,这是一个函数的跳转指令,";"号后面是注释,告诉我们这里调了objc_alloc这个函数,接着我们给objc_alloc打个断点,进入objc_alloc函数中,如下图,想了解怎么打断点可以参考文末附录。
可以看到,出现了两个函数objc_msgSend和_objc_rootAllocWithZone,这是我们可以通过点击control键+下箭头来一步一步地走汇编指令,发现跳过_objc_rootAllocWithZone,执行了objc_msgSend,当汇编走第11行到时候,感兴趣的读者可以通过lldb读下x0和x1寄存器(register read x0),看看他们当前是什么,演示如下图:
po (char *)是强转的意思,可以看到x0是person而x1是alloc方法
上面这个图告诉我们
这个objc_msgSend函数其实就是通过Person去调alloc方法
接着我们给[NSObjc_alloc]打个断点,
alloc里有一个_objc_rootAlloc,接着进入到这个函数里
这个时候又看到了两个方法_objc_rootAllocWithZone和objc_msgSend,接着走汇编指令,可以看到它跑到了_objc_rootAllocWithZone这个方法里来了,到了这一步,就得去到我们的源码里再进行近一步的分析了,读者可以移步阅读下文的源码分析,这个方法返回的是一个id的类型,是_class_createInstanceFromZone的返回值。
可以进入_objc_rootAllocWithZone看看里面实现了什么
这里就运用到了ret指令了,在这打个断点,然后通过lldb来查看x0寄存器里是什么
1.2 汇编分析的结论
通过上面的分析,编者给大家做个小总结:
alloc是真正给我们创建实例对象的方法
alloc方法的大概流程是这样的:
alloc -> objc_alloc -> objc_msgSend ->alloc -> _objc_rootAlloc ->_objc_rootAllocWithZone -> _class_createInstanceFromZone
看着这个结论大家可以吐槽下,分析了这么久就只得到这么一个函数之间的跳转,没有实际意义,其实拿到这个流程后对我们去分析源码是有很大助力的,当然如果你的汇编能力很强,可以继续推下去,核心代码在_class_createInstanceFromZone,通过汇编你能更了解其底层到底做了什么,但可能有部分会被编译器给优化掉了,所以建议到了这步可以去分析源码了。
2、源码分析:
在进行源码分析前,读者可以到官网去下载objc里的alloc源码,这是链接,可以找最新版的objc4-838.1
由于已经用汇编演示了alloc底层大致流程了,源码这里就不断点分析其流程了,直接分析每个方法实现了什么,最核心的方法是_class_createInstanceFromZone,前面的方法都是一些跳转和判断。
2.1 objc_alloc
源码如下:
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
这个callAlloc方法我们在汇编没有看到是因为编译器的优化,可以点进去看到看下callAlloc源码
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
这里可以看到_objc_rootAllocWithZone和objc_msgSend这两个函数,第一次从objc_alloc进来是先跑到objc_msgSend,因为这个时候checkNil为true,allocWithZone为默认的flase,所以fastpath(!cls->ISA()->hasCustomAWZ())中的cls是还没有指针isa,所以为是flase,因此第一次进来跑了((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)),这个方法会触发objc_msgSend调用initialize类方法,所以第二次会跑到_objc_rootAllocWithZone里去
2.2 第二次调callAlloc
通过第一次的callAlloc的返回,然后打断点会发现再一次进入到callAlloc
第二次调callAlloc的时候,checkNIl是false,由于第一次调用objc_msgSend调了initialize方法,这时到bist是有值了,所以cls上是有了指针,那么hasCustomAWZ()这个方法是用来判断是否实现allocWithZone的自定义,可以看下这个方法;
bool hasCustomAWZ() {
return ! bits.hasDefaultAWZ();
}
这个allocWithZone是可以自定义的如果没有自定义,那么就会调用系统默认的allocWithZone方法。
综上,我们知道第二次调callAlloc程序会跑到_objc_rootAllocWithZone(cls, nil)里去,那我们去看下这个方法里实现了什么
2.3 _objc_rootAllocWithZone
先看下源码:
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
其实就是返回_class_createInstanceFromZone(cls, 0, nil,OBJECT_CONSTRUCT_CALL_BADALLOC)返回值。
2.4 _class_createInstanceFromZone
源码:
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
// 判断当前的class或supclass是否有.cxxConstruct构造方法的实现
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
// 判断当前的class或supclass是否有.cxxDestruct构造方法的实现
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
//计算需要的大小
// 进行内存对齐获得实例的大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
//初始化实例的isa指针
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
看到这么长的代码,首先可以判断这块代码是核心代码了,几个比较重要的点我在代码里进行注释
2.5结论
通过断点来进行源码的的解读,可以得到alloc的具体底层是这么一个流程:alloc->_objc_rootAlloc->callAlloc(cls,false,true)->allocWithZone->_objc_rootAllocWithZone->class_createInstance->_class_createInstanceFromZone
其中 _class_createInstanceFromZone比较核心的代码,下面我们来将这部分进行详细探索
3、init和new
init的内部其实是什么都不做,只是返回了一个self
这个是一种工厂模式,为了让你重写这个init方法,可以在init内部进行一些初始化的操作。
那么new方法其实就是alloc+init,这里就不多讲了
4、内存对齐
当我们创建对像的时候,肯定得给对象分配内存空间
_class_createInstanceFromZone里的这一句代码就是进行内存的对齐和分配
cls->instanceSize(extraBytes)
instanceSize方法的源码:
可以看到程序来到了word_align(unalignedInstanceSize())
就是x加上7再与上一个非7,
解释:比如一个数:0011, 7的二进制是0111, 7的取反是1000
首先加上7: 0011 + 0111 = 1010
再与上7的取反 1010 & 1000 = 1000 这一步相当于给前3位清0
最终得到的就是8的二进制1000,这个时候是不是就是对齐到8了,不信的话可以换个数自己来试试。
这个时候4位是不是1000,那个就是8。
效果可以这么理解:(x+7)>>3<<3就是x加上7然后在左移3再右移3
好了现在弄懂什么是内存对齐后,我们继续回到源码
这里有个cache说明这里有个缓存,进入fastInstanceSize(extraBytes)
这个align16就是16字节对齐的意思
这里就又个问题了,有缓存的时候是以16 字节对齐的,但没缓存的时候是以8字节对齐的,那么我们alloc方法是以16字节对齐还是8字节对齐的呐?答案在下问。
5、alloc的内存原理
上面了解了内存对齐后,我们在另一个源码libmalloc里去研究下allco的内存是怎么分配的,这个源码和苹果可能是不会完全重合的。
这个_malloc_zone_calloc方法层级比较深,不细讲这个,可以全局搜索_nano_malloc_check_clear
有个segregated_size_to_fit方法,里面其实就是我们前面讲到的字节对齐的算法来的,其中NANO_REGIME_QUANTA_SIZE这个变量是16 ,因此是一个16字节的对齐。现在可以回答前问的一个问题了,
alloc的创建对象是以16字节对齐的!
6、编译器的优化
我们在分析汇编的时候是没有看到 _class_createInstanceFromZone这个方法的,其实是编译器进行了优化得到才出现这种情况。可以进行下图的操作进行设置编译器的优化等级,越往下优化等级越高