我们之前的篇幅介绍了对象
,也知道对象是一个类
的实例
。那么它的结构又是怎么样的。为了更直接的观察。我们做好充足的前戏
提前定义好了两个类。
Person
继承NSObject
,Developer
继承Person
,代码如下。
准备工作
Person.h
文件
@interface Person : NSObject
{
///成员变量
NSString *_variables;
}
///一个属性
@property (nonatomic, strong) NSString *attributes;
///类方法
+ (void)classMethod;
///实例方法
- (void)instanceMethod;
@end
Developer.h
文件
@interface Developer : Person
{
///成员变量
NSString *_subVariables;
}
///一个属性
@property (nonatomic, strong) NSString *subAttributes;
///类方法
+ (void)subClassMethod;
///实例方法
- (void)subInstanceMethod;
@end
main.m
文件
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK 0x00007ffffffffff8ULL
Person *person = [Person alloc];
Developer *developer = [Developer alloc];
NSLog(@"person %@", person);
NSLog(@"developer %@", developer);
}
return 0;
}
要用到的lldb指令
指令 | 作用 |
---|---|
p | 是 expr - 的缩写。它的工作是把接收到的参数在当前环境下进行编译,然后打印出对应的值。 |
po | 即 expr -o- 。它所做的操作与p 相同。如果接收到的参数是一个指针,那么它会调用对象的 description 方法并打印。如果接收到的参数是一个 core foundation 对象,那么它会调用 CFShow 方法并打印。如果这两个方法都调用失败,那么 po 打印出和 p 相同的内容。总的来说,po 相对于 p 会打印出更多内容。一般在工作中,用 p 即可,因为 p 操作较少,效率更高。 |
p/x | 以16 进制读取对象的地址或者值 |
x/4gx | 以16 进制形式读取4 个8 位的内存空间里面存储的值 |
(lldb) x/4gx person
0x1039b9230: 0x001d80010000231d 0x0000000000000000
0x1039b9240: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x001d80010000231d & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x0000000100002318
(lldb) po 0x0000000100002318
Person
(lldb) x/4gx Person.class
0x100002318: 0x00000001000022f0 0x00007fff91427118
0x100002328: 0x0000000100504630 0x000580240000000f
(lldb) p/x 0x00000001000022f0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00000001000022f0
(lldb) po 0x00000001000022f0
Person
(lldb) x/4gx 0x00000001000022f0
0x1000022f0: 0x00007fff914270f0 0x00007fff914270f0
0x100002300: 0x0000000100604270 0x0004e03500000007
(lldb) p/x 0x00007fff914270f0 & 0x00007ffffffffff8ULL
(unsigned long long) $9 = 0x00007fff914270f0
(lldb) po 0x00007fff914270f0
NSObject
(lldb) x/4gx 0x00007fff914270f0
0x7fff914270f0: 0x00007fff914270f0 0x00007fff91427118
0x7fff91427100: 0x00000001039b9880 0x0004e03100000007
(lldb) p/x 0x00007fff914270f0 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff914270f0
(lldb) po 0x00007fff914270f0
NSObject
我们打个断点通过lldb
来观察一下person
对象发现它的isa
指针指向Person
类,Person
类的isa
指针指向的还是Person
类,
这里面有个细节,就是尽管都是Person
类,但是他们并不是一个类
,仔细观察我们发现person
的isa
指针地址为0x0000000100002318
,而Person.class
的isa
指针地址为0x00000001000022f0
。他们并不是同一个类
我们把后面这个看起来像是它自己的类
称之为元类
。
随后元类
的isa
指针指向为NSObject
的地址为0x00007fff914270f0
。即便我们一直这样观察下去也发现,循环指向0x00007fff914270f0
即NSObject
。
再暴力点直接x/4gx NSObject.class
发现它的isa也是0x00007fff914270f0
结论
1、对象
的 isa
指向 类
(也可称为类对象
)
person
-> isa
他所属的类 Person
2、类
的 isa
指向 元类
Person类
-> isa
他的Person元类
(虽然看起来是他自己,但真的不是它自己。因为isa
地址不一样)
3、元类
的 isa
指向 根元类
,即NSObject
Person元类
-> isa
= 他的根元类NSObject
4、根元类
的 isa
指向 它自己
也是NSObject
NSObject根源类
-> isa
它自己
NSObject
(这个的内存地址始终唯一
)
我们来解释一下什么是元类
,应该就可以明白刚才所说的看起来是它自己。但是又不是它自己
我们都知道 对象
的isa
是指向类
,类
的其实也是一个对象
,可以称为类对象
,其isa
的位域指向苹果定义的元类
1、元类
是系统给的,其定义和创建都是由编译器
完成,在这个过程中,类
的归属来自于元类
2、元类
是类对象
的类
,每个类
都有一个独一无二
的元类
用来存储 类方法
的相关信息。
3、元类
本身是没有名称
的,由于与类
相关联,所以使用了同类名
一样的名称
如果简单的理解话。你可以把它理解成一个副本类。有这一样的名字
类是否唯一?
Class developerClass1 = [Developer class];
Class developerClass2 = [Developer alloc].class;
Class developerClass3 = object_getClass([Developer alloc]);
NSLog(@"developerClass1=%p", developerClass1);
NSLog(@"developerClass2=%p", developerClass2);
NSLog(@"developerClass3=%p", developerClass3);
/// developerClass1=0x100003380 developerClass2=0x100003380 developerClass3=0x100003380
Class personClass1 = [Person class];
Class personClass2 = [Person alloc].class;
Class personClass3 = object_getClass([Person alloc]);
NSLog(@"personClass1=%p", personClass1);
NSLog(@"personClass2=%p", personClass2);
NSLog(@"personClass3=%p", personClass3);
/// personClass1=0x100003330 personClass2=0x100003330 personClass3=0x100003330
Class objectClass1 = [NSObject class];
Class objectClass2 = [NSObject alloc].class;
Class objectClass3 = object_getClass([NSObject alloc]);
NSLog(@"personClass1=%p", objectClass1);
NSLog(@"personClass2=%p", objectClass2);
NSLog(@"personClass3=%p", objectClass3);
/// developerClass1=personClass1 personClass2=0x7fff91427118 personClass3=0x7fff91427118
我们又通过这一坨很无聊的代码得出另外一个结论,任何类
在内存
中只存在一份
核心isa走位 与 类的继承关系图
这个图第一次我看很懵逼。现在看依然懵逼。
但是。学以致用,把他套入到我们自己的类用。就容易理解很多
手残党画的不好。但是确实套用我们刚才示例代码的进来。清晰了不少。
我们梳理一下这幅图中的俩条线索
isa走位链路
1、实例对象 isa
指向他所属的类
2、类对象isa
指向它的元类(其实也是它,但是内存地址不是同一个)
3、元类的isa指向它的根元类
4、根元类的isa指向它,无限循环,形成闭环,所有的根元类就是NSObject
supclass走位链路
我们都知道类与类之间可以存在继承关系
1、子类
继承父类
2、父类
继承根类
,此时的根类
是指NSObject
3、根类
继承nil
,所以万物皆NSObject
。
元类
也有继承关系
1、子元类
继承父元类
2、父元类
继承根元类
3、根元类
继承根类
,此时根类
为NSObject
容易混淆
的一个点是。类与类之间是有继承
关系,但是实例对象
与实例对象
之间没有继承
关系
如Developer
,Person
,NSObject
三个类
的关系是Developer
继承Person
,Person
继承NSObject
它们的实例对戏那个为developer
, person
, object
。你不们理解为他们的类是继承
关系。所以他们三个实例对象
也存在继承
关系。这是错误
的。
objc_class & objc_object
isa
走位我们理清楚了,又来了一个新的问题:为什么 对象
和 类
都有isa
属性呢?
不提到两个结构体类型:objc_class
& objc_object
我们之前提及NSObject
的底层编译是NSObject_IMPL
结构体
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
在objc4
源码中搜索objc_class
的定义,源码中对其的定义有两个版本
- 旧版 位于 runtime.h中,已经被废除,代码如下
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;
- 新版 位于
objc-runtime-new.h
,这个是objc4-781
最新优化的。我们就来研究以这个版本为准,由于代码比较多。只展示核心部分代码
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
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
}
从新版的定义中,可以看到 objc_class
结构体类型是继承自 objc_object
的.objc_object
定义又如下代码
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
我们来探索一下类里面都哪些信息
isa
属性:继承自objc_object
的isa
,占 8
字节
superclass
属性:Class
类型,Class
是由objc_object
定义的,是一个指针,占8
字节
cache
属性:简单从类型class_data_bits_t
目前无法得知,而class_data_bits_t
是一个结构体类型,结构体的内存大小需要根据内部的属性来确定,而结构体指针才是8
字节
bits
属性:只有首地址经过上面3
个属性的内存大小总和的平移,才能获取到bits
计算 cache 类的内存大小
进入cache
类cache_t
的定义(只贴出了结构体中非static
修饰的属性,主要是因为static
类型的属性 不存在结构体的内存中),代码如下
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是12字节
- 【情况一】if流程
buckets 类型是struct bucket_t *,是结构体指针类型,占8字节
mask 是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
- 【情况二】elseif流程
_maskAndBuckets 是uintptr_t类型,它是一个指针,占8字节
_mask_unused 是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
_flags
是uint16_t
类型,uint16_t
是 unsigned short
的别名,占 2
个字节
_occupied
是uint16_t
类型,uint16_t
是 unsigned short
的别名,占 2
个字节
总结:所以最后计算出cache
类的内存大小 = 12 + 2 + 2 = 16
字节
接下来就是如何获取bits了
要获取bits
的中的内容,只需通过类的首地址平移32
字节即可
我们刚才发现。其中的data()
获取数据,我们先利用lldb
再次打印看能否观察出有价值的信息
x/6gx Person.class
打印出类的首地址
p (class_data_bits_t *)0x100002568
打印出平移了32
位的bits
信息
最后我们打印发现了一个
class_rw_t
.我们查看源代码发现
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->methods;
} else {
return method_array_t{v.get()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->properties;
} else {
return property_array_t{v.get()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->protocols;
} else {
return protocol_array_t{v.get()->baseProtocols};
}
}
这三个是不是对应的方法
,属性
,协议
呢?lldb
调试日志如下
p $3.methods()
打印出方法数组
p $4.list
获取元类方法数组
的方法列表
p *$5
获取元类方法列表
的第一个方法
我们通过
p $6.get(0)
,p $6.get(1)
, p $6.get(2)
, p $6.get(3)
依次打印出来instanceMethod
,cxx_destruct
方法和两个属性方法attributes
,setAttributes
。
通过上述内容最终得出
类的实例方法存储在类的bits
属性中,例如Person
类的实例方法instanceMethod
就存储在 Person
类的bits
属性中