iOS 底层探索篇 —— OC对象本质 & noPointerIsa
一. 对象的本质探索
Clang
clang
是一个由Apple
主导编写,基于LLVM的C/C++/OC的编译器
操作代码
实例
通过这个指令,就可以把main.m 编译成 main.cpp 文件,可以更好的观察底层的一些结构及实现的逻辑,方便理解底层原理。
对象的本质
对象在底层的本质是结构体
,那么是如何得到这个结论的呢。
首先我们在main.m 文件中声明一个类LGPerson
,有一个属性 NSString LSName
(为了避免是巧合,取一个特殊的名字)。
然后我们按照上文中的方法将 main.m 编译成 main.cpp 文件,然后将main.cpp 文件打开,并且搜索一下我们的类LGPerson。
我们可以看到LGPerson 中有两个参数
第一个参数就是isa
,是继承自NSObject_IMPL
,属于伪继承,伪继承
的方式是直接将NSObject_IMPL结构体定义为LGPerson中的第一个属性,意味着LGPerson 拥有 NSObject_IMPL中的所有成员变量
。
第二个参数就是我们的成员变量 LSName
。
同时我们注意到,在main.cpp中,LGPerson的本质类型是 objc_object
,这是为什么呢?
这是因为LGPerson继承自NSObject, 而 NSObject 在底层中的实现就是objc_object
,因此LGPerson的本质类型是objc_object
。
Class类型的本质类型
我们在main.cpp中寻找,找到上图可知, 得出class 是 objc_class 的结构体指针
。并且我们也可以得知为什么id 不加*, 因为底层中已经加过了。
getter 和 setter
继续在main.cpp 中寻找,我们找到
从名字以及参数中,我们大致可以看出这个是LGPerson 的getter 和setter 方法,但是这个参数我们从来没有见到过。这些参数是隐藏参数
。
为什么return 是 self + objc_ivar
呢?因为我们需要拿到内存地址才能拿到内存的值,拿到person首地址,然后拿到ivar 空间平移的量进行平移
,才能获得LSName所在的地址,拿到地址才能获取里面的值。
二. 联合体位域
在这个结构体中,存了四个bool值,每个bool值占一个字节,根据内存对齐原则,可以得出这个struct总共占了4个字节。
而用sizeof可以得出,这个struct真的是占用了4个字节。显然,bool 不是 0 就是1,实际上用4位去存就可以了,用四个字节去存有点浪费内存了。
位域
这里,我们可以用位域,来指定这个成员占多少位。举个🌰。
这里在成员后面加了: 1
,代表这个成员只占用1位,这样四个成员加起来就只占四位了。因为我们至少也需要一个字节,所以这个结构体就只占用一个字节
了。我们来验证一下:
从输出栏可以看到,car2 现在确实只占了1个字节。
结构体(struct)所有变量是共存的
结构体是指把不同的数据组合成一个整体,其变量是共存的,结构体内存 >= 所有成员占用的内存总和。
- 优点:是有容乃大,全面。
- 缺点:struct内存空间的分配是
粗放
的,不管用不用,全分配
。
联合体(union)中是各变量是互斥的
联合体也是由不同的数据类型组成,但其变量是互斥
的,所有的成员共占一段内存,占用的内存等于最大的成员占用的内存
。
- 缺点:不够包容,采用了
内存覆盖技术,同一时刻只能保存一个成员的值
,如果对新的成员赋值,就会将原来成员的值覆盖掉。 - 优点:内存使用更加精细灵活,也
节省了内存空间
。
三. noPointerIsa
我们先在源码中找到isa。
1. initIsa
2. initIsa(cls, false, false);
3. isa_t
4. union isa_t
到这里,我们发现了isa_t是一个联合体位域。因为指针有8字节,而类上有很多信息可以存储,如果8个字节只是用来存指针,那么内存就会大大的浪费,所以这里定义了一个结构体位域ISA_BITFIELD
,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本arm64
(对应ios 移动端) 和 x86_64
(对应macOS)
- arm64
- x86_64
nonpointer
:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等has_assoc
:关联对象标志位,0没有,1存在has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象shiftcls
:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。magic
:用于调试器判断当前对象是真的对象还是没有初始化的空间weakly_referenced
:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。deallocating
:标志对象是否正在释放内存has_sidetable_rc
:当对象引用技术大于 10 时,则需要借用该变量存储进位extra_rc
:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
针对两种不同平台,其isa的存储情况如图所示
- 实战演练:
我们根据打印出来的结果得知,0x001d800100008275 没有占满8个字节,否则应该是0xXX1d800100008275。
我们假设内存是0x111d800100008275,那么和0x001d800100008275相差就是1223979098744774912。
更直观一点,我们可以把64 位打印出来
从图片可以看到,64位中还有好多位没有被使用。
那么我们如何证明类的地址和对象的地址关联了呢。
首先我们找到之前的结构体位域ISA_BITFIELD
,如何在他的上面看到一个叫做ISA_MASK
的家伙,这个是ISA的掩码
。得到了ISA的掩码,我们只要拿到原来的地址,并于掩码进行与运算,就可以得到类的地址。
实际操作一下:
我们也可以通过位运算
来进行验证。我们之前得到结构体位域ISA_BITFIELD
,由于程序是运行在mac上的,所以此时的 shiftcls
占44位。
-
将isa地址
左移3位
:p/x 0x011d800100008275 >> 3,得到0x0023b0002000104e -
在将得到的0x0023b0002000104e
右移20位
:p/x 0x0023b0002000104e << 20 ,得到0x0002000104e00000 -
为什么是右移20位?因为先左移了3位,相当于向左偏移了3位,而右边需要抹零的位数有17位,所以一共需要移动20位
-
将得到的0x0002000104e00000 再
左移17位
:p/x 0x0002000104e00000 >> 17 得到新的0x0000000100008270, 可以看出来我们得到的和LGPerson.class
是一样的