引子
一说到这个alloc,相信最为一个一直面向对象却还是单身的iOS程序员来说,简直再熟悉不过了,其出现的频率,可以说是every day。这样熟悉的代码,也能发现新大陆哦。小板凳摆好,瓜子、啤酒、八宝粥。。。嗯嗯,直接上代码:
TestPerson *baseP = [TestPerson alloc];
TestPerson *person1 = [baseP init];
TestPerson *person2 = [baseP init];
TestPerson *person3 = [TestPerson alloc];
NSLog(@"%@-------%p-------%p",baseP,baseP,&baseP);
NSLog(@"%@-------%p-------%p",person1,person1,&person1);
NSLog(@"%@-------%p-------%p",person2,person2,&person2);
NSLog(@"%@-------%p-------%p",person3,person3,&person3);
打印得到的结果:
<TestPerson: 0x600001564080>-------0x600001564080-------0x7ffee3e41d28
<TestPerson: 0x600001564080>-------0x600001564080-------0x7ffee3e41d20
<TestPerson: 0x600001564080>-------0x600001564080-------0x7ffee3e41d18
<TestPerson: 0x600001564090>-------0x600001564090-------0x7ffee3e41d10
仔细观察下打印的结果,哟,baseP、person1、person2三者的之间的对象地址竟然是一样的,但是那个指针地址却又不一样。
我们再来看下person3,好家伙,它和baseP、person1、person2不仅对象地址不一样,指针地址也不一样。。。。。。
看到这里,是不是该有点小疑问了😁 产生what is it弄啥嘞的疑问了,(注:此处只有:【有】这个单选,不然小编表示泪奔三千里(^▽^)~ ~ ~ ~ ~ ~ )
从打印结果来看,person3、baseP、person1、person2的指针地址的变化,
0x7ffee3e41d10 + 0x8 = 0x7ffee3e41d18;
0x7ffee3e41d18 + 0x8 = 0x7ffee3e41d20;
0x7ffee3e41d20 + 0x8 = 0x7ffee3e41d28;
都是8字节偏移量,而且还是连续的。再有,baseP、person1、person2的内存地址都是0x600001564080,而person3的内存地址是0x600001564090。
所以,使用 alloc 初始化,是要开辟新的内存空间的,而 init 则不能开辟新的内存空间。栈开辟空间是连续的,开辟的内存是由高地址指向低地址。
当然了,说到栈,就不由想到堆,那么堆开辟空间,是由低地址指向高地址。
看到这里,是不是还是比较朦朦胧胧,这小编到底要说明个啥?嘿嘿,前面是引子,通过地址的变化,可以看出熟悉的alloc好像还有很多我们不熟悉的地方,那么接下来,就我们就一起探寻下alloc中我们不熟悉的区域。(大神们,可以直接飘过了,如有不足之处,相当欢迎斧正哈。。。。)
在开始之前,需要进行一些准备工作
到苹果开源库下载源码,源码地址
(下载后的源码是不能直接运行的,需要进行相应的配置,这里,小编就直接提供一个github里面一个配置好的、很不错的Demo,里面还有详细的配置步骤----GitHub)
使用三种探寻底层的方法
当我们自己写一些demo,想去查看alloc的具体操作的时候,苹果只是提供了相应的API,但是没有具体的实现可以用来研究。哼哼,以为这样就能让我们屈服了吗?
这里,小编分享 三种 探寻alloc的实现流程方法 (^▽^),(低调低调)
方法一、通过符号断点跟踪流程
看图操作哈,运行代码,到断点处时,就可以按住
control 键,再点击 step into 开始单步执行。单步执行到对应的底层符号断点处。
在这里就单步执行两步,就进入到objc_alloc方法里面
那么我们拿到了objc_alloc方法,就可以用符号断点进行调试
我们可以去掉最开始时,在TestPerson*baseP = [TestPerson alloc]; 这行代码处打的断点了。直接使用我们的符号断,运行工程。
运行后,断点就直接来到我们objc_alloc方法上面来了。我们还可以在汇编当中,看到紧接着还会进入到_objc_rootAllocWithZone方法,然后再是objc_msgSend。
根据这个方法,通过符号断点,去断_objc_rootAllocWithZone方法,看接着又调用了哪些方法,以此类推,我们就可以调试到我们需要调试的地方。
(啊哈,风停了、雨歇了,我感觉自己又行了😁)
方法二、通过汇编进行断点调试跟踪
我们还是要在
TestPerson *baseP = [TestPerson alloc];设置断点。那么,这个时候,debug得设置成汇编模式。
设置步骤:Xcode -> Debug -> Debug Workflow -> Always Show Disassembly
设置好了之后,我们就开始运行工程了。
通过汇编,当前断点在第7行,我们在第九行设置断点,断住objc_alloc符号。
当执行到第九行断点时,通过step into进入到objc_alloc方法里面。
当然,这里也可以使用:按住 control 键,再点击 step into 开始单步执行,这样的方法。
方法三、直接添加想要搜索的符号断点跟踪
就比如,现在要断alloc方法,那么就直接下符号断点(简单、cu bao)
哦豁,这个厉害,一步到位,但是。。。。。。嘿嘿 这种方式,只有在你需要断住的地方,再把断点激活,可以在需要断点的前一行再下个断点。等运行到的时候,再设置符号断点。
如果是全程都是在激活状态的话,想想工程里面得有多少个alloc调用,想想就爽。(借你一把锤子,合金的😁)
看到这里,恭喜读者,喜提三种调试方法。但是,又见但是。。。。。这三种方法,虽然有效,但是效率太低了,每次都得不断的进行断点跟踪,不仅繁琐,还还很麻烦。调试的时候,一个不小心,手抖了下,多点击几次,跳过了某个方法,不是又得重来,说起来,就一把泪啊,总得想个比较稳点的方法。嗯嗯。去厕所思考下。。。。。。
有了,如果我们把苹果提供给我们的 objc 源码编译成工程运行起来,嘿嘿嘿,那还不是想怎么样,就怎么样,O(∩_∩)O哈哈~
通过源码编译,进行alloc的源码探究
根据前文中提到的可以编译的源码下载下来,运行后,来跟踪alloc的具体流程。
首先下断点,进入到alloc源码实现里面
+ (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 大概率为假
//fastpath 大概率为真
//其实将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));
}
紧接着,再查看_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查看实现
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
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
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) {
//将cls类和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);
}
根据我们梳理的流程,可以得到一个流程图
接下来,再对其中的细节再进行分析
首先是计算内存大小的地方
进入instanceSize方法,查看实现
inline size_t instanceSize(size_t extraBytes) const {
//快速计算内存大小
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
//计算类中所有变量需要的内存大小 extraBytes额外字节数一般是0
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
//最小返回16字节
if (size < 16) size = 16;
return size;
}
再进入cache.fastInstanceSize方法,查看实现
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
进入 align16方法,查看实现
static inline size_t align16(size_t x) {
//16字节对齐算法 &为与操作 ~为取反操作
return (x + size_t(15)) & ~size_t(15);
}
再深入继续研究下align16这个东西的具体过程,需要有点位运算的小基础哦😁
x + size_t(15)) & ~size_t(15)
&:为与操作 ~:为取反操作
&(与)-- 规则是:全部为1则为1,反之则0
~(取反)-- 规则是:1变0,0变1
以9为例哈
9+15=24
24:0001 1000 (二进制)
15:0000 1111
15(取反):1111 0000
0001 1000
1111 0000
= 0001 0000
结果:16
那么我们就能得到:
方法align16,其实际上就是取n * 16 (n为正整数),就比如(m + 15)= sum,m为任意正整数,sum是16的整数倍,sum / 16,得到的还是一个整数,余数舍去。这个,就是16字节对齐。
那么,为什么要16字节对齐了?
1、提高性能和读取速度。因为CPU在读取数据时,是以字节块为单位进行读取的,如果频繁的读取字节未对齐的数据,降低了CPU的性能和读取速度,所以这样做其实就是用空间换取时间。
2、更安全。之所以是16字节对齐,而不是8字节对齐,因为对象中的isa指针是占8字节的,如果只有8字节的空间单位的话,一个isa指针就占满了,不预留空间,可能造成这个对象的isa指针和另一个对象的isa指针紧挨着,容易造成访问混乱。所以16字节对齐,会预留部分空间,访问更安全
分析完内存计算,就得接着分析calloc,也就是开辟内存,返回地址指针,进入到该方法里面查看实现
通过 instanceSize 方法,计算出所需的内存空间大小,接着就向系统申请 size 大小的内存,赋值给 objc,因此objc是指向内存地址的指针。
来来来,验证下:
从图中可以看出,当在第一个断点的时候,obj还没有进行赋值,此时有地址值,说明系统给开辟一个占位内存地址;
当执行calloc方法过后,所打印出来的地址(指针地址);
但是和平常见到的地址指针(<TestPerson: 0x7ffee3e41d10>)不一样,为什么呢?
1、因为obj地址还没有和传入的cls进行关联;
2、就是calloc只是开辟内存空间;
接着分析initInstanceIsa方法,初始化指针 ,和类关联起来,查看其实现
进入 initInstanceIsa 方法,查看实现
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
执行calloc之后,内存已经分配,initInstanceIsa初始化isa指针,并将isa指针指向已经分配好的内存地址,再将isa指针与cls类进行关联。
在isa指针初始化以后,打印objc的结果是
到了此处,欧耶,大功告成,alloc的底层探究,就完成了,有木有点收获啊,(不许没有啊<( ̄▽ ̄)/)
其实,alloc的主要目的就是开辟内存,并使得isa指针和cls类进行关联。
感谢各位的光临~ ~ ~ ~ ~ ~