
我们之前的篇幅介绍了对象,也知道对象是一个类的实例。那么它的结构又是怎么样的。为了更直接的观察。我们做好充足的前戏提前定义好了两个类。
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<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;
计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是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<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->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属性中