这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
一、isa分析到元类
上一篇isa分析,我们分析出对象的isa & ISA_MASK得到LRPerson类,那类中是否有isa呢,类的isa又指向什么呢?
接上篇对这个
LRPerson类再x/4gx查看内存,并打印出类的isa & ISA_MASK,也得到了LRPerson类。也就是说0x0000000100008388 0x0000000100008360都指向LRPerson。
可以看出类也开辟了内存,存放
isa和其他信息。那么我们不妨猜想一下:类和对象一样可以开辟内存,并且不止有一个类。下面尝试验证一下
查看下面这段代码打印:
可以看出类对象指向同一个内存地址
0x100008388,那上面类的isa & ISA_MASK得到的0x0000000100008360是什么呢?
此时借助MachOView查看可执行文件
在符号表中可以看到有个
METACLASS,这就是元类。元类是系统生成编译的。
到此可以分析出这个流程:
对象isa -> 类isa -> 元类
二、isa走位图和继承链
1、isa走位
上面分析到元类,那么元类的isa以及后续流程呢?
通过
lldb一步步分析打印,观察出以下结论:
对象isa->类类isa->元类元类isa->根元类根元类isa->根元类根类isa->根元类
通过代码验证下:
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class cass = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n-%p 实例对象\n-%p 类\n-%p 元类\n-%p 根元类\n-%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
得到打印结果:
-0x10104e840 实例对象
-0x100357140 类
-0x1003570f0 元类
-0x1003570f0 根元类
-0x1003570f0 根根元类
结合分析得出isa走位图:
2、isa继承链
在OC中,类是有继承关系的,那么元类 根元类有继承关系吗?下面探索一下,先创建一个类LRTeacher继承于LRPerson,在观察下面代码打印
// LRPerson元类
Class pMetaClass = object_getClass(LRPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"LRPerson元类的父类:%@ - %p",psuperClass,psuperClass);
// LRTeacher -> LRPerson -> NSObject
// 元类也有一条继承链
Class tMetaClass = object_getClass(LRTeacher.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"LRTeacher元类的父类:%@ - %p",tsuperClass,tsuperClass);
// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"根类的父类:%@ - %p",nsuperClass,nsuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(object_getClass(NSObject.class));
NSLog(@"根元类的父类:%@ - %p",rnsuperClass,rnsuperClass);
打印结果如下:
LRPerson元类的父类:NSObject - 0x1003570f0
LRTeacher元类的父类:LRPerson - 0x100008360
根类的父类:(null) - 0x0
根元类的父类:NSObject - 0x100357140
根据打印结果分析:
LRPerson元类继承于NSObject元类即根元类LRTeacher元类继承于LRPerson元类NSObject根类继承于nilNSObject根元类继承于NSObject根类
通过上面分析的isa走位图和继承链,结合起来就是苹果官网上经典的一张图
三、源码分析类的结构
对象的内存中有isa有成员变量,那么类的内存中有什么呢?我们在源码中可以看到类的根本是objc_class,是一个结构体。
objc_class结构体中有这四个成员变量:隐藏成员变量ISA,继承于objc_object、superclass父类、cache、 bits。前两个我们已经比较熟悉了,那么cache和 bits是什么呢?
先看bits后边注释中提到了class_rw_t,进入class_rw_t可以看到包含方法 属性 协议等等
struct class_rw_t {
...
const method_array_t methods() const {...}
const property_array_t properties() const {...}
const protocol_array_t protocols() const {...}
}
现在我们只走到objc_class,怎么从类中获取这些内存数据呢?
四、指针和内存平移
目前还不知道如何获取当前内存的结构,这里先看一下指针和内存平移。
// 普通指针
int a = 10;
int b = 10;
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
// 对象指针
LRPerson *p1 = [LRPerson alloc];
LRPerson *p2 = [LRPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
观察以上代码,得到打印结果如下:
10 -- 0x7ffeefbff4cc
10 -- 0x7ffeefbff4c8
<LRPerson: 0x100714f90> -- 0x7ffeefbff4c0
<LRPerson: 0x10070e660> -- 0x7ffeefbff4b8
分析:
普通指针2个地址不同,但指向的值是同一个,可以理解为指向同一个内存空间。这就是值copy。而两个对象指针的地址不同,指向的内存空间也不同。
再看下数组指针
// 数组指针
int c[4] = {1,2,3,4};
int *d = c;
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
打印结果:
分析:
&c和&c[0]地址相同,说明数组的首地址就是第一个元素的地址。内存地址是连续的并且相差4字节,代表当前元素类型为4字节。指针d+1代表平移一个步长(即当前元素类型的大小),这里表示平移4字节,如果数组中是指针或对象那么平移8字节。
根据分析结论,我们知道可以通过指针地址的平移来做取值操作,代码验证下
打印结果:
1
2
3
4
有了这个验证结果,那类是否也可通过内存平移的方式获取其中的数据呢?
五、类的结构内存计算
打印查看类的的内存情况
根据之前源码分析类的结构,可以得知首地址
0x0000000100008360是isa,紧跟着应该是superclass(结构体指针占8字节),截图中已验证0x0000000100357140为NSObject。那下面是cache和bits吗?今天先来看bits,后续单独篇章探究cache。
从类的结构中得知isa和superclass都是Class类型即结构体指针占8字节,
想要通过内存平移到bits,首先要知道cache的大小。下面跟进cache_t源码:
cache_t是结构体,结构体大小要看其内部成员变量的大小。我们发现cache_t结构体中有很多东西,其中方法在方法区、全局变量在全局区不占用结构体内存
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;// 8字节
union {
struct {
explicit_atomic<mask_t> _maybeMask;// 4字节
#if __LP64__
uint16_t _flags;// 2字节
#endif
uint16_t _occupied;// 2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;// 8字节
};
...
}
最终影响结构体大小的成员变量有两个:一个是_bucketsAndMaybeMask,类型是explicit_atomic<uintptr_t>,explicit_atomic结构体是一个泛型,其真实大小取决于uintptr_t即8字节;另一个是联合体,联合体内又包含一个结构体和_originalPreoptCache,由于联合体是互斥的,经分析联合体也是8字节。
所以cache_t大小为16字节。
到此获取bits需要从类的首地址偏移32字节即0x20(isa(8) + superclass (8) + cache (16))。
首地址
偏移0x20后强转为class_data_bits_t *类型就得到了bits的地址
六、lldb分析类的属性
上面已经获取到bits的地址,我们在objc_class结构体中看到下面这行代码
尝试一下获取
bits.data(),由于是指针需要用->调用
我们发现
$2->data()得到了class_rw_t *类型的变量。再查看$3的内容发现没有之前提到的方法 属性 协议等。这里需要通过$4.properties()获取,lldb分析流程如下:
七、lldb分析类的方法
上面分析了类的属性,修改下LRPerson,添加一些属性和方法
继续通过lldb分析类的方法
在分析方法时,还原
method_list_t后,通过get(index)无法获取方法,这里和属性有所区别
struct property_t {
const char *name;
const char *attributes;
};
struct method_t {
...
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
...
public:
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
...
};
property_t结构体中有两个成员变量和打印信息相符,而method_t结构体中有个big结构体,尝试打印big结构体即得到我们想看到的信息。
还原method_list_t后可以看到count = 5,即有5个方法,打印出这5个方法:
我们发现分别是
saySomething和hobby、name的get set方法,LRPerson的类方法say666却没有,因为类方法存储在元类中的方法列表里。
八、lldb分析类的协议
先写一个协议,LRPerson遵守这个协议并实现协议的方法
在class_rw_t结构体中有个protocols方法
lldb分析类的协议
当获取到
protocol_list_t时,发现和属性property_list_t、方法method_list_t结构有所区别。查看protocol_list_t结构体
protocol_list_t结构体中list[0]为protocol_ref_t类型,获取到protocol_ref_t后直接强转为protocol_t,打印出详细信息就可以看到instanceMethods,instanceMethods即协议的方法列表,在这个方法列表中我们找到了先前写的协议的方法。