OC是一门面向对象语言,面向对象离不开对象,类,继承,类方法,实例方法,属性,实例变量,对于习惯了面向对象的同学来说,这些似乎是一门语言的天然特征,一切本该如此,不需要问为什么,但对于OC来说,一切都是可以寻根溯源的,因为它是由更底层的语言c/c++来实现这些看似本该如此的特征。现在就深入内部,看看类是如何实现的,以及类的结构。
先从一段简单的代码开始:
//一个只有一个属性和一个方法的Person类
@interface Person : NSObject
@property (copy, nonatomic) NSString* name;
- (void)doSomeThing;
@end
@implementation Person
- (void)doSomeThing {
NSLog(@"do some thing");
}
@end
//再给他创建一个继承的子类Teacher
@interface Teacher : Person
@end
@implementation Teacher
@end
//在main中创建这个类
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSLog(@"%@",p);
}
return 0;
}
在NSLog(@"%@",p);
这里打个断点,可以看到p
的内存地址:
p
对象的内存分布,在lldb窗口输入x p
来打印p
的十六进制内存信息:可以看到内存的首地址就是上面断点中看到的
p
对象的地址0x100606940
,冒号后面就是它里面存放的内容,通过NSObject
头文件定义可以知道,对象内存的首地址存放的就是他的isa
数据。
//NSObject.h中NSObject的定义
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
之前分析isa
结构得知isa
中应当存放着一个对象的类信息,通过isa
可以找到对象对应的类,下面就用lldb调试,看看由p
这个实例中的isa
能否找到它所属的类Person
。
分析isa
实现源码就能知道,isa
和ISA_MASK
进行与运算,就能拿到isa
中真正存放类信息的33(ARM平台)或44(x86平台)字节,下面就来验证一下,在lldb中输入x/4g p
:
可以看到通过
x/4g
打印的p
的内存内容和上面通过x p
打印出来的内容是一致的,只是字节顺序是相反的。下面就来看看存放isa
的前8字节和ISA_MASK
相与是否能获取到p
所对应的Person
类,打印看看是什么结果:可以看到,首地址的8个字节内容
0x001d80010000226d
与上ISA_MASK 0x0000000ffffffff8
,得到一个地址0x0000000100002268
,那么这个地址是什么呢,po
打印下便知:这下就一目了然了,这就是要找的类
Person
,这就证明了对象是可以通过isa
找到它对应的类,那么紧接着是不是可以用同样的方法看看类到底是什么呢?可以看到格式化输出刚才的
Person
类的地址0x0000000100002268
,也打印出了内存的内容,假设这段内存中的第一段地址存放的也是一个isa
,也与上ISA_MASK
打印下看看是什么结果:这次直接
po Person
类地址与上ISA_MASK
的结果,看到依然是一个Person
,这就奇怪了,难道内存中存在有两个Person
类,地址还不一样吗?的确如此,一个类在OC中确实会存在两份,这就是“元类”的概念,第二个Person
类是第一个Person
类的元类,为什么会有元类,这就是OC实现面向对象的一种机制,一个类在OC的运行时中也是一个实例,而类中也有类方法,执行类方法时,元类就出现了,通过“类实例”的isa
就可以找到元类中存放的类方法进而执行,而元类其实也是一个特殊的“实例”,它里面存放着类方法表,需要执行类方法时就去它里面查找执行。这样就实现了一个面向对象的执行链条,一个对象实例需要执行实例方法,就通过自己的isa
找到类,而类在内存中也是一个“类实例”,在“类实例”中存放着实例方法表,如果一个类需要执行类方法,就通过“类实例”的isa
找到“元类实例”,在“元类实例”中查找需要执行的类方法。而类和元类的创建维护全部是由编译器和运行时完成的,我们并没有感知到。下面再来看看第二个元类
Person
是否也能打印打它的isa
,先打印下元类的格式化内存布局:同样,拿到前8字节的
isa
内容与上ISA_MASK
在打印:最后
po
一下这个0x00007fff8fa270f0
地址:结果是
NSObject
,这说明元类的isa
指向了NSObject
,下面就来看一个非常经典的图:这样看就很清楚了,
Person
的元类最终指向了NSObject
的元类,所以就有了上面NSObject
的打印。如果继续打印NSObject
的元类的isa
,依然还会输出NSObject
,因为根元类的isa
指向了自己,形成了一个闭环。这里分析了
isa
的走向,解决了实例方法和类方法调用的问题,那么下面还需要解决面向对象中的继承问题,如何解决类之间的继承关系,就需要另外一个成员superclass
来解决。
//objc_runtime-new.h中objc_class的定义
struct objc_class : objc_object {
// Class ISA;
Class superclass;
....
}
通过runtime源码,能够看到一个类中,第一个8字节是isa
,紧接着就是指向superclass
的结构体的指针,那么我没如果打印一个“类实例”内存布局中的第二个8字节,也就找到了superclass
,来验证一下上面图中所画的superclass
的走向:
//实例p的内存布局
(lldb) x/4gx p
0x100606940: 0x001d80010000226d 0x0000000000000000
0x100606950: 0x0000000000000000 0x0000000000000000
//第一个8字节0x001d80010000226d与上ISA_MASK,得到Person类地址0x0000000100002268
(lldb) p/x 0x001d80010000226d & 0x00007ffffffffff8ULL
(unsigned long long) $28 = 0x0000000100002268
//打印Person类的内存布局
(lldb) x/4gx 0x0000000100002268
0x100002268: 0x0000000100002240 0x00007fff8fa27118
0x100002278: 0x0000000100495970 0x0004802c00000007
//通过Person类的isa与上ISA_MASK,得到Person元类的地址0x0000000100002240
(lldb) p/x 0x0000000100002240 & 0x00007ffffffffff8ULL
(unsigned long long) $32 = 0x0000000100002240
//打印Person元类的内存布局
(lldb) x/4gx 0x0000000100002240
0x100002240: 0x00007fff8fa270f0 0x00007fff8fa270f0
0x100002250: 0x0000000100608f40 0x0004e03500000007
//Person类的第二个8字节存放的是0x00007fff8fa27118,po一下
(lldb) po 0x00007fff8fa27118
NSObject
//Person元类的第二个8字节存放的是0x00007fff8fa270f0,po一下是NSObject元类
(lldb) po 0x00007fff8fa270f0
NSObject
//打印NSObject类的内存布局,第二个8字节是nil,说明他的父类指向空
(lldb) x/4gx 0x00007fff8fa27118
0x7fff8fa27118: 0x00007fff8fa270f0 0x0000000000000000
0x7fff8fa27128: 0x0000000100495d50 0x0001801000000003
//NSObject元类类的内存布局,第二个8字节正是NSObject类的地址
//说明他的父类指向NSObject类,和图中画的superclass走向一致
(lldb) x/4gx 0x00007fff8fa270f0
0x7fff8fa270f0: 0x00007fff8fa270f0 0x00007fff8fa27118
0x7fff8fa27100: 0x00000001004962c0 0x0004e03100000007
上面提到了objc_object
和objc_class
结构体,下面就来看看这两个结构体之间是什么关系,通过runtime的源码,我们可以找到这两个结构体的定义,这里有一个需要注意的点,就是关于objc_class
的定义是有两种的,为了向前兼容更老版本的OC,会有下面这样的objc_class
定义:
//runtime.h中objc_class的定义
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到最下面的OBJC2_UNAVAILABLE;
这个宏以及#if !__OBJC2__
这个编译条件,这就意味着当OC版本不是OBJC2
时,runtime就会使用这个版本的objc_class
结构体。再来看看OBJC2
条件下的objc_object
和objc_class
:
//objc_private.h中objc_object的定义
struct objc_object {
private:
isa_t isa;
....
}
//objc_runtime-new.h中objc_class的定义
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
是继承了objc_object
的,在c++中结构体也是可以继承的,这也就意味着objc_class
中也有一个isa
。这也就解释了,为什么所有实例、类及元类都会有isa
,并且是自身内存布局中的第一个8字节。紧接着后面是cache_t
类型,在后面就是class_data_bits_t bits;
,但是却没有看到方法列表,属性列表等这些类必须的信息,那么从哪里找到这些类的信息呢,isa
和superclass
必然不会存储这些信息,cache
是个cache_t
类型,可以看到他的内部定义:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
....省略
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
....
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
....
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
....
};
可以看到cache_t
内部也没有我们需要的方法列表属性列表,那么就要看看class_data_bits_t bits;
中的内容了,要想查看class_data_bits_t bits;
的内容可以借助内存偏移来打印这个位置的内存内容,但是如何知道这个bits
在内存中的哪个位置呢,这就要看他前面的几个成员分别占用多少内存:
1、isa
是isa_t
联合体,看源码可以知道占8字节
2、Class superclass;
是指向objc_class
结构体的指针,也占8字节
最后只要计算出cache_t cache;
的大小,就可以知道class_data_bits_t bits;
所在的内存位置了,下面就分析一下cache_t cache;
的大小。
在cache_t
结构体定义中,首先可以看到三个宏定义的判断:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
....
#else
#error Unknown cache mask storage type.
#endif
那么这三个分支会走哪一个呢,当然通过后面的分析也可以暴力的认为除了最后的#else
其他分支都不影响cache_t
所占内存大小,但是这样总是不太好,先看看这些宏都是什么含义,在objc-config.h
最下面可以看到这几个宏定义:
#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3
#if defined(__arm64__) && __LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
从这些定义中,我们大致能明白这里应该是在判断系统平台和cpu架构,根据不同的平台和cpu来决定cache_t
结构体存储的方式,刨根问底,__arm64__
和__LP64__
又是什么意思呢?看下面:
Most C++ compilers have a predefined macro containing the version number of the compiler. Programmers can use preprocessing directives to check for the existence of these macros in order to detect which compiler the program is compiled on and thereby fix problems with incompatible compilers.
大致的意思是大部分C++编译器会预先定义一些宏,包含编译器的版本信息,这样开发者就可以通过过程检测,来检查这些宏是否存在,进而判断哪个编译器被用来编译,这样就可以解决一些编译器不兼容的问题。(来自《Calling conventions for different C++ compilers and operating systems》,Last updated 2012-02-29,作者:By Agner Fog. Copenhagen University College .)
Unfortunately, not all compilers have well-documented macros telling which hardware platform and operating system they are compiling for. The following macros may or may not be defined:
不幸的是,不是所有的编译器都被很好地宏定义来被告知他们所编译的目标平台是什么硬件平台和操作系统,下面的宏可能被定义也可能没有:
可以看到__LP64__
是Linux 64 bit
平台的的宏定义,而__arm64__
应该是针对arm移动平台64位cpu的宏定义,从这两个线索我们就可以判断cache_t
结构体的三个分支是如何走的了,根据我们的调试环境,是MAC系统,非__arm64__
平台,所以就会进入CACHE_MASK_STORAGE_OUTLINED
这个判断分支,我们就根据这个分支的情况去计算cache_t
所占的内存大小:
//只看第一个分支中的内容即可
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 是unsigned int 的别名, 占4字节
.....
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
}
其中explicit_atomic
是个模板结构体,它的长度由struct bucket_t *
类型确定,是一个指向结构体的指针,所以是8字节,同样_mask
的长度由mask_t
确定,它的定义是typedef uint32_t mask_t;
,是一个nsigned int
占4字节,剩下的静态成员不计算在结构体内部,因为编译后的静态变量存储在静态区,最后还有两个成员_flags
和_occupied
,为什么要计算_flags
,因为有#if __LP64__
的宏判断,我们调试的环境是MAC系统,和Linux都是类Unix系统,有许多相似之处,而且是64位平台,所以会有这个判断,要计算uint16_t _flags;
的长度,uint16_t
定义为typedef unsigned short uint16_t;
,所以_flags
是2字节,最后_occupied
也是2字节,最终计算struct cache_t
共占用8+4+2+2 = 16字节,后面我们可以同过lldb调试来验证。
那么要想知道objc_class
中class_data_bits_t bits;
中的内容, 只需通过类的首地址, 偏移isa
+superclass
+cache
的长度,共平移32字节就可以得到了:
//打印Person类内存布局
(lldb) x/4gx Person.class
0x1000020e8: 0x00000001000020c0 0x0000000100334140
0x1000020f8: 0x000000010032e440 0x0000801000000000
//查看首地址0x1000020e8是否为Person类
(lldb) po 0x1000020e8
Person
//将0x1000020e8偏移32位,16进制进位后变成0x100002108,并强转为class_data_bits_t *打印,拿到bits首地址
(lldb) p (class_data_bits_t *)0x100002108
(class_data_bits_t *) $2 = 0x0000000100002108
上面的打印结果中显示了如何通过内存偏移拿到我们想要的class_data_bits_t 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);
}
....
};
省略掉非关键的代码,可以看到结构体内部定义了一个data()
方法,返回一个class_rw_t
的指针,用lldb打印看看这个class_rw_t
是什么:
//打印0x0000000100002108指针内容,得到class_data_bits_t结构体
(lldb) p *$2
(class_data_bits_t) $3 = (bits = 4301869812)
//执行class_data_bits_t结构体data()方法,获取到成员class_rw_t的指针
(lldb) p $3->data()
(class_rw_t *) $5 = 0x00000001006952f0
Fix-it applied, fixed expression was:
$3.data()
//打印class_rw_t指针的内容,可以得到
(lldb) p *$5
(class_rw_t) $6 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294975600
}
firstSubclass = Teacher
nextSiblingClass = NSUUID
}
(lldb)
通过lldb打印,可以看到class_data_bits_t
结构体的data()
方法返回了class_rw_t
类型的数据,其中包含了类的很多关键信息,有firstSubclass
就是他的子类Teacher
。我们再来看看class_rw_t
的内部结构:
//objc_runtime_new.h中class_rw_t定义,非关键代码已略
struct class_rw_t {
....
Class firstSubclass;
Class nextSiblingClass;
private:
....
const method_array_t methods() const {
....
}
const property_array_t properties() const {
....
}
const protocol_array_t protocols() const {
....
}
};
这里面就很清楚的看到methods()
,properties()
,protocols()
,顾名思义,这里就是存放类方法表,属性表以及协议的地方。下面看看是否能用lldb打印出Person类的方法表和他的属性列表:
//拿到Person类的class_rw_t
(lldb) p *$5->data()
(class_rw_t) $6 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294975616
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
//调用class_rw_t的methods()方法
(lldb) p $6.methods()
(const method_array_t) $24 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000020c8
arrayAndFlag = 4294975688
}
}
}
//得到method_array_t,获取method_array_t的list首地址指针
(lldb) p $24.list
(method_list_t *const) $25 = 0x00000001000020c8
//打印method_list_t中的内容,可以看到第一个就是doSomeThing方法
(lldb) p *$25
(method_list_t) $26 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "doSomeThing"
types = 0x0000000100000f8a "v16@0:8"
imp = 0x0000000100000dd0 (KCObjc`-[Person doSomeThing])
}
}
}
//再来打印propertylist看看Person类的属性
(lldb) p $6.properties()
(const property_array_t) $27 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100002158
arrayAndFlag = 4294975832
}
}
}
//打印property_array_t的list指针
(lldb) p $27.list
(property_list_t *const) $28 = 0x0000000100002158
//打印list的内容,可以看到Person类声明的属性name
(lldb) p *$28
(property_list_t) $29 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
}
}
(lldb)
总结:在新版本的OC源码中(本文使用的是objc4-781),通过上面的分析,我们验证了类的isa
走向,以及类的superclass
走向,结构体objc_class
的类信息放在了class_data_bits_t bits;
的class_rw_t
中,里面存放了类的firstSubclass
、methods()
、properties()、protocols()
等关键信息。