通过上一篇文章对isa
的分析,我们知道了所有的对象都包含isa
,并且isa
存储了类的相关信息,所以这篇文章我们主要通过isa
来引出类的底层结构以及一些信息
代码分析
创建对象
LYPerson *person = [[LYPerson alloc] init];
lldb分析
(lldb) x/4gx person
0x100513970: 0x001d80010000820d 0x0000000000000000
0x100513980: 0x0000000000000000 0x0000000000000000
(lldb)
通过之前的分析,我们知道所有对象的第一个属性都是isa
,故上面的0x001d80010000820d
为isa
的地址,我们继续通过打印isa
的地址来看下
(lldb) po 0x001d80010000820d & 0x00007ffffffffff8ULL
LYPerson
(lldb) p/x 0x001d80010000820d & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008208
(lldb) po 0x0000000100008208
LYPerson
这里需要& 0x00007ffffffffff8ULL
主要是因为isa
只有中间的44(或33)
位才是类信息,所以需要把其它位置为0
,通过上面的地址打印我们得出结论:对象的isa
指向类信息,那么我们继续来打印下类的isa
看看是什么
(lldb) x/4gx 0x0000000100008208
0x100008208: 0x00000001000081e0 0x00007fff98b0e118
0x100008218: 0x0000000100513990 0x0001802400000003
(lldb) p/x 0x00000001000081e0 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00000001000081e0
(lldb) po 0x00000001000081e0
LYPerson
这里我们发现类的isa
只想的信息,打印出来也是LYPerson
,通过内存地址我们可以肯定此处的LYPerson
跟上一步的LYPerson
不是同一个,但是类在内存中又只会存在一份,因此我们引出了元类
的概念,至此,我们得出结论:类的isa
指向元类信息.
接下来我们给出类在内存中只存在一份的证明以及元类
的说明
//MARK:--- 分析类对象内存 存在个数
void testClassNum(){
Class class1 = [LYPerson class];
Class class2 = [LYPerson alloc].class;
Class class3 = object_getClass([LYPerson alloc]);
NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}
元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类(类似于对象的归属是类)
元类是类对象的类,每个类都有一个独一无二的元类用来存储类方法的相关信息。
元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
接下来我们继续分析下元类的isa
(lldb) x/4gx 0x00000001000081e0
0x1000081e0: 0x00007fff98b0e0f0 0x00007fff98b0e0f0
0x1000081f0: 0x00000001006471f0 0x0003e03500000007
(lldb) p/x 0x00007fff98b0e0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00007fff98b0e0f0
(lldb) po 0x00007fff98b0e0f0
NSObject
通过打印isa
我们发现LYPerson
的元类的isa
指向了NSObject
,那么此处的NSObject
是不是就是我们的根类呢?我们来证明下
(lldb) p/x NSObject.class
(Class) $9 = 0x00007fff98b0e118 NSObject
很明显,两个NSObject
并不是同一个类,因此我们认定上面的NSObject
为根元类
,这里我们得出结论:元类的isa
指向根元类.接下来我们继续打印根元类的isa
(lldb) x/4gx 0x00007fff98b0e0f0
0x7fff98b0e0f0: 0x00007fff98b0e0f0 0x00007fff98b0e118
0x7fff98b0e100: 0x00000001020081b0 0x0005e03100000007
(lldb) p/x 0x00007fff98b0e0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff98b0e0f0
(lldb) po 0x00007fff98b0e0f0
NSObject
这里我们发现根元类的isa
与它本身的地址相同,由此我们得出结论:根元类的isa
指向它本身
isa & 继承链
通过上面的分析再加上我们的继承关系,于是就有了下面这个著名的图
上面这幅图,对于isa
的走位我们已经通过代码证明过了,没有任何问题.那么关于集成链有两点需要注意下
- 根元类的父类为根类
- 根类的父类为
nil
以上两点也说明了NSObject
做为基类,是万物起源,我们可以看下底层编译代码
struct NSObject_IMPL {
Class isa;
};
// 结构体
typedef struct objc_class *Class;
Class分析
源码分析
通过上面的分析我们来到了我们非常熟悉的Class
,接下来我们就来具体分析下Class
的定义objc_class
的源码实现
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
struct objc_class : objc_object {
// 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
//....方法部分省略,未贴出
}
通过源码我们可以发现objc_class
中包含的元素主要有ISA
,superclass
,cache
,bits
,对与ISA
我们已经了解了,superclass
很明显是父类,也没什么好分析的,那么剩下的cache
和bits
,cache
主要是缓存数据,我们可以放到后面,这里主要还是分析下bits
里面有哪些内容,那么如何获取到bits
呢,这里我们补充下个知识点--内存偏移
内存偏移
偏移地址是指段内相对于段起始地址的偏移值,
例如一个存储器的大小是1KB,可以把它分为4段,第一段的地址范围就是0—255,第二段的地址范围就是256-511,依次类推。
我们拿数组来举例
//数组指针
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);
0x7ffeefbff560 -- 0x7ffeefbff560 - 0x7ffeefbff564
0x7ffeefbff560 -- 0x7ffeefbff564 - 0x7ffeefbff568
通过上面我们可以得出:数组的首地址即位第一个元素的地址,第二个元素的地址即为第一个元素的地址加上元素类型所占的字节大小,int
占用4个字节,即+4
,若为double
则+8
综上,内存偏移就可以理解为首地址加偏移量
class_data_bits_t bits分析
在了解了内存偏移后,我们来尝试下获取bits
,首先ISA
和superclass
都是Class
类型,Class
是结构体指针类型,占用8
个字节,所以加一起是16
个字节,接下来我们分析下cache
,源码如下
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
通过源码,我们得出cache
占用16
个字节,因此bits
距首地址偏移32
个字节,接下来我们通过lldb
进行调试
// 打印LYPerson的内存地址
(lldb) x/4gx LYPerson.class
0x100008200: 0x00000001000081d8 0x000000010034c140
0x100008210: 0x000000010184dd90 0x0001802400000003
// 首地址为0x100008200,偏移32位获取class_data_bits_t
(lldb) p (class_data_bits_t *)0x100008220
(class_data_bits_t *) $30 = 0x0000000100008220
(lldb)
通过objc_class
源码我们可以发现bits
中存储的信息为class_rw_t
结构体类型,可以通过data()
方法获取.源码如下
struct objc_class : objc_object {
...
class_rw_t *data() const {
return bits.data();
}
...
}
我们继续通过lldb
调试
(lldb) p $30->data()
(class_rw_t *) $31 = 0x0000000100645c60
(lldb) p *$31
(class_rw_t) $32 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000216
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
接下来通过查看class_rw_t
的源码,我们发现,class_rw_t
存储了属性列表
,方法列表
,协议
,ro(class_ro_t类型)
等信息,如下图
接下来,继续通过lldb
调试
// 获取属性列表
(lldb) p $32.properties()
(const property_array_t) $33 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000081a8
arrayAndFlag = 4295000488
}
}
}
// 获取属性数组
(lldb) p $33.list
(property_list_t *const) $34 = 0x00000001000081a8
// 打印具体值
(lldb) p *$34
(property_list_t) $35 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
}
}
(lldb) p $32.methods()
(const method_array_t) $36 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000080e0
arrayAndFlag = 4295000288
}
}
}
(lldb) p $36.list
(method_list_t *const) $37 = 0x00000001000080e0
(lldb) p *$37
(method_list_t) $38 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 5
first = {
name = "sayName"
types = 0x0000000100003f78 "v16@0:8"
imp = 0x0000000100003d60 (KCObjc`-[LYPerson sayName])
}
}
}
(lldb) p $38.get(0)
(method_t) $39 = {
name = "sayName"
types = 0x0000000100003f78 "v16@0:8"
imp = 0x0000000100003d60 (KCObjc`-[LYPerson sayName])
}
(lldb) p $38.get(1)
(method_t) $40 = {
name = "saySex"
types = 0x0000000100003f78 "v16@0:8"
imp = 0x0000000100003d90 (KCObjc`-[LYPerson saySex])
}
(lldb) p $38.get(2)
(method_t) $41 = {
name = ".cxx_destruct"
types = 0x0000000100003f78 "v16@0:8"
imp = 0x0000000100003e10 (KCObjc`-[LYPerson .cxx_destruct])
}
(lldb) p $38.get(3)
(method_t) $42 = {
name = "name"
types = 0x0000000100003f8c "@16@0:8"
imp = 0x0000000100003dc0 (KCObjc`-[LYPerson name])
}
(lldb) p $38.get(4)
(method_t) $43 = {
name = "setName:"
types = 0x0000000100003f94 "v24@0:8@16"
imp = 0x0000000100003de0 (KCObjc`-[LYPerson setName:])
}
(lldb) p $38.get(5)
Assertion failed: (i < count), function get, file /Users/LY/Desktop/LY/objc4_debug/objc4-781/runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb)
通过上面获取到的properties
我们得到了类的所有属性列表,但是却并没有找到对应的成员变量的列表,于是我们通过查看上面的ro
的源码可以发现有一个ivars
属性,同样的方式分析ro
我们找到了所有的成员变量,这里不做赘述.大家可以自行尝试
通过上面获取到的methods
我们得到了类的所有实例方法列表,但是却并没有找到对应的类方法的列表,根据我们最开始探索的isa
指向,我们知道对象的isa
指向类,类的isa
指向元类.那么现在对象方法存在了类中,会不会类方法存在元类中呢,于是我们通过上面的步骤对元类进行调试,发现类方法确实存在元类中,这里不做赘述.大家可以自行尝试
通过上面的调试我们得出如下结论
- 类中存储了类的所有
属性列表
,``方法列表,
协议等信息在
class_rw_t`中 - 类的方法列表中除了实例方法还包括
setter
和getter
方法 - 类方法并不存在类信息中而是存在元类中
- 成员变量存在
class_ro_t
的ivars
中
至此,对于类的结构以及类中的class_data_bits_t
存储了哪些信息我们已经基本都了解了,这篇文章就先到这里了,后续我们再对类的cache
进行分析.