一、探索历程
思考:从哪里开始探索? -> 对象的初始化?-> [对象 alloc]?
不管三七二十一,既然是探索alloc流程,那就先整一个alloc来玩一玩
-
创建一个GomuPerson对象
- 初始化对象:通过下面一个小小的操作来引入思考
GomuPerson *p1 = [GomuPerson alloc];
GomuPerson *p2 = [p1 init];
GomuPerson *p3 = [p1 init];
//: 问题: p1,p2,p3 是相同的对象,还是不同的对象?
//: 猜是没有任何结果的,那我们不妨打印一下,让结果更直观的出现在我们面前,走你~
NSLog(@"p1 : %@",p1);
NSLog(@"p2 : %@",p2);
NSLog(@"p3 : %@",p3);
//: log 如下:
//: p1 : <GomuPerson: 0x600000376a40>
//: p2 : <GomuPerson: 0x600000376a40>
//: p3 : <GomuPerson: 0x600000376a40>
结论:
p1,p2,p3 对象的内存指针地址相同,既然他们的内存指针地址相同,那我们可以推断他们是相同的对象。
思考:那指向内存指针地址的指针地址是否相同呢?
那我们就打印一下指向对象指针地址的指针地址
NSLog(@"p1 : %p",&p1);
NSLog(@"p2 : %p",&p2);
NSLog(@"p3 : %p",&p3);
//: log 如下:
//: p1 : 0x7ffee62df138
//: p2 : 0x7ffee62df130
//: p3 : 0x7ffee62df128
结论:
- 指向内存指针指针的指针地址不相同。不同的指针指向了共用同一块内存指针地址的对象
- 由p1,p2,p3的指针地址推断,栈内存是连续的(0x30 + 0x08 = 0x38, 0x28 + 0x08 = 0x30 [这里是16进制运算]),并且所占内存都是8字节(指针占内存8字节)
- init不会对我们开辟的对象的内存空间进行修改,地址指针的创建来自于alloc
附图
二、开始探索
首先想到就是 command + 鼠标左键点击 alloc 进入源码,但是失败了,发现这部分代码没有开源,那我们应该该怎么办呢?
苹果开源库:
1:https://opensource.apple.com/source 所以开源库都在里面,包括各个老版本
2:https://opensource.apple.com 可以根据系统版本选择更新的下载
知道哪里下载库了,那我们怎么知道alloc属于哪个库呢?
这里为大家提供3个方法,供参考
方法一: 下符号断点的形式直接跟流程
-
下符号断点的方法
-
先在我们要研究的对象GomuPerson初始化处下一个断点
- 执行程序到断点处
-
因为我们要研究alloc,所以下一个名为alloc的符号断点
-
让程序继续走
- 得到我们想要的东西了
libobjc.A.dylib
,推测alloc 在这个库中
- 这里为什么走到了
[NSObject alloc]
方法,而不是[GomuPerson alloc]?
- 因为GomuPerson 继承 NSObject,而且 GomuPerson 里面没有 alloc方法
方法二:Ctrl + step into
-
首先在我们还是要研究的对象GomuPerson初始化处下一个断点
-
让程序执行到这里之后,按住Ctrl,这个下一步的按钮就会变成如下图所示
- 按住Ctrl,多点几次下一步,来到了
objc_alloc
- 把
objc_alloc
作为符号断点,加上的一瞬间就又找到了libobjc.A.dylib
方法三:汇编查看跟流程(最常用的)
-
首先在我们还是要研究的对象GomuPerson初始化处下一个断点
-
Debug -> Debug Workflow -> Always Show Disassembly 进入汇编
- 得到
objc_alloc
- 用方法二下符号断点又可以拿到
libobjc.A.dylib
三、找到源码库libobjc.A.dylib
即 objc4
下载源码+编译源码请移步到 iOS_objc4-781.2 最新源码编译调试
四、打开源码工程,开始alloc探索之旅
方法一: 通过 command + 鼠标左键 一步一步进入源码看流程
- 进入
alloc
- 进入
[NSObject alloc]
- 进入
_objc_rootAlloc
- 进入
callAlloc
由上面步骤我们可以先梳理一个流程如下:
那么问题来了:
问题一:进入callAlloc
方法之后,是进入objc_msgSend
呢,还是进入_objc_rootAllocWithZone
?
问题二:我们根据代码走查梳理出来的步骤是否准确?
方法二:下符号断点,验证上面问题和流程
-
我们下了如下3个符号断点
-
打开Debug -> Debug Workflow -> Always Show Disassembly 进入汇编然后打开上面3个断点,通过断点调试,得到以下流程
结论:
- 我们刚刚走查代码梳理的流程错误?因为缺少了
callAlloc
这步 - 执行到
callAlloc
方法之后,下一步执行_objc_rootAllocWithZone
,没有执行objc_msgSend
方法三:进入源码下断点,验证上面结论1和2。
问题:由于objc_alloc
和_objc_rootAlloc
都是调用的callAlloc
,那他们到底是怎样调用的?是两个都要调用吗?那callAlloc岂不是要走2次?
-
下四个断点如下图
- 经过观察发现,
objc_alloc
和_objc_rootAlloc
的第二个入参checkNil
不同
- 经过一番断点调试得出
objc_alloc
和_objc_rootAlloc
都会调用,callAlloc
确实要走两次,第一次调用objc_alloc
时,callAlloc
方法会走最后一句return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))
,接着调用_objc_rootAlloc
方法时,callAlloc
方法才会调用_objc_rootAllocWithZone
方法,得出以下流程图
那么问题又来了:callAlloc
被调用了两次,为什么汇编调试的时候,一次都没有走符号断点呢?
这个问题我们下次单独一期来讲解,它牵涉到<编译优化>,有兴趣的朋友可以先自行研究。
继续探索
_objc_rootAllocWithZone
之后的流程
- 调用alloc的核心方法
_class_createInstanceFromZone
- 经过一系列断点调试,发现
_objc_rootAllocWithZone
会走这三步,那我可以开始思考?alloc
既然是开辟内存,那如果让我们开辟内存,我们会怎么做呢?1.计算内存大小。2.向系统申请内存。3.内存与类关联。从上图方法名我们也不难猜测出这3步的作用。得到以下的流程图:
五、拓展知识
开辟空间是怎么进行16位内存对齐的(为什么16位?哈,苹果爸爸规定的,以前是8位)
通过一个很6的算法:(x + size_t(15)) & ~size_t(15)
//: 比如当前x 传入8字节
8 + 15 = 23
换算成16位,2进制:
0000 0000 0001 0111
//: ~size_t(15) 先把15换算成2进制
0000 0000 0000 1111
//: 取反 并与23 & 运算
1111 1111 1111 0000
0000 0000 0001 0111
0000 0000 0001 0000
//: 结果为16,从这个运算不难看出,15取反后后四位都是0,这个算法就是抹掉后四位,那就从倒数第五位开始运算,则结果都为16倍数
init
new
做了什么事?
- init
//: init源码
- (id)init {
return _objc_rootInit(self);
}
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
//: 从上面源码不难看出,init什么也没做,就返回了自己
结论:init的作用:构造方法,也是一种工厂设计,方便开发者重新,给开发者提供相应入口,比如:initWithFrame
- new
//: new源码
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
//: [GomuPerson new] 就相当于 [[GomuPerson alloc] init]
结论:经过一番断点调试,发现[GomuPerson new]和第一次GomuPerson *p = [GomuPerson alloc]
的流程一模一样,所以 new:相当于(alloc + init)
GomuPerson *p = [GomuPerson alloc];
GomuPerson *p1 = [GomuPerson alloc];
再初始化一个对象p1,会发现p1的流程又变了,如图:
结论:第二次初始化相同对象的时候,不会再走_objc_rootAlloc
这个流程。
那为什么呢?
NSObject *obj = [NSObject alloc];
和上面初始化p1的流程一样
问题:为什么p1和obj都不走_objc_rootAlloc
呢?
- 先回答第一个问题:为什么
[NSObject alloc]
,不走_objc_rootAlloc
方法:
-
在main函数之前打个断点
- 添加一个
alloc
的符号断点
结论:原来进入main
函数之前,_objc_rootAlloc
函数已经被调用了,就相当于初始化p的流程,所以后面我们再次初始化[NSObject alloc]
的时候,就和初始化p1的流程一样了。
- 然后我们取消
main
函数的断点 - 添加一个
objc_alloc
的符号断点
发现系统第一个调用的是NSArray
结论:NSArray
继承于NSObject
,系统初始化NSArray
的时候,会调用callAlloc
方法里的msgSend
,由于NSArray
没有alloc
方法,所以这个消息会发送给根父类NSObject
,得出NSObject
的初始化方法由系统帮我们执行了。
- 第二个问题:为什么
+ (id)alloc {}
下面的方面明明是_objc_rootAlloc
,它为什么跑去调用objc_alloc
?
进入LLVM开源代码,搜索objc_alloc
会找到一个关键方法
结论:调用alloc
方法,在LLVM层(在编译启动就已完成),对alloc
进行了修饰,会指向objc_alloc
方法
- 第三个问题:那为什么执行完
objc_alloc
方法后,又会执行一次alloc
->_objc_rootAlloc
方法呢?
- 第一次调用
objc_alloc
方法,会执行到msgSend
,进入LLVM源码查看
- 第一次是调用if里面的条件判断,执行
tryGenerateSpecializedMessageSend
,传入的Sel
为objc_alloc
,所以程序第一次走了objc_alloc
。 - 返回一个NO,接着执行下面的
GenerateMessageSend
,传入的Sel
为alloc
,所以第二次走到了alloc
->_objc_rootAlloc
- 第四个问题: 第二次初始化
GomuPerson
对象流程alloc
->objc_alloc
->callAlloc
->_objc_rootAllocWithZone
为什么第二次初始化对象的时候就不调用_objc_rootAlloc
了呢?
- 先回顾一下第一次初始化对象的流程
alloc
->objc_alloc
->callAlloc
->objc_msgSend
->alloc
->_objc_rootAlloc
->callAlloc
->_objc_rootAllocWithZone
- 对比第一次和第二次发现,第一次调用
objc_alloc
走到了callAlloc
的objc_msgSend
方法,第一次调用objc_alloc
走到了callAlloc
的_objc_rootAllocWithZone
方法。 - 差异在哪?第二次初始化对象,不会调用
objc_msgSend
方法。 - 那我们现在探索就变成
objc_msgSend
方法做了什么。 -
objc_msgSend
就是通过sel
去找Imp
,找到之后cache,既然有了cache,那我们第二次初始化GomuPerson对象的时候,就直接可以进入快速查询,直接找cache。(该部分内容属于<OC方法底层原理>,后面我们会专门开一期来阐述)。