开篇
好好学习,不急不躁。每天进步一点点。
课程回顾补充
1、isa 字节大小
上一次,我们对isa进行了推导,而可能对 isa 的大小为 8 字节,可能会存在疑惑,那么我们在这里补充说明一下;
通过源码,我们直接进入NSObject内部
可以看到,isa是一个Class
类型;全局查找Class
通过上图可以看出,Class 是一个结构体指针,别名为Class,但真正对应底层llvm为objc_class * 类型;
之前了解过,一个结构体指针,他的大小为8
字节;所以这里的isa也就是 8
字节大小;
2、联合体内部分析
之前对 isa_t
这个联合体位域的内部结构,进行分析,可能有些不明白,在 __x86_64__
下,shiftcls
为什么会在是44位,而其他的为什么会是17
位,和3
位;
从上图,为们可以看到,前3位,占据3个字节, shiftcls占据 44 字节,后5 位占据 17字节。我们iOS是小端模式,所以是从右往左读取,也是从右往左存储;
如下图:
类信息分析
1、ISA
1、isa 走向流程
上一次,我们了解了对象的本质,以及 isa 的简单推导。今天我们来分析一下类的原理走向,以及深入了解经典的 isa 走向图;
走代码,查看类以及其底层关系;
LGPerson *p = [LGPerson alloc];
声明一个 LGPerson 类;
通过 p/x p,查询类信息;再通过x/4gx 获取地址内存信息;使用 isa 与 mask,我们将得到这个类
的地址;
复制代码
可能会好奇 为什么要 与 上 mask 呢?
与上mask,其实是 `首地址位运算`,之前我们讲过,类的信息,都是存储在 `isa_t` 的
`shiftcls`中,而`shiftcls` 如上图,是位于 3 字节之后,所以为了能 取出 `shiftcls`,
我们需要进行位移运算,而mask是系统提供的,在 _x86_64系统下的掩码,可以快速运算;
复制代码
通过上面的操作,我们通过地址,可以得出类地址,那么这个类地址,他的更底层,又是什么信息呢?
来一波骚操作,我们使用这个类地址,看看这个地址,会有什么样的内存信息呢?
通过实践,LGPerson的类地址,还能继续打印出类信息,而且类地址打印的类信息,与上面打印的不一样;
继续实践,尝试使用这个内存结构的isa去与上mask
纳尼?????怎么会一样???就 alloc 一个 LGPerson,怎么会有两个地址?
验证一下,通过多种方式获取看看类对象在内存里,是不是存在多份。。。。。。
void lgTestClassNum(void){
Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}
复制代码
结果显示,只有一个地址,而且跟上面打印的第一个LGPerson地址一样,那么,上面的第二个地址,是什么呢?为什么会是LGPerson?
这就引出了,今天我们要讲的,类他爹 ---- > 元类
回顾一下,刚才的流程:
通过对象的isa,我们摸到了类,通过类的isa,我们摸到了元类
但是,在iOS中,从未有过元类这个定义;我们换另一种方式,查看一下元类是否真实存在;
将工程的可执行文件,拖入MachOView工具中,查看oc底层文件
在工程内,我们并没有对Metal Class 进行声明,由次可以,Metal Class 是系统底层自动声明的。也就不需要我们负责与维护。
那么元类,是否还能指向其他不为人知的类吗? 我们尝试继续打印,看看能不能得出元类的更底层;
通过对LGPerson层层摸索,最终我们摸索到NSObject,那这个NSObject,与我们直接使用NSObject对象,有什么关系吗?
2、isa 走向总结
我们通过这个流程,绘制一个简单的流程图,总结一下isa的走向;
通过对象的isa,我们探索到了
类
,再通过类的isa,探索到元类
,元类isa,继续探索到根元类
,也就是NSObject,再继续探索,就形成了闭环,回归NSObject。
并且我们发现,NSObject 作为根类,他的走向图,只进行了2次走向,就回归NSObject,所以NSObject的走向,可以总结为:根类 isa —> 根元类 isa —-> 根元类 isa
通过这些探索,我们统一思想,iOS里,任何对象,他的元类的父类,都是根元类,也就是NSObject,所以NSObject是一切类的基础。
2、类继承链
我们都知道,iOS 中,类是由继承关系的。但是最终的父类是谁,父类又源至谁,我们都比较模糊,接下来,我们就彻底理清一下这些继承关系;
LGTeacher继承LGPerson,但是在寻找底层元类的时候,他没有直接显示出,最底层的根元类,我们依照之前的思路,分析一下:
以LGTeacher为分析对象,他继承LGPerson,对象isa —> 类isa — > 元类,相当于说,LGTeacher指向元类后,被元类截胡了,不在由自身对象直接去指向根元类,而是由元类,再开一个分支,去指向根元类;
所以说,对象会有一条isa走向链,元类会有一条isa走向链,根元类也会有一条is a走向链;同理,继承链
也是如此;
并且,在我们通过元类获取根元类,根元类获取父类时,得到的值,是null,也就是说,NSObject的父类是null
由此,我们引出了一张,非常经典的,继承关系链走位图
类分析
1、类的内部结构分析
分析完ISA和继承链,接下来,我们对类的内部结构,进行分析;
我们都知道,一个对象,内部存储有 isa,shiftcls, 成员变量等数据;那么类呢,类的内部会是什么样的存储结构呢?
我们通过 x/4gx LGPerson.class,可以直接打印出LGPerson的内部信息;
类的底层,其实就是objc_object,所以我们需要通过源码,去分析类内部信息;
通过源码,搜索 objc_class ,查找他的声明,其中在runtime.h中,我们发现一处声明比较特殊,在此记录一下:
在 objc_runtime_new.h
文件中,我们找到了现在使用的 objc_class的声明方法,里面声明了:
struct objc_class : objc_object {
......
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
......
}
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
......
后面还有不少代码,可自行查看源码
复制代码
Class superclass
:存储当前的父类;
cache_t cache;
: 用于缓存指针和 vtable,加速方法的调用;
__has_feature
:判断编译器,对()内的条件,是否支持;
ptrauth_calls
:针对 arm64 架构,进行指针验证;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构,具体可参考苹果原文;
developer.apple.com
class_data_bits_t bits;
:相当于 class_rw_t 指针加上 rr/alloc 的标志,bits
用于存储类的方法、属性、遵循的协议等信息的地方;
class_data_bits_t 内部,包装有写入bits的方法 data(),最终返回class_rw_t的结构体
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
复制代码
class_rw_t
,内部,可以看到里面有method_array_t
,property_array_t
,protocol_array_t
等方法,接下来,我们就提出去这里的方法;
我们之前可以获取LGPerson类的地址,类的地址,其实就是当前内存结构里的首地址,我们可以按照内存平移的步骤,获取类里的bits;
2、获取类的内存结构
直接通过x/4gx QLYPerson.class,查看类的内存结构信息;
我们需要读取bits内部的,所以可以依靠内存平移的方式来读取,但问题是,我们不清楚,需要平移多少字节;我们需要分析内部结构,来确定,需要平移多少字节;
cache_t内存大小
当我们看到 cache_t内部结构的时候,啊~~~~,疯了。里面的代码太多了。这 TM 怎么计算大小?
在cache_t内部,有方法,全局变量;我们知道,方法是不占用结构体内存大小的,所以,哈~~,把方法干翻了,老子不看它。 全局变量也不占用我们的内存结构,它存在全局区,所以,干了它;
最后,我们得到平移位置为: 8+8+8+8 = 32 ;
然后进行地址平移
data() 信息读取
我们通过一些列大战,终于拿到了data(),可是data()内,并没有看到类的成员变量,还得继续查找,看看成员变量存放在哪里?
通过查看class_rw_t内部代码,它里面有method_array_t
,property_array_t
,protocol_array_t
等方法;如此,我们便可以通过这些方法,读取需要的数据;
我们通过打印 properties() 函数,能获取更深层的数据,但这并不是我们的需求,我们需要的是打印出 name 这个属性,但我们也因此知道,name 是存储在list_array_tt这个内部结构里;
唯有继续深究,查咯,看list_array_tt里,如何才能读取数据;
既然可以读取内部的元素,那么我们就可以通过方法,来读取数组里的数据;
上面打印properties()的数据时,我们看到内部有一个list,那么直接打出list看看;
至此,我们就很完美的打印出属性啦;
增加难度,我们试试,在QLYPerson内增加方法,和成员变量,看看是否也能打印出来
接下来,我们尝试获取方法
查看源码,看看 property 和 method 内部底层,有什么变化;
在property内部,我们获取的数据,来源于prpperty_list_t这个结构体,而这个结构体,不会对数据进行任何处理,直接存储进去;
实际上,我们读取出来的数据,就是 property_t这个值
可以看到,property_t这个结构体,内部是没有任何业务处理的,直接就是成员变量的方式进行读取;
而查看 method_list_t,method_t,我们发现,系统会对存入的数据,进行一番改造
method_t 内部,并没有直接的成员变量,所以我们无法直接通过get()去读取;
⚠️⚠️⚠️注意:但是我们可以看到,在struct big内部,有相应的成员变量;所以,我们可以直接获取这里的big,可以看到,他里面提供了,获取big的方法。
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big { // ⚠️⚠️⚠️注意:但是我们可以看到,在struct big内部,有相应的成员变量;
SEL name;
const char *types;
MethodListIMP imp;
};
........
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
........
复制代码