前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC对象原理(一)

OC对象原理(一)

作者头像
拉维
发布2021-10-08 15:20:03
2700
发布2021-10-08 15:20:03
举报
文章被收录于专栏:iOS小生活iOS小生活

已过而立之年,新的里程碑,新的起点。决定再拼一把,在专业深度上再上一层楼。


一、定位源码位置

我们要探究一个对象,那么就要找到其属性或者方法等所对应的源码。首先,我来介绍三种探索源码(即定位源码位置)的方式。

示例代码:

代码语言:javascript
复制
  LaVieLeader *leader1 = [LaVieLeader alloc];
  LaVieLeader *leader2 = [leader1 init];
  LaVieLeader *leader3 = [leader1 init];
  NSLog(@"\n%@, %@, %@", leader1, leader2, leader3);
  NSLog(@"\n%p, %p, %p", leader1, leader2, leader3);
  NSLog(@"\n%p, %p, %p", &leader1, &leader2, &leader3);

其打印结果:

<LaVieLeader: 0x600001d80520>, <LaVieLeader: 0x600001d80520>, <LaVieLeader: 0x600001d80520>

0x600001d80520, 0x600001d80520, 0x600001d80520

0x7ffee7cd40c8, 0x7ffee7cd40c0, 0x7ffee7cd40b8

我们发现,leader1、leader2和leader3的对象和对象地址的打印结果都相同,但是&leader1、&leader2和&leader3却不同。要知道为什么,就要看其源码。接下来我们就来探索一下alloc的源码。

1,直接代码中下普通断点

(1)

(2)Step Into Instruction

即摁住Control键,然后点击如下符号

(3)最终会定位到libobjc.A.dylib`objc_alloc:

需要注意的是,我们要使用真机调试才会定位到libobjc.A.dylib`objc_alloc,而使用模拟机是不能进入到libobjc.A.dylib`objc_alloc的,因为真机是arm64,而模拟机是x86,二者是不一样的。

2,下符号断点

(1)

(2)Symbolic Breakpoint

下符号断点

在Symbol里面填写符号标识,要定位哪个方法就填写哪个方法名:

(3)下完符号断点,在第一步的断点处,直接点击下一步,就会定位到libobjc.A.dylib`+[NSObject alloc]:

需要注意的是,第一步的断点很重要,如果我们不在对应的位置加上普通断点,而是直接加上第二步的符号断点,那么我们就不知道定位的是哪一个对象的alloc方法。

3,通过汇编

OC是一门高级语言,最终还是会转换成能被机器识别的汇编语言。所以我们可以直接查看汇编源码来定位。

(1)允许展示汇编源码

(2)打一个普通断点

(3)运行,就会定位到第二步所打断点的汇编源码处:

(4)找到打断点方法(alloc)所对应的C方法(objc_alloc),然后打个断点:

(5)Step Into Instruction(Control+Step Into)

