「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」前端
ZXPerson *p1 = [ZXPerson alloc];
这段代码来已此做为咱们探索的入口。control
点击Setp into
以后,咱们会进入到汇编页面。alloc_test`objc_alloc
那么咱们就清楚alloc
以后是调用的 objc_alloc
这个函数,紧接着咱们进行对这个函数添加符号断点继续调试。
continue
继续跟踪,发现新添加的符号断点已经断住了,咱们从汇编界面分析在调用objc_alloc
以后,会调用一个叫作_objc_rootAllocWithZone
的函数,最后会调用一个objc_msgSend
的函数发送消息,然后就完成了alloc的流程。Debug
> DebugWorkFlow
>Always show Disassembly
以后运行。这时候咱们看到红框标示的那段语句,从后面的提示咱们就能够得知,这段代码实际就是调用了objc_alloc的方法了。后续咱们再根据objc_alloc增长符号断点便可。git
ps:这里分享点汇编的小知识 咱们看到的bl这个指令是属于 ARM 架构的汇编语法,他的功能是跳转到 0x104182564 这个地址去执行相关程序,而且把他下一步执行的地址0x1041821fc保存到 lr寄存器(又能够叫作x30寄存器) 中以便在objc_alloc执行ret命令以后能够回来继续往下执行
github
ZXPerson *p1 = [ZXPerson alloc];
时,咱们直接添加alloc的符号断点便可立刻定位。那么,到此咱们3中探索的方法就结束完了。当咱们知道探索方法以及入口以后,咱们怎么能有效的进行代码跟踪呢?若是是下载源码进行静态分析显然让人以为不是那么爽,若是能够作到就跟调试咱们本身编写的程序同样那就太完美了吧。是否真的能实现呢?答案当时是可行的,下面咱们就来搞起!web
首先咱们现须要去苹果开源网站去下载源码,根据咱们上面探索的结果发现alloc的底层都是由objc来负责的,因此咱们须要的就是objc4-818.2的源码。可是!当你兴冲冲的下载完毕打开项目而且编译时,你就发现根本编译不经过会有不少错误。怎么办?后端
方案一:你们能够参考这个文章来进行处理解决。解决源码顺利编译方法步骤sass
方案二(推荐):就是直接拿人家编译好的下载便可。最新macOS源码编译开源项目
markdown
以上都准备好以后咱们来打开项目,咱们建立一个target选择命令行工具。架构
建立完成以后,将咱们以前的ZXPerson
拷贝到target目录下面。app
在
ps:注意这里有一个坑点,就是在 Build Phases的 Compile Sources下把 main.m 文件夺挪到第一位来,要不可能不会触发断点,我不知道是否是个人Xcode(v12.5)问题,你们能够试一下
编辑器
main.m
文件里编写ZXPerson
初始化的代码,同时加上断点。
最后,在新建的target的build Settings
中搜索runtime
,找到Enable Hardened Runtime
选项,将其改成NO
;而后继续在Build Phases
的Dependencies
中引入objc
库。
接着激动人心的时刻来了,选则好target运行便可。当触发断点时点击Step into能够继续跟踪时咱们就大功告成了!
到这里我们已经具有进行源码调试的能力了,下面就能够探索一下alloc的主线流程啦。
ps:注意有一个调试技巧,每次想跟踪对象时,先把除 ZXPerson *p1 = [ZXPerson alloc]; 以外的断点关闭,等断点断在 ZXPerson *p1 = [ZXPerson alloc]; 时,再把相关的断点打开,不然会有其余对象触发断点,而就不是咱们想追踪的 ZXPerson 对象了。
这部份内容我只想简单的描述一下,不想作过多的解释,由于这个知识点咱们平时并不须要特别关注,只要理解原理便可。
原理: 当咱们编写完Objective-C程序完成编译以后,最终都会以汇编形式进行执行,那么在这个编译过程当中,编译器(LLVM)会对咱们的代码进行优化处理,具体他会根据程序来缩减、删除、简化等方式进行处理,例如:咱们定义了一个变量 NSString *str
可是并无使用它,虽然这个变量是存在于咱们的程序代码中的,可是最终编辑器会将这段代码进行删除,这个过程就是编译器的优化,你们只用理解这个概念便可。
在Xcode中控制优化等级:
Build Settings
中搜索optimi
回到看到一个Optimization Level
选项后面就是能够调整优化级别例如:Release
时默认就是最快且最小
模式,而在Debug
模式下就是默认不优化的状态。第一步:咱们先来到了objc_alloc
方法,经过名称咱们大体能够猜到,经过[cls alloc]
方式alloc对象时,都应该先走到这里。
第二步:来到allAlloc
方法,这个方法有几个分支咱们先不用管,先把分支打上断点,经过断点咱们发现这里直接走到最后return
语句,这段话经过objc_msgSend方法
向cls
类的alloc
方法发送消息。
第三步:咱们来到alloc
这个只是过渡方法直接无视继续往下。
第四步:来到objc_rootAlloc
方法,仍是过渡方法直接无视继续往下。
第五步:又来到了allAlloc
方法,此次进入了objc_rootAllocWithZone
方法。
第七步:又是一个过渡方法直接无视。
第五步:进入class_createInstanceFromZone
方法,咱们先经过这个方法返回值来分析,经过查看咱们发现返回的是一个叫obj
变量,在往上查找就看到了obj
变量的初始化代码,咱们能够总体的分析出来大体的逻辑,首先经过instanceSize
来获得对象在内存所需的大小;而后对obj
对象从新分配内存空间,这个obj
对象能够理解成是一个空对象,它自己并无说明含义,由于咱们alloc
的是ZXPerson
类的对象,因此还须要将obj
与ZXPerson
类创建绑定关系,而来联系这层关系的就是咱们熟悉的Isa
。后面hasCxxDtor
是将C++的相关功能也赋值给这个obj。
说了这么多咱们一块儿来验证一下obj对象的变化,直接上图更直观。
经过LLDB调试咱们分别打印了obj在内存里面的变化。最后返回obj
最后附上一个流程图:
一个类的实例在建立以后不添加任何代码的状况下,在内存中占用的大小是8字节,为何是8字节呢?由于实例对象在内存结构中存放在第一位的是Isa指针,而指针的大小就是占用8字节。咱们能够经过增长断点进行验证;
如上图,咱们能够再左侧实例对象中观察到Isa的指针地址为0x011d8001000080e9
,而后咱们利用LLDB在右侧输入x zxp
(显示 zxp 指针的内存状况),等待打印出结果后咱们就会看到,首个8字节的地址,由于iOS属于小端模式因此在读取内存时是从右往左读
,咱们能够p 0x011d8001000080e9
打印一下看看是否会显示Isa的内容,结果出来以后并无跟我预想的同样,缘由是须要&上ISA_MASK
,为何须要&上ISA_MASK
?ISA_MASK
值是什么?带着这两个问题咱们一块儿来寻找答案。
咱们从alloc流程中已经得知,与初始化Isa相关的事情都是在_class_createInstanceFromZone()
函数中实现的,那么我直接来到改函数的initInstanceIsa()
方法,而后跟进查看一下;
跟进以后发现了叫initIsa()
方法,继续前进。
到这里咱们看到了程序再给一个叫newisa
的对象赋值,而这个对象的类型是isa_t
,咱们都知道Isa指向的是该对象的类信息,这里已经明显的有setClass()
的方法,咱们只需看看是否有getClass
方法?该方法中是否有咱们想找的东西。
继续跟踪isa_t
,果真发现了getClass
方法,继续跟进查到了clsbits &= ISA_MASK
经过上面的注释,大体猜到是MASK是一个掩码,目的是为了屏蔽除了类指针与签名以外的一些东西。那么咱们再看一下ISA_MASK
内容是什么?
经过搜索咱们找到了。我这里由于是非ram64架构的,因此匹配到了这个0x0000000ffffffff8ULL
最后咱们来验证一下!果真打印的是Isa指针指向的类
刚才咱们是在不添加任何代码的状况下,如今咱们增长几个属性变量看一下内存的变化;而后咱们这回用过x/4gx zxp
方式对打印进行格式化(每隔4段以16进制的数据进行展现)结果以下:
咱们发现zxp
对象第一个位置仍是Isa
,后面的数据分别存储了zxName
、zxAge
、zxSex
、zxHieght
,优化的部分不知道你们是否看出来了,zxAge
、zxSex
由于是int类型(占4字节)与char类型(占1字节)因此共用了8字节的空间,这就是内存对齐
(有关内存对齐的内容我会在下一篇中介绍)。下面咱们分别来验证一下:
结构示意图: