Objective-C 底层对象探究-上

目录

  • 1、背景
  • 2、底层探索的三个方法
  • 3、如何进行源码调试
  • 4、编译器的优化
  • 5、alloc的主线流程
  • 6、对象在内存中的结构

1. 背景

对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增加符号断点即可。
    ps:这里分享点汇编的小知识 我们看到的bl这个指令是属于 ARM 架构的汇编语法,他的功能是跳转到 0x104182564 这个地址去执行相关程序,并且把他下一步执行的地址0x1041821fc保存到 lr寄存器(又可以叫做x30寄存器) 中以便在objc_alloc执行ret命令之后可以回来继续往下执行

  • 通过符号断点快速定位:

    最后一种方法最暴力,当断点执行到ZXPerson *p1 = [ZXPerson alloc];时,我们直接添加alloc的符号断点即可马上定位。
    那么,到此我们3中探索的方法就结束完了。

    图片.png

3. 如何进行源码调试

当我们知道探索方法以及入口之后,我们怎么能有效的进行代码跟踪呢?如果是下载源码进行静态分析显然让人觉得不是那么爽,如果可以做到就跟调试我们自己编写的程序一样那就太完美了吧。是否真的能实现呢?答案当时是可行的,下面我们就来搞起!

首先我们现需要去苹果开源网站去下载源码,根据我们上面探索的结果发现alloc的底层都是由objc来负责的,所以我们需要的就是objc4-818.2的源码。但是!当你兴冲冲的下载完毕打开项目并且编译时,你就发现根本编译不通过会有很多错误。怎么办?

  • 方案一:大家可以参考这个文章来进行处理解决。解决源码顺利编译方法步骤

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

    图片.png

    图片.png
    以上都准备好之后我们来打开项目,我们创建一个target选择命令行工具。
    图片.png
    创建完成之后,将我们之前的ZXPerson拷贝到target目录下面。
    图片.png
    图片.png
    ps:注意这里有一个坑点,就是在 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的主线流程

  • 对象在内存中的结构

    到此本篇内容以及结束!如果您喜欢的话可以赏个赞!

你可能感兴趣的:(Objective-C 底层对象探究-上)