这是我参与更文挑战的第1天,活动详情查看: 更文挑战
写了多年的OC代码,alloc是老朋友了,我们总是在创建对象的时候遇见他,但却没有探究过其背后到底做了什么?今天,我们就来一探究竟。
假设
在探索之前,我们先假设一下,如果是由你自己来实现alloc进行内存分配的话,我们会怎么去考虑这个问题呢? 我想无非是从以下这三个方面:
- 给谁?
- 给多大?
- 怎么给?
我们知道创建对象其实底层是对内存的分配,这三个方面放到内存分配中就刚好对应成下面三个关键操作:
- 给谁? >> 分配完之后,标记这块内存和类关联起来(分配完得标记这块内存是谁的)
- 给多大? >> 分配多大的内存(要分配的话,你得知道分配多大吧?)
- 怎么给? >> 进行分配
验证和探究
如何验证我们刚才的假设呢?俗话说的好:talk is cheap,show me the code,让我们把objc里面相对应的源码找出来。
等下,问题又来了,源码这么多,怎么知道要找哪一部分来看呢?这里有一个小技巧:
调试技巧
我们先在alloc行打上一个断点,然后执行,等断点断住的时候,再创建一个“符号断点Symbolic Breakpoint”:symbol字段填alloc,继续点运行,程序会继续往下然后断在一个汇编代码界面, 我们看看汇编代码开头的提示:
好了,知道我们要找的就是libobjc 相关的代码了。
准备工作
- 我们先从objc4-818.2 把源码下载下来。
alloc源码探索
下面对源码进行跟踪,先上流程图,从alloc方法开始,接下来的执行流程大概就是图上所画的:
图中我们可以看到,_class_createInstanceFromZone 方法中岔开了三个方法:
- cls->instanceSize : 计算内存大小
- (id)calloc(1, size) : 开辟内存,返回地址指针
- obj->initInstanceIsa :初始化指针,和类关联起来 刚好分别对应了我们文章一开头所说的三个操作。
这样看来_class_createInstanceFromZone就是分析的核心,岔开的三个方法就是三个核心方法。
没事,不急,我们先把_class_createInstanceFromZone之前的外围流程一步步分析完。
外围方法
最开始的入口
+ (id)alloc {
return _objc_rootAlloc(self);
}
进入_objc_rootAlloc
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
进入callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ //判断是不是 objc2.0版本
//slowpath(x):x很可能为假,为真的概率很小
//fastpath(x):x很可能为真
//其实将fastpath和slowpath去掉是完全不影响任何功能,写上是告诉编译器对代码进行优化
if (slowpath(checkNil && !cls)) return nil;
//判断该类是否实现自自定义的 +allocWithZone,没有则进入if条件句
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));
}
其中slowpath 和fastpath 宏的定义如下:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
__builtin_expect 起的是优化性能的作用,表示分支预测时第一个参数较大概率会是第二个参数,所以 fastpath(x) 表示 x 较大概率为真,slowpath(x) 表示 x 较大概率为假,返回值就是 x 本身。 这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)。 意思是:EXP==N的概率很大。
进入_objc_rootAllocWithZone
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->instanceSize【分配多大的内存】
- 需要多大的内存?
- 实际分配的内存(内存对齐操作,为了CPU处理更高效)
为什么需要16字节对齐
- cpu 读取数据是以固定字节块来读取的,这样就不需要频繁的变更每次读取的字节长度了,如果频繁的读取字节未对齐的数据,降低了cpu的性能和读取速度,这是一个用空间换取时间的做法。
- 更安全 由于在一个对象中isa指针是占8个字节,如果不进行节对齐 ,对象之间就会紧挨着,容易造成访问混乱。16字节对齐,会预留部分空间,访问更安全
(id)calloc(1, size) 【开辟内存,返回地址指针】
此时执行po obj能打印出地址值,说明系统已经给他分配了一块内存地址,# 但是和平常见到的地址指针(<类名: 0x100726d00> )不一样,为什么呢?
- obj没有和cls进行关联绑定
- 同时验证了calloc只是开辟了内存
Tips:函数malloc不能初始化所分配的内存空间,函数calloc() 会将所分配的内存空间中的每一位都初始化为零。 malloc与calloc用来动态分配内存空间,而realloc则是对给定的指针所指向的内存空间进行扩大或者缩小。
obj->initInstanceIsa【初始化指针 ,和类关联起来】
进入initInstanceIsa
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
现在isa指针初始化以后,我们再去打印objc,发现已经可以出现类名了,说明此时已和类进行了关联。
new的作用:
我们知道,创建对象除了alloc,还有另外一种常见的方式,就是new方法,这里把new方法的实现贴出来:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
其实new相当于alloc和init的组合。其缺点是,当我们调用new方法的时候,不会走自定义的init方法,而是直接走的NSObject的init,所以不建议开发时候使用。
结语
至此,我们的alloc探索也完成了。
总结:alloc的核心就是分配内存,而分配内存又包含三个关键操作:
- 分配多大的内存
- 进行分配
- 分配完之后,标记这块内存和类关联起来