OC底层探索06-isa本身藏了多少信息你知道吗?
一直都说类最终都会编译为struct,可是怎么验证呢?编译后的结构体内部都会有些什么东西呢?
查看Clang编译文件
//Clang默认依赖Foundation库
//当前目录下:把目标文件编译成c++文件.pp
clang -rewrite-objc main.m -o main.cpp
//编译目标文件内有UIKit等其他库需要导入依赖,
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m
//使用`xcode`安装安装的`xcrun`命令
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp (真机)
运行命令后,打开main.cpp。好长好长,但是无所谓我们关注的是:
- 类编译后是什么?
- 类里都有些什么?
- 看到
struct进一步验证了类是一个结构体。(之struct NSObject_IMPL NSObject_IVARS前,文章中提到类是继承结构体objc_object万物之源) - 结构体内部除了有
struct NSObject_IMPL NSObject_IVARS,成员变量:HRTestName- 检查编译文件中所有的类都会有NSObject_IMPL这个参数,那它一定就是
isa没跑了。 - 成员变量编译之后和类放在一起,而属性编译后并不是变量而是以
get,set方法的形式存在。
- 检查编译文件中所有的类都会有NSObject_IMPL这个参数,那它一定就是
联合体、位域
联合体
因为在isa使用了一种位域技术,来保存内部信息,这里简单介绍一下联合体、位域
联合体(union):各变量是“互斥”的,同时只能有一个变量有值。优点是内存使用更为精细灵活,也节省了内存空间。C语言共用体详解
位域
如果有一个需求,需要能表达东南西北四个方向。第一想到的就是创建4个Bool值来进行控制,可是4个Bool需要:4个字节。
现在需求变了,需要还能表达东南,西南,西北,东北,继续创建Bool来控制吗?而且太笨了。。。
如果通过这样一个结构来描述呢?
//伪代码
方向{
东 1 >> 0 //0001
南 1 >> 1 //0010
西 1 >> 2 //0100
北 1 >> 3 //1000
}
东:0001, 南:0010,西:0100,北:1000
东南:0011,西南:0110,西北:1100,东北:1001
只需要使用半个字节-4位就可以清楚描述这些信息。
这就是位域技术:通过位运算,将每一位都放入信息。
isa指针
在OC底层探索03一文中的alloc创建步骤3initInstanceIsa中提到了isa值的创建。通过查看iSA值的创建过程找到我们想要的答案。
//isa的类型
union isa_t {
isa_t() { }
Class cls;
uintptr_t bits; //自定义类信息会存在这里
struct {
// isa值的内容
ISA_BITFIELD;
};
};
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
...
//只放出核心代码
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
使用ISA_BITFIELD对isa的值进行了约定。
Style_月月-简书
根据位域的知识,再来看这幅图中的结构,有木有豁然开朗。
从上至下,对应二进制的从低位到高位。每一位都存满了各种类的信息。
需要的注意shiftcls这个位置,这个位置存储的就是类的信息,就是通过这个位置的信息,将isa和类建立了联系。
现在知道了isa中都有些什么信息。可是口说无凭,下面就来验证一下。
isa指针信息的LLDB验证
0x001d8001000033bd这个值就是isa。但是需要特别注意的是这个值并不是指针地址,它就是一个十六进制的值。这个点对本文的理解很重要。
lldb调试的一些常用命令
p 输出基本类型
p/t 输出二进制
p/x 输出十六进制
po 调用基本的description方法
x 打印十六进制地址
x/4gx 将十六进制分组方便观察,并打印4组
只要能通过lldb的调试从isa中找到类的信息,就可以验证之前的结论。
- 这是当前类的指针.
- 注意标红位置:
这个末尾指针是有规律的,后3位永远都是0.(这个是通过多次试验得出,如果有问题或者知道如何验证,希望不吝赐教)需要注意isa结构中类的信息是从第4位开始的,只要将isa的后3位改为0就可以直接得到类信息,所以在保存的时候需要将类指针进行位移(uintptr_t)cls >> 3;这种设计太巧妙了!!!
验证方法一
根据对ISA_BITFIELD的观察,shiftcls前有3位,后有17位。将这些位置都置为0,就可以得到isa中类的信息
验证方法一
这就是一个简单的位移运算,如果不明白自己动手试一下就知道了。
验证方法二
想要将这些位置改为0,当然也可以使用&运算
//__arm64__
define ISA_MASK 0x0000000ffffffff8ULL
//__x86_64__
define ISA_MASK 0x00007ffffffffff8ULL
这就是apple提供的掩码。也被称为isa的面具。
验证方法二
验证过程相对简单,这种方式也比较常用。
总结
apple工程师使用了位域的技术,在isa中保存了类的很多信息。这也是一种对于内存的优化。当然类里的其他信息-方法、属性,会在下文中进行解释。
再一次感叹apple工程师的强大,致敬!!!