最后会定位到libobjc.A.dylib`objc_alloc:

以上就是定位源码位置的三种方式。

二、汇编源码

前面,我们已经定位到了alloc的方法源码是在libobjc.A.dylib中,接下来我们就要找到libobjc.A.dylib这个库的源码。

苹果爸爸 开源了部分源码,我们访问如下网址就可以找到苹果所有的开源代码:

代码语言:javascript
复制
https://opensource.apple.com/tarballs/

我们找到objc4/文件夹,然后下载最新的objc4-756.2即可。

后面的分析,都是基于objc4-756.2源码。编译运行之后,截图如下:

由于我们是要找alloc方法的源码,而每一个OC方法都是通过大括号来实现的,所以我们全局搜索alloc {,结果如下:

然后我们依次点击对应的方法和函数,就会找到alloc方法的调用线:

alloc->_objc_rootAlloc->callAlloc,最后会找到callAlloc函数:

代码语言:javascript
复制
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

alloc的流程图如下:

接下来我们按照上述方法函数调用线,来添加符号断点,如下:

然后运行程序,就会依次定位到对应的符号断点处。需要注意的是,我们先将排在后面的符号断点给关掉,然后定位到前面的符号断点处,再打开接下来的符号断点,这样的话才可以定位到我们所要研究的对象所调用的方法。

alloc方法的源码实现如下:

代码语言:javascript
复制
+ (id)alloc {
    return _objc_rootAlloc(self);
}

所以我们将断点定位到_objc_rootAlloc函数进行研究:

然后在控制台读取寄存器(register read)

所谓的寄存器,就是存储指针的容器。这里的x0、x1、x2......等,是用于程序调用的参数传递。

需要特别注意一下x0。x0在寄存器中排在第一个,所以x0是第一个参数的传递者,但同时在返回的时候也是返回值的存储地方

我们都知道,alloc的作用是给对象申请内存,那么是如何实现的呢?使用汇编来分析确实是可以分析的,但是很难跟踪,所以并不推荐大家使用。接下来我将给大家介绍一个简洁的方法。

三、直接源码编译来分析

这个简洁的方法就是直接编译objc4源码。这里可以参考文章《iOS_objc4-756.2 最新源码编译调试》进行配置:

代码语言:javascript
复制
https://juejin.im/post/5d9c829df265da5ba46f49c9

好,配置完了之后,我们运行最新的objc4-756.2代码,我们定位alloc,最终会定位到如下代码:

代码语言:javascript
复制
if (!zone  &&  fast) {
    obj = (id)calloc(1, size);
    if (!obj) return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
}

下面对该代码一一解说。

calloc是开辟一块内存,该内存就是一个实例对象,但是此时该实例对象的内存空间还不能和任何的类对象产生关联。

initInstanceIsa是初始化上面开辟出来的内存空间的isa指针,也就是将实例对象内存空间与其Class关联起来

size指示应该为对象开辟多少大小的内存。

关于这个size,也就是给对象开辟的内存空间大小,有如下两个结论:

1,size必须是8字节的倍数,也就是8字节对齐

之所以必须是8字节的倍数,主要是为了方便CPU进行内容的读取。我们设置了必须是8字节的倍数的这个规定,那么CPU就会以8字节一个单位进行读取操作,不然的话,它就不知道下一次读取该读取多大的内存,这势必将影响CPU的读取效率。但是这样做有一个弊端,也就是会浪费部分内存空间。也就是说,我们是用空间换取时间

2,size最少是16字节。这是为了预留出一些内存空间以应对特殊情况。

四、查看内存段的存储

前面我们知道了,一个对象的内存大小是8字节的倍数,我们接下来就来看看如何读取对象的内存段。

在某处打好断点,程序跑到该断点处的时候,在编译器输出栏,进行如下输入:

x leader1 的作用是以16进制打印leader1对象的地址空间

需要注意的一点是,这里的0x282424470是栈顶指针(即isa)的起始位置,所以 po 0x282424470 的结果就是leader1所对应的类对象LaVieLeader。

还需要注意的一点是,直接通过x leader1打印出来的地址空间是iOS小端模式,也就是说,其地址空间是反的。

除了x leader1,其实我们还可以通过x/4xg leader2来读取对象的内存空间:

x/4xg leader2的作用是:读取leader2对象的前四个单位(每一个单位是8字节)的内存空间

五、init的作用

上面我们知道了,alloc开辟内存空间;那么init做了什么呢?

代码语言:javascript
复制
id
_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有什么用呢?

init的作用主要是系统提供给我们一个接口,我们可以通过重写该方法来初始化自定义的一些属性。这其实是工厂模式的一种体现。

六、LLDB调试

控制台调试是一名高级开发工程师必须掌握的技能,我们可以通过在l'ldb控制台中输入 help 来查看lldb调试的文档:

接下来我们再看看po这个命令是干啥的:

代码语言:javascript
复制
(lldb) help po
Evaluate an expression on the current thread.  Displays any returned value with
formatting controlled by the type's author.  Expects 'raw' input (see 'help
raw-input'.)

Syntax: po <expr>

Command Options Usage:
  po <expr>


'po' is an abbreviation for 'expression -O  --'

可以看到,po是以当前对象的expression方法进行返回的,而有些变量的值是无法po出来的,此时就可以通过如下指令读取出来:

代码语言:javascript
复制
(lldb) e -f f -- 0x4050800000000000
(long) $3 = 66

lldb有很多的调试指令,熟练掌握的话,将大大提高debug效率。

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档