Objective-C 底层对象探究-上

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!前端

目录

1. 背景

  • 学习不迷茫,无阻我飞扬!你们好我是Tommy!今天对iOS对象alloc方法进行了详细研究,目的是为了了解对象底层的本质、和对象在内存中的结构。若是你也有一样的兴趣?不要怀疑的阅读下去吧!~

2. 底层探索的三个方法

  • 经过符号断点:
    • 首先咱们将断点打到 ZXPerson *p1 = [ZXPerson alloc];这段代码来已此做为咱们探索的入口。
    • 开始编译运行以后咱们来到断点,经过按住control 点击Setp into以后,咱们会进入到汇编页面。

    图片.png

    • 在汇编顶部显示的alloc_test`objc_alloc 那么咱们就清楚alloc以后是调用的 objc_alloc这个函数,紧接着咱们进行对这个函数添加符号断点继续调试。

    图片.png 图片.png 图片.png 图片.png

    • 点击continue继续跟踪,发现新添加的符号断点已经断住了,咱们从汇编界面分析在调用objc_alloc以后,会调用一个叫作_objc_rootAllocWithZone的函数,最后会调用一个objc_msgSend的函数发送消息,然后就完成了alloc的流程。
  • 经过汇编:
    • 首先咱们先经过Xcode设置开启汇编显示功能,Debug > DebugWorkFlow >Always show Disassembly 以后运行。

    图片.png这时候咱们看到红框标示的那段语句,从后面的提示咱们就能够得知,这段代码实际就是调用了objc_alloc的方法了。后续咱们再根据objc_alloc增长符号断点便可。git

    ps:这里分享点汇编的小知识 咱们看到的bl这个指令是属于 ARM 架构的汇编语法,他的功能是跳转到 0x104182564 这个地址去执行相关程序,而且把他下一步执行的地址0x1041821fc保存到 lr寄存器(又能够叫作x30寄存器) 中以便在objc_alloc执行ret命令以后能够回来继续往下执行github

  • 经过符号断点快速定位:
    • 最后一种方法最暴力,当断点执行到ZXPerson *p1 = [ZXPerson alloc];时,咱们直接添加alloc的符号断点便可立刻定位。那么,到此咱们3中探索的方法就结束完了。

    图片.png

3. 如何进行源码调试

  • 当咱们知道探索方法以及入口以后,咱们怎么能有效的进行代码跟踪呢?若是是下载源码进行静态分析显然让人以为不是那么爽,若是能够作到就跟调试咱们本身编写的程序同样那就太完美了吧。是否真的能实现呢?答案当时是可行的,下面咱们就来搞起!web

  • 首先咱们现须要去苹果开源网站去下载源码,根据咱们上面探索的结果发现alloc的底层都是由objc来负责的,因此咱们须要的就是objc4-818.2的源码。可是!当你兴冲冲的下载完毕打开项目而且编译时,你就发现根本编译不经过会有不少错误。怎么办?后端

  • 方案一:你们能够参考这个文章来进行处理解决。解决源码顺利编译方法步骤sass

  • 方案二(推荐):就是直接拿人家编译好的下载便可。最新macOS源码编译开源项目 图片.png 图片.pngmarkdown

  • 以上都准备好以后咱们来打开项目,咱们建立一个target选择命令行工具。架构

    图片.png

  • 建立完成以后,将咱们以前的ZXPerson拷贝到target目录下面。app

    图片.png图片.pngps:注意这里有一个坑点,就是在 Build Phases的 Compile Sources下把 main.m 文件夺挪到第一位来,要不可能不会触发断点,我不知道是否是个人Xcode(v12.5)问题,你们能够试一下编辑器

  • main.m文件里编写ZXPerson初始化的代码,同时加上断点。 图片.png

  • 最后,在新建的target的build Settings 中搜索runtime ,找到Enable Hardened Runtime选项,将其改成NO;而后继续在Build PhasesDependencies中引入objc库。

    图片.png 图片.png 接着激动人心的时刻来了,选则好target运行便可。当触发断点时点击Step into能够继续跟踪时咱们就大功告成了! 图片.png 图片.png 图片.png到这里我们已经具有进行源码调试的能力了,下面就能够探索一下alloc的主线流程啦。

    ps:注意有一个调试技巧,每次想跟踪对象时,先把除 ZXPerson *p1 = [ZXPerson alloc]; 以外的断点关闭,等断点断在 ZXPerson *p1 = [ZXPerson alloc]; 时,再把相关的断点打开,不然会有其余对象触发断点,而就不是咱们想追踪的 ZXPerson 对象了。

4. 编译器的优化(LLVM优化)

  • 这部份内容我只想简单的描述一下,不想作过多的解释,由于这个知识点咱们平时并不须要特别关注,只要理解原理便可。

  • 原理: 当咱们编写完Objective-C程序完成编译以后,最终都会以汇编形式进行执行,那么在这个编译过程当中,编译器(LLVM)会对咱们的代码进行优化处理,具体他会根据程序来缩减、删除、简化等方式进行处理,例如:咱们定义了一个变量 NSString *str 可是并无使用它,虽然这个变量是存在于咱们的程序代码中的,可是最终编辑器会将这段代码进行删除,这个过程就是编译器的优化,你们只用理解这个概念便可。

  • 在Xcode中控制优化等级:

    • Build Settings 中搜索optimi 回到看到一个Optimization Level选项后面就是能够调整优化级别例如:图片.png图片.png
    • 这时咱们会发现,当处于Release时默认就是最快且最小模式,而在Debug模式下就是默认不优化的状态。图片.png

5. alloc的主线流程

  • 第一步:咱们先来到了objc_alloc方法,经过名称咱们大体能够猜到,经过[cls alloc]方式alloc对象时,都应该先走到这里。图片.png

  • 第二步:来到allAlloc方法,这个方法有几个分支咱们先不用管,先把分支打上断点,经过断点咱们发现这里直接走到最后return语句,这段话经过objc_msgSend方法cls类的alloc方法发送消息。图片.png

  • 第三步:咱们来到alloc这个只是过渡方法直接无视继续往下。图片.png

  • 第四步:来到objc_rootAlloc方法,仍是过渡方法直接无视继续往下。图片.png

  • 第五步:又来到了allAlloc方法,此次进入了objc_rootAllocWithZone方法。图片.png

  • 第七步:又是一个过渡方法直接无视。图片.png

  • 第五步:进入class_createInstanceFromZone方法,咱们先经过这个方法返回值来分析,经过查看咱们发现返回的是一个叫obj变量,在往上查找就看到了obj变量的初始化代码,咱们能够总体的分析出来大体的逻辑,首先经过instanceSize来获得对象在内存所需的大小;而后对obj对象从新分配内存空间,这个obj对象能够理解成是一个空对象,它自己并无说明含义,由于咱们alloc的是ZXPerson类的对象,因此还须要将objZXPerson类创建绑定关系,而来联系这层关系的就是咱们熟悉的Isa。后面hasCxxDtor是将C++的相关功能也赋值给这个obj。图片.png图片.png

  • 说了这么多咱们一块儿来验证一下obj对象的变化,直接上图更直观。

    图片.png 图片.png 图片.png

  • 经过LLDB调试咱们分别打印了obj在内存里面的变化。最后返回obj 图片.png

  • 最后附上一个流程图: 图片.png

6. 对象在内存中的结构

  • 一个类的实例在建立以后不添加任何代码的状况下,在内存中占用的大小是8字节,为何是8字节呢?由于实例对象在内存结构中存放在第一位的是Isa指针,而指针的大小就是占用8字节。咱们能够经过增长断点进行验证; 图片.png

  • 如上图,咱们能够再左侧实例对象中观察到Isa的指针地址为0x011d8001000080e9,而后咱们利用LLDB在右侧输入x zxp(显示 zxp 指针的内存状况),等待打印出结果后咱们就会看到,首个8字节的地址,由于iOS属于小端模式因此在读取内存时是从右往左读,咱们能够p 0x011d8001000080e9打印一下看看是否会显示Isa的内容,结果出来以后并无跟我预想的同样,缘由是须要&上ISA_MASK,为何须要&上ISA_MASKISA_MASK值是什么?带着这两个问题咱们一块儿来寻找答案。

  • 咱们从alloc流程中已经得知,与初始化Isa相关的事情都是在_class_createInstanceFromZone()函数中实现的,那么我直接来到改函数的initInstanceIsa()方法,而后跟进查看一下; 图片.png

  • 跟进以后发现了叫initIsa()方法,继续前进。 图片.png

  • 到这里咱们看到了程序再给一个叫newisa的对象赋值,而这个对象的类型是isa_t,咱们都知道Isa指向的是该对象的类信息,这里已经明显的有setClass()的方法,咱们只需看看是否有getClass方法?该方法中是否有咱们想找的东西。 图片.png

  • 继续跟踪isa_t,果真发现了getClass方法,继续跟进查到了clsbits &= ISA_MASK经过上面的注释,大体猜到是MASK是一个掩码,目的是为了屏蔽除了类指针与签名以外的一些东西。那么咱们再看一下ISA_MASK内容是什么?

    图片.png 图片.png

  • 经过搜索咱们找到了。我这里由于是非ram64架构的,因此匹配到了这个0x0000000ffffffff8ULL 图片.png

  • 最后咱们来验证一下!果真打印的是Isa指针指向的类 图片.png

  • 刚才咱们是在不添加任何代码的状况下,如今咱们增长几个属性变量看一下内存的变化;而后咱们这回用过x/4gx zxp方式对打印进行格式化(每隔4段以16进制的数据进行展现)结果以下: 图片.png 图片.png 图片.png

  • 咱们发现zxp对象第一个位置仍是Isa,后面的数据分别存储了zxNamezxAgezxSexzxHieght,优化的部分不知道你们是否看出来了,zxAgezxSex由于是int类型(占4字节)与char类型(占1字节)因此共用了8字节的空间,这就是内存对齐(有关内存对齐的内容我会在下一篇中介绍)。下面咱们分别来验证一下: 图片.png

  • 结构示意图: 图片.png

总结:

  • 咱们知道了如何经过三种方式来探索底层代码;
  • 经过下载编译好的源码项目,使咱们能够经过调试来进行探索。
  • LLVM是有优化策略的,能够在Xocde中能够手动修改。
  • alloc的主线流程
  • 对象在内存中的结构
写到最后
导航:
相关文章
相关标签/搜索