OC底层原理01-alloc流程探索

一、探索历程

思考:从哪里开始探索? -> 对象的初始化?-> [对象 alloc]?

不管三七二十一,既然是探索alloc流程,那就先整一个alloc来玩一玩

  1. 创建一个GomuPerson对象


    image.png
  2. 初始化对象:通过下面一个小小的操作来引入思考
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

结论:

  1. 指向内存指针指针的指针地址不相同。不同的指针指向了共用同一块内存指针地址的对象
  2. 由p1,p2,p3的指针地址推断,栈内存是连续的(0x30 + 0x08 = 0x38, 0x28 + 0x08 = 0x30 [这里是16进制运算]),并且所占内存都是8字节(指针占内存8字节)
  3. init不会对我们开辟的对象的内存空间进行修改,地址指针的创建来自于alloc

附图


image.png

二、开始探索

首先想到就是 command + 鼠标左键点击 alloc 进入源码,但是失败了,发现这部分代码没有开源,那我们应该该怎么办呢?

苹果开源库:
1:https://opensource.apple.com/source 所以开源库都在里面,包括各个老版本
2:https://opensource.apple.com 可以根据系统版本选择更新的下载

知道哪里下载库了,那我们怎么知道alloc属于哪个库呢?

这里为大家提供3个方法,供参考

方法一: 下符号断点的形式直接跟流程

  1. 下符号断点的方法


    image.png
  2. 先在我们要研究的对象GomuPerson初始化处下一个断点


    image.png
  3. 执行程序到断点处
  4. 因为我们要研究alloc,所以下一个名为alloc的符号断点


    image.png
  5. 让程序继续走


    image.png
  6. 得到我们想要的东西了 libobjc.A.dylib,推测alloc 在这个库中
    image.png
  7. 这里为什么走到了[NSObject alloc]方法,而不是[GomuPerson alloc]?
  • 因为GomuPerson 继承 NSObject,而且 GomuPerson 里面没有 alloc方法

方法二:Ctrl + step into

  1. 首先在我们还是要研究的对象GomuPerson初始化处下一个断点


    image.png
  2. 让程序执行到这里之后,按住Ctrl,这个下一步的按钮就会变成如下图所示


    image.png
  3. 按住Ctrl,多点几次下一步,来到了 objc_alloc
    image.png
  4. objc_alloc作为符号断点,加上的一瞬间就又找到了libobjc.A.dylib
    image.png

方法三:汇编查看跟流程(最常用的)

  1. 首先在我们还是要研究的对象GomuPerson初始化处下一个断点


    image.png
  2. Debug -> Debug Workflow -> Always Show Disassembly 进入汇编


    image.png
  3. 得到 objc_alloc
    image.png
  4. 用方法二下符号断点又可以拿到libobjc.A.dylib

三、找到源码库libobjc.A.dylibobjc4

下载源码+编译源码请移步到 iOS_objc4-781.2 最新源码编译调试

四、打开源码工程,开始alloc探索之旅

方法一: 通过 command + 鼠标左键 一步一步进入源码看流程

  • 进入alloc
    image.png
  • 进入[NSObject alloc]
    image.png
  • 进入_objc_rootAlloc
    image.png
  • 进入 callAlloc
    image.png

由上面步骤我们可以先梳理一个流程如下:


image.png

那么问题来了:

问题一:进入callAlloc方法之后,是进入objc_msgSend呢,还是进入_objc_rootAllocWithZone?
问题二:我们根据代码走查梳理出来的步骤是否准确?

方法二:下符号断点,验证上面问题和流程

  • 我们下了如下3个符号断点


    image.png
  • 打开Debug -> Debug Workflow -> Always Show Disassembly 进入汇编然后打开上面3个断点,通过断点调试,得到以下流程


    image.png

结论:

  1. 我们刚刚走查代码梳理的流程错误?因为缺少了callAlloc这步
  2. 执行到callAlloc方法之后,下一步执行_objc_rootAllocWithZone,没有执行objc_msgSend

方法三:进入源码下断点,验证上面结论1和2。
问题:由于 objc_alloc_objc_rootAlloc 都是调用的 callAlloc,那他们到底是怎样调用的?是两个都要调用吗?那callAlloc岂不是要走2次?

  • 下四个断点如下图


    image.png

    image.png

    image.png

    image.png
  • 经过观察发现,objc_alloc_objc_rootAlloc 的第二个入参checkNil不同
    image.png
  • 经过一番断点调试得出objc_alloc_objc_rootAlloc都会调用,callAlloc确实要走两次,第一次调用objc_alloc 时,callAlloc方法会走最后一句return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)),接着调用_objc_rootAlloc方法时,callAlloc方法才会调用_objc_rootAllocWithZone方法,得出以下流程图
    image.png

那么问题又来了:callAlloc被调用了两次,为什么汇编调试的时候,一次都没有走符号断点呢?

这个问题我们下次单独一期来讲解,它牵涉到<编译优化>,有兴趣的朋友可以先自行研究。

继续探索_objc_rootAllocWithZone之后的流程

  1. 调用alloc的核心方法_class_createInstanceFromZone
    image.png
  • 经过一系列断点调试,发现_objc_rootAllocWithZone会走这三步,那我可以开始思考?alloc既然是开辟内存,那如果让我们开辟内存,我们会怎么做呢?1.计算内存大小。2.向系统申请内存。3.内存与类关联。从上图方法名我们也不难猜测出这3步的作用。得到以下的流程图:
    image.png

五、拓展知识

开辟空间是怎么进行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的流程又变了,如图:

image.png
结论:第二次初始化相同对象的时候,不会再走_objc_rootAlloc这个流程。

那为什么呢?

NSObject *obj = [NSObject alloc];
和上面初始化p1的流程一样
问题:为什么p1和obj都不走_objc_rootAlloc呢?

  • 先回答第一个问题:为什么[NSObject alloc],不走_objc_rootAlloc方法:
  1. 在main函数之前打个断点


    image.png
  2. 添加一个alloc的符号断点
    image.png
结论:原来进入main函数之前,_objc_rootAlloc函数已经被调用了,就相当于初始化p的流程,所以后面我们再次初始化[NSObject alloc]的时候,就和初始化p1的流程一样了。
  1. 然后我们取消main函数的断点
  2. 添加一个objc_alloc的符号断点
    image.png

    发现系统第一个调用的是NSArray
    image.png

    结论:NSArray继承于NSObject,系统初始化NSArray的时候,会调用callAlloc方法里的msgSend,由于NSArray没有alloc方法,所以这个消息会发送给根父类NSObject,得出NSObject的初始化方法由系统帮我们执行了。
  • 第二个问题:为什么+ (id)alloc {}下面的方面明明是_objc_rootAlloc,它为什么跑去调用objc_alloc
    进入LLVM开源代码,搜索objc_alloc会找到一个关键方法
    image.png
结论:调用alloc方法,在LLVM层(在编译启动就已完成),对alloc进行了修饰,会指向objc_alloc方法
  • 第三个问题:那为什么执行完objc_alloc方法后,又会执行一次alloc -> _objc_rootAlloc方法呢?
  1. 第一次调用objc_alloc方法,会执行到msgSend,进入LLVM源码查看
    image.png
  2. 第一次是调用if里面的条件判断,执行tryGenerateSpecializedMessageSend,传入的Selobjc_alloc,所以程序第一次走了objc_alloc
  3. 返回一个NO,接着执行下面的GenerateMessageSend,传入的Selalloc,所以第二次走到了alloc -> _objc_rootAlloc
  • 第四个问题: 第二次初始化GomuPerson对象流程 alloc -> objc_alloc -> callAlloc -> _objc_rootAllocWithZone 为什么第二次初始化对象的时候就不调用_objc_rootAlloc了呢?
  1. 先回顾一下第一次初始化对象的流程 alloc -> objc_alloc -> callAlloc -> objc_msgSend -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone
  2. 对比第一次和第二次发现,第一次调用 objc_alloc 走到了 callAllocobjc_msgSend方法,第一次调用 objc_alloc 走到了 callAlloc_objc_rootAllocWithZone方法。
  3. 差异在哪?第二次初始化对象,不会调用 objc_msgSend 方法。
  4. 那我们现在探索就变成 objc_msgSend 方法做了什么。
  5. objc_msgSend 就是通过 sel 去找 Imp,找到之后cache,既然有了cache,那我们第二次初始化GomuPerson对象的时候,就直接可以进入快速查询,直接找cache。(该部分内容属于<OC方法底层原理>,后面我们会专门开一期来阐述)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354