isa指向分析
通过《iOS底层原理探究04-OC对象的本质&联合体位域&isa分析》我们对isa已经有了一定的了解,现在我们来研究下isa具体的指向情况。
对象的isa指向
对象的isa指向类这个我们都知道下面就来验证一下
这里需要知道nopointerisa
的存在,普通的isa是直接指向类的,但是得添加OBJC_DISABLE_NONPOINTER_ISA = YES
参数能会使用普通的isa
否则默认会使用nopointerisa
当我们不使用
nopointerisa
时isa
是直接指向类的
- 对象中第一个变量就是
isa
即0x0000000100008700
- 非
nopointerisa
时isa
指向类po 0x0000000100008700
打印LGPerson
image.png
首先x/4gx
查看p
对象的内存,首地址0x011d800100008365
就是p
的isa
指针,nopointerisa
需要&上ISA_MASK
才是isa
指向的类的地址(这些在上一篇里都有介绍),po
输出了LGPerson
,后面又通过p/x
验证了一下LGPerson.class
和isa & ISA_MASK
的地址,结果是一样的,这样就充分验证了对象p
的isa
指向它的类LGPerson
。
类的isa指向
现在我们验证了对象的isa
是指向类的,那么类的isa
又指向什么呢,我们来继续探究
-
x/4gx
查看类的内存情况得到类的isa
地址(即内存的首地址)0x0000000100008338
-
po
输出类的isa & ISA_MASK
输出了LGPerson
,这个LGPerson
还是当前类的吗? -
p/x 0x0000000100008338 & 0x00007ffffffffff8
通过p/x isa & ISA_MASK
输出地址,发现是0x0000000100008338
和之前的$12 = 0x0000000100008360 LGPerson
并不是通一个地址,因此这两个LGPerson
并不是相同的。
通过上面的一通操作我们得到了这样一个结果0x0000000100008360
和 0x0000000100008338
都输出 LGPerson
,这里我们大胆猜想一下 ,是不是类和对象一样在内存里也会存在很多个,接下来我们验证一下这个猜想
我们使用各种方法得到的类打印出来的地址都是同一个,说明我们的猜想不正确。
用烂苹果(MachOView)看一下吧
把Products文件夹下编译生成的可执行文件(002-isa分析)放到烂苹果里分析下
到符号表(Symbol Table)里查看
-
0000000100008360
是_OBJC_ClASS(类)
类型的LGPerson
-
0000000100008338
是_OBJC_METACLASS(元类)
类型的LGPerson
这下就清晰了 一个是类,一个是元类,类的isa
指向元类,这个元类我们代码里并没有创建,是系统编译的时候自动生成的,也就是说 对象 isa -> 类 isa -> 元类
此时我会类的isa
指向元类,那元类的isa指向哪里呢?接下来继续探究一下
输出元类的
isa
& ISA_MASK
发现指向的是根元类NSObject
,进一步输出跟元类的isa
& ISA_MASK
是指向自己的,这个我们输出NSObject
类的地址发现和根元类NSObject的地址并不相同。至此我们可以得到这样一个结论
一般的类isa的走位图是这样的,那么根类NSObject的isa的走位图又是什么样的呢
可以看到NSObject还是有点区别的比普通的类少了一层
结合到一起就得到了完整的isa走位图
接下来看下继承链
运行结果说明
-
LGPerson
元类的父类是NSObject元类
-
LGTeacher(LGPerson的子类)元类
的父类是LGPerson元类
- 根类NSObject没有父类
-
根元类NSObject的父类是NSObject类
至此继承链也已经很清晰了
继承链
结合isa走位图和继承链图得到苹果官网上的这张图
isa流程图.png
类的isa指向和继承链已经很清晰了。关于isa的走位和继承链就探索到这里,接下来开始探究类的内存结构。
类的内存结构
在《iOS底层原理探究04-OC对象的本质&联合体位域&isa分析》里面我们已经知道了OC的类Class在底层是struct objc_class *
类型,接下来就来看看objc_class的结构
struct objc_class : objc_object {
...省略无关代码
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
...省略无关代码
}
因为类objc_class有好几百行代码,我们省略掉无关紧要的代码和方法代码(结构体的方法不在接口对象里存而是在方法区保存),直接研究他的成员变量,可以得到objc_class包含4个成员变量
- ISA(从objc_object继承过来的)
- superclass父类
- cache 缓存
- bits 类的数据
第一个isa我们在前面的内容其实已经验证过了
看下第二个成员变量是不是superclass
image.png
下面我们使用LGPerson
类来做示例,看下属性、方法、成员变量在类里是怎么存的。
image.png
po
第二个成员变量确实打印了父类NSObject
cache
我们先不看(下一篇重点介绍)先看bits
,但是要想看bits
就先得知道cache
的大小,然后通过类的首地址+偏移量(ISA + superclass + cache)来找到bits
的地址进而查看bits
的内存结构。ISA
和superclass
都是Class
类型8字节 但是cache
是个结构体它的大小要根据成员变量确定,所以要先简单看下它的结构确定大小。
struct cache_t {
private:
explicit_atomic _bucketsAndMaybeMask;
union {
struct {
explicit_atomic _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic _originalPreoptCache;
};
...省略静态变量
...省略无关方法代码
}
因为静态变量是存在静态存储区的,方法存在方法区都不影响cache_t结构体的大小所以省略掉这些代码,下面就很清晰了就剩一个explicit_atomic
和联合体。
-
typedef unsigned long uintptr_t;
可知_bucketsAndMaybeMask
是8字节 - 联合体中
_maybeMask(mask_t)
+_flags(uint16_t)
+_occupied(uint16_t)
,mask_t
是uint32_t
类型4字节 uint16_t是2字节 ,就是 4 + 2 + 2 = 8字节,联合体中另外一个成员_originalPreoptCache
是指针类型也是8字节,所以联合体整体占用8字节(关于联合体看这里) -
cache_t
就是8 + 8 = 16字节
那么拿到class的首地址 + 32就是bits
的位置。
按我们之前的思路确实能获取到
class_data_bits_t *
类型的bits
,但是怎么看这个数据结构呢。
struct class_data_bits_t {
...省略无关代码
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...省略无关代码
}
通过源码可以找到class_data_bits_t
中存在data ()
取数据的方法我们来试一下。
通过
data()
方法确实取到了class_rw_t
,打印得到了class_rw_t
的数据结构,再来看下class_rw_t
的源码。
struct class_rw_t {
...省略无关代码
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get(&ro_or_rw_ext)->baseProtocols};
}
}
};
-
methods()
取方法列表方法 -
properties()
取属性列表方法 -
protocols()
取协议列表方法
打印属性列表
使用properties()
方法获取到了property_array_t
通过控制台打印的结构看到property_array_t
里有个list
结构,list
里包含了ptr
,深入进去得到了property_list_t
类型的一个列表,使用get()
方法查看list
的内容
image.png
至此我们在类内存结构里找到了类的两个属性name
、hobby
。
获取类里的方法
(lldb) x/4gx LGPerson.class
0x100008380: 0x00000001000083a8 0x000000010036c140
0x100008390: 0x0000000100364360 0x0000802800000000
(lldb) p/x 0x100008380+0x20
(long) $1 = 0x00000001000083a0
(lldb) p (class_data_bits_t *)0x00000001000083a0
(class_data_bits_t *) $2 = 0x00000001000083a0
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101349aa0
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic = {
Value = 4295000344
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.methods()
(const method_array_t) $5 = {
list_array_tt = {
= {
list = {
ptr = 0x0000000100008160
}
arrayAndFlag = 4295000416
}
}
}
(lldb) p $5.list
(const method_list_t_authed_ptr) $6 = {
ptr = 0x0000000100008160
}
(lldb) p $6.ptr
(method_list_t *const) $7 = 0x0000000100008160
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt = (entsizeAndFlags = 27, count = 6)
}
(lldb) p $8.get(0).big()
(method_t::big) $9 = {
name = "sayNB"
types = 0x0000000100003f77 "v16@0:8"
imp = 0x0000000100003d40 (KCObjcBuild`-[LGPerson sayNB])
}
(lldb) p $8.get(1).big()
(method_t::big) $10 = {
name = "hobby"
types = 0x0000000100003f6f "@16@0:8"
imp = 0x0000000100003db0 (KCObjcBuild`-[LGPerson hobby])
}
(lldb) p $8.get(2).big()
(method_t::big) $11 = {
name = "setHobby:"
types = 0x0000000100003f8b "v24@0:8@16"
imp = 0x0000000100003de0 (KCObjcBuild`-[LGPerson setHobby:])
}
(lldb) p $8.get(3).big()
(method_t::big) $12 = {
name = "init"
types = 0x0000000100003f6f "@16@0:8"
imp = 0x0000000100003ce0 (KCObjcBuild`-[LGPerson init])
}
(lldb) p $8.get(4).big()
(method_t::big) $13 = {
name = "name"
types = 0x0000000100003f6f "@16@0:8"
imp = 0x0000000100003d50 (KCObjcBuild`-[LGPerson name])
}
(lldb) p $8.get(5).big()
(method_t::big) $14 = {
name = "setName:"
types = 0x0000000100003f8b "v24@0:8@16"
imp = 0x0000000100003d80 (KCObjcBuild`-[LGPerson setName:])
}
(lldb)
- 流程跟获取属性的大致一样
- 获取列表的时候换成
methods()
方法 - 遍历时候
get()
之后需要调用big()
方法才能获取到元素
为什么需要调用big(),来看源码
method_t
的结构里方法的信息存储在struct big
结构体里,所以需要调用big()
方法
总结
通过对类的结构的探究,了解到类的方法列表
、属性列表
、协议列表
都储存在bits
数据结构中,后面我们会继续对类的存储探究。
遗留问题
本篇留下了一个问题:类的实例方法
是存在methods()
数据接口中的但是类方法
并没有存在这里,那类方法
存在哪里呢?我再下一篇《iOS底层原理探究06-类的底层原理下》中为您解答