OC底层原理10:类 & 类结构分析

张建 lol

前言

本章主要的目的是分析 类结构

objc_class 和 objc_object

为什么 对象 都有 isa指针

iOS-OC底层原理08:isa和类关联探索 中,使用 clang 编译过 main.m 文件,从编译后的 main.cpp 文件中,我们分析了 对象本质结构体,如下:

  • NSObject 的底层编译是 struct objc_object 结构体
1
2
3
4
5
6
7
8
9
typedef struct objc_object NSObject;
struct NSObject_IMPL {
Class isa;
};

typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};

由编译后的 c++ 源码可知:

  • NSObject 底层被编译成结构体 struct objc_object 类型,因此 对象 的本质是 结构体

  • isa指针Class 类型的,是由 struct objc_calss 结构体定义的类型,所有的 Class 都是以 objc_class 为模板创建的

  • objc_object 结构体内部有 objc_class 这个结构体,那么问题来了

【问题】objec_classobjc_object 有什么关系呢?

我们通过查看 objc4源码 找到 objc_classobjc_object 的定义,来分析一下两者之间的关系:

  • objc4 源码中搜索 objc_class,其源码定义如下:

    • 一个位于 runtime.h 文件中,已经被废弃了

  • 一个位于 objc-runtime-new.h 文件中,是可用的

  • objc4 源码中搜索 objc_object,其源码定义如下:

    • 位于 objc.h 文件中

通过上述查找源码和其定义,可总结出以下几点内容:

  • 结构体类型 objc_class 继承自 objc_object 类型,其中 objc_object 也是一个结构体,且有一个 isa 属性,所以 objc_class 也拥有了 isa 属性

  • mian.cpp 底层编译文件中,NSObject 中的 isa 在底层是由 Class 定义的,其中 class 的底层编码来自 objc_class 类型,所以 NSObject 也拥有了 isa属性

  • NSObject 是一个类,用它初始化一个实例对象 objcobjc 满足 objc_object 的特性(即有isa属性),主要是因为 isa 是由 NSObjectobjc_class 继承过来的,而 objc_class 继承自 objc_objectobjc_objectisa 属性。所以 对象 一个 isa,isa表示指向,来自于当前的 objc_object

  • objc_object(结构体) 是 当前的 根对象,所有的 对象 都有这样一个特性 objc_object,即拥有 isa属性

【百度面试题】objc_object 与 对象的关系?

  • 所有的 对象 都是 来自NSObject(来自于OC端),但是真正到 底层 是一个 objc_object(C/c++) 结构体 类型的

  • 所有的 对象 都是 以objc_object 模板 继承 过来的

【总结】:

  • 所有的 对象 + 类 + 元类 都有 isa

  • 所有的 对象 都是由 objc_object继承 来的

  • 简单概括就是 万物皆对象,万物皆来源于 objc_object,有以下两点结论:

    • 所有以 objc_object 为模板创建的 对象,都有 isa属性
    • 所有以 objc_class 为模板创建的 ,都有 isa属性
  • 在结构层面可以通俗的理解为 上层OC底层 的对接:

    • 底层是通过 结构体 定义的 模板,例如 objc_class、objc_object
    • 上层 是通过底层的模板创建的一些类型,例如 ZJPerson

补充知识-内存偏移

在分析 类信息 中存储哪些信息之前,需要先 了解内存偏移,因为分析 类信息 需要用到内存偏移

  1. 【普通指针】

定义一个方法

1
2
3
4
5
6
7
8
9
10
// 普通指针
void testOffset1(){
// a/b 是 变量 、 10 是 值
int a = 10;
int b = 10;
// a/b : 打印得值 &a/&b : 取a的地址指针
NSLog(@"%d---%p",a,&a);
NSLog(@"%d---%p",b,&b);
}

在mian.m调用,查看打印结果:

1
2
2020-10-04 13:41:07.378260+0800 KCObjc[23781:838866] 10---0x7ffeefbff4a8
2020-10-04 13:41:07.378948+0800 KCObjc[23781:838866] 10---0x7ffeefbff4ac

由上述的打印结果可知:

  • a、b 都是 常量型变量a、b地址指针不一样,分别指向 值10,在内存中 10 是通过 值拷贝,分别赋值给 a、b

  • a 的地址是 0x7ffeefbff4a8b 的地址是 0x7ffeefbff4ac,他们相差 4字节,这是由 a/b 本身类型(Int占4个字节) 决定的

其地址指针指向如图所示:

  1. 【对象指针】

定义一个方法

1
2
3
4
5
6
7
8
// 对象指针
void testOffset2(){
// p1:指针
ZJPerson * p1 = [ZJPerson alloc];
ZJPerson * p2 = [ZJPerson alloc];
NSLog(@"%@---%p",p1,&p1);
NSLog(@"%@---%p",p2,&p2);
}

在mian.m调用,查看打印结果

1
2
2020-10-04 14:14:59.341733+0800 KCObjc[24079:859357] <ZJPerson: 0x101035830>---0x7ffeefbff4a0
2020-10-04 14:14:59.341901+0800 KCObjc[24079:859357] <ZJPerson: 0x101035980>---0x7ffeefbff4a8

由上述的打印结果可知:

  • p1、p2一级地址指针p1、p2 是指向 [ZJPerson alloc] 申请内存空间

  • &p1、&p2二级指针&p1、&p2 指向 p1、p2 对象的 一级地址指针

其地址指针指向如图所示:

  1. 【数组指针】

定义一个方法

1
2
3
4
5
6
7
8
// 数组指针
void testOffset3(){
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);
}

在mian.m调用,查看打印结果

1
2
2020-10-04 14:38:14.720430+0800 KCObjc[24171:872712] 0x7ffeefbff490 -- 0x7ffeefbff490 - 0x7ffeefbff494
2020-10-04 14:38:14.720522+0800 KCObjc[24171:872712] 0x7ffeefbff490 -- 0x7ffeefbff494 - 0x7ffeefbff498

由上述的打印结果可知:

  • &c&c[0] 都是取 首地址,即数组名等于首地址

  • &c&c[1] 相差 4个字节,地址之间相差的字节数,主要 取决于 存储的 数据类型

  • *d 代表取 c 的地址,可以通过 lldb 调试 p *(d+1)、*(d+2) 获取 地址 所对应的值 2、4

  • 可以通过 首地址+偏移量 取出数组中的其他元素,其中偏移量 数组的 下标,内存中首地址实际移动的 字节数 等于 偏移量 * 数据类型字节数

其地址指针指向如图所示:

探索类信息中都有哪些内容

由前文可知,所有的 都是以 objc_class 模板创建的,通过 objc4 源码可知,objc_class 中包含许多属性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct objc_class : objc_object {
// Class ISA; // 继承自objc_object 的isa指针
Class superclass; // Class 类型的 superclass
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

// 获取data
class_rw_t *data() const {
return bits.data();
}

//...此处省略许多方法
}

【这些属性的定义】:

  • isa 属性:继承自 objc_object 的isa,占 8 字节

  • superclass 属性:Class 类型,Class 是由 objc_object 定义的,是一个指针,占 8 字节

  • cache 属性:cache_t 类型

  • bits 属性:简单从类型 class_data_bits_t 目前无法得知,而 class_data_bits_t 是一个结构体类型,结构体的 内存大小 需要根据 内部的属性 来确定,而 结构体指针才是8字节,只有 首地址 经过上面 3个属性内存大小总和平移,才能获取到 bits

结论:
我们知道了 objc_class 中存储了 信息,那么如何去验证呢?

【验证方法】lldb 调试验证:

  • 自定义一个ZJPerson类,继承自NSObject
1
2
3
4
@interface ZJPerson : NSObject
@end
@implementation ZJPerson
@end
  • main.m 中初始化 ZJPerson 类和 NSObject
1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...

NSObject * obj = [NSObject alloc];
ZJPerson * p = [ZJPerson alloc];
NSLog(@"Hello, World! %@ - %@",obj,p);
}
return 0;
}
  • 下断点到 NSObject... 行,通过 lldb 调试,过程如下:

计算cache_t的内存大小

由上面的分析,我们知道了 cache_t 内存大小 和 内部属性 有关,因此我们需要进入 objc4-781新版本 内部源码去分析

进入 cachecache_t 的定义(只贴出了结构体中 非static 修饰的 属性,主要是因为 static类型 的属性 不存在结构体 的内存中),有如下几个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
// 是一个结构体指针类型,占8字节
explicit_atomic<struct bucket_t *> _buckets;
// 是 mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
explicit_atomic<mask_t> _mask;
#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

#if __LP64__
// 内部是 unsigned short 类型的 2字节
uint16_t _flags; // 2
#endif
// 内部是 unsigned short 类型的 2字节
uint16_t _occupied; // 2

}

计算 cache_t 内部属性的大小,最后的内存大小总和都是 16 字节

  • _buckets 类型是 struct bucket_t *,是结构体指针类型,占8字节

  • maskmask_t 类型,而 mask_tunsigned int 的类型,占4字节

  • _flagsuint16_t 类型,uint16_tunsigned short 的类型,占 2个字节

  • _occupieduint16_t 类型,uint16_tunsigned short 的类型,占 2个字节

总结:所以最后计算出 cache_t 类的内存大小 = 8 + 4 + 2 + 2 = 16字节

获取bits

由上述计算 Class类型 ISA 是 8字节Class superclass 是 8 字节cache_t cache 是 16字节; 可知,想要获取 bits 的中的内容,只需通过 类的 首地址平移 32字节(8 + 8 + 16) 即可

【验证】:通过lldb命令调试

自定义两个类 ZJPerson 和继承自ZJPerson的 ZJStudent 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface ZJPerson : NSObject
// 名字
@property (nonatomic,copy)NSString * name;
// 方法
- (void)test;
@end
@implementation ZJPerson
// 方法
- (void)test{
NSLog(@"来了!");
}
@end

@interface ZJStudent : ZJPerson
@end
@implementation ZJStudent
@end

main.m中定义两个类

1
2
3
4
ZJPerson * person = [ZJPerson alloc];
ZJStudent * student = [ZJStudent alloc];
// class_data_bits_t
NSLog(@"hello world :%@ %@",person,student);

下断点到 NSLoglldb 命令调试 bits 流程如下:

由调试结果可知:

  • 通过 p/x ZJPerson.class 获类的信息,即 首地址 信息

  • 通过 x/4gx 0x0000000100002188 拿到首地址 0x100002188,经过 32位平移 得到 bits 地址 0x1000021a8

  • 通过 p (class_data_bits_t *)0x1000021a8 获取 bits

  • 通过 p $1->data() 获取 class_rw_t

    • 如果 class_data_bits_t ** 号,说明是对象,访问对象用 ->
    • 如果 class_data_bits_t结构体,用 .
  • 通过 p *$2 打印 bitsclass_rw_t 内部信息,firstSubclass = ZJStudent,表示第一个继承子类 ZJStudent

class_rw_t

通过查看 class_rw_t 定义的源码发现,结构体中有提供相应的方法去获取 属性列表方法列表 等,如下所示:

属性列表(property_list)

【准备工作】:在ZJPerson中增加一个 属性 和一个 成员变量

1
2
3
4
5
6
7
8
@interface ZJPerson : NSObject
{
NSString * sex; // 性别
}
// 名字
@property (nonatomic,copy)NSString * name;
@end
@implementation ZJPerson

【探索】 由上图可知 propertiesproperty_array_t 类型的,其内部源码如下:

我们通过 lldb 调试属性列表,如下:

由lldb调试可知:

  • p $3.properties() 命令中的 propertoes 方法是由 class_rw_t 提供的,方法中返回的实际类型为 property_array_t

  • 由于 list 的类型是 property_list_t ,是一个指针,所以通过 p *$4 获取内存中的信息,同时也证明 bits 中存储了 property_list,即属性列表

  • p $6.get(0),获取ZJPerson中的第一个属性 name

  • p $6.get(1),想要获取 ZJPerson 中的成员变量 sex, 发现会报错,提示数组越界了,说明 property_list 中只有 一个属性 name

【问题】探索成员变量的存储

由此可得出 property_list 中只有 属性,没有 成员变量 ,属性与成员变量的区别就是 有没有set、get方法,如果 ,则是 属性,如果 没有,则是 成员变量

那么问题来了,成员变量 存储在哪里?为什么会有这种情况?请移至文末的分析与探索

方法列表(methods_list)

【准备工作】 在ZJPerson中增加两个方法,一个对象方法和一个类方法

1
2
3
4
5
6
7
8
9
10
11
12
@interface ZJPerson : NSObject
- (void)sayHello;
+ (void)sayBye;
@end
@implementation ZJPerson
- (void)sayHello{
NSLog(@"hello");
}
+ (void)sayBye{
NSLog(@"bye");
}
@end

【开始探索】通过 lldb 调试来获取方法列表,步骤如图所示:

由lldb调试可知:

  • 系统在编译的时候自动帮你生成

  • 通过 p $4.methods() 获得具体的 方法列表的list结构,其中 methods 也是class_rw_t 提供的方法

  • 通过 p *$6. 打印的 count = 4 可知,存储了 4 个方法,其中 syaHello自己 声明的方法,cxx_destruct、name、setName:系统 生成的方法

  • 可以通过 p $7.get(i)内存偏移 的方式获取单个方法,i 的范围是 0-3

  • 如果在打印 p $7.get(4),获取第五个方法,也会报错,提示 数组越界

补充知识-属性、成员变量、实例变量

我们通常都知道 @property属性 的意思,但是什么是 成员变量实例变量 呢?我们探索一下:

1、首先我们自定义一个类 ZJPerson,放在 main.m 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 成员变量 vs 属性
@interface ZJPerson : NSObject
{
NSString *hobby;
NSObject *objc;
}
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;
@end
@implementation ZJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {

// 属性&成员变量&实例变量
ZJPerson * P = [ZJPerson alloc];
NSLog(@"Hello, World!");
}
return 0;
}

2、我们通过 clang 编译一下这个 main.mmain.cpp ,来查看 C 的底层实现,步骤如下:

1)我们打开终端,cd到你要编译文件的文件夹下:

1
mac@192 ~ % cd /Users/mac/Desktop/逻辑教育/LG-OC底层大师班上课资料/20200914-大师班第5天-类原理分析-资料/01--课堂代码/001-类的属性与变量/001-类的属性与变量

2)输入编译命令clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxxx.m编译你的目标文件:

1
mac@192 001-类的属性与变量 % clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

3)然后你目录下就会重写一个 cpp 文件,内容比较多你可以搜索关键类 ZJPerson 对照查看:

3.我们打开 main.cpp 查找 ZJPerson 来查看一下:

  • hobbyobjc 没有下划线 _,是 成员变量
  • nickNamename 有下划线 _,是 属性

继续往下看:

  • hobbyobjc 没有没有生成 settergetter 方法,是 成员变量
  • nickNamename 有生成 settergetter 方法,是 属性

4、由上面我们知道了什么是成员变量和属性,那么实例变量呢?

  • hobby 不能进行实例化,是 成员变量

  • objc 能进行实例化,是 实例变量 ,也可以称特殊的成员变量,如: objc = [NSObject alloc] ;

5、我们用一下图表示 成员变量&实例变量&属性 的关系:

6、由上面的分析我们得知了,什么是 属性、成员变量和实例变量

由上面的探索我们了解了什么是 属性成员变量,我们也可以通过下面的方法来验证一下 属性列表成员变量列表 是不是正确的,如下:

  • zjObjc_copyIvar_copyProperies 函数:打印 变量属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void zjObjc_copyIvar_copyProperies(Class pClass){

unsigned int count = 0;
Ivar *ivars = class_copyIvarList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
// 获取实例变量名
const char*cName = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:cName];
ZJLog(@"class_copyIvarList:%@",ivarName);
}
free(ivars);

unsigned int pCount = 0;
objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
for (unsigned int i=0; i < pCount; i++) {
objc_property_t const property = properties[i];
// 获取属性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
// 获取属性值
ZJLog(@"class_copyProperiesList:%@",propertyName);
}
free(properties);
}

在main.m中调用

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
@autoreleasepool {
//属性&成员变量&实例变量
ZJPerson * P = [ZJPerson alloc];
// 打印
Class pClass = object_getClass(P);
zjTypes();
zjObjc_copyIvar_copyProperies(pClass);
NSLog(@"Hello, World!");
}
return 0;
}

查看打印结果:

1
2
3
4
5
6
7
class_copyIvarList:hobby
class_copyIvarList:objc
class_copyIvarList:_nickName
class_copyIvarList:_name

class_copyProperiesList:nickName
class_copyProperiesList:name

由结果可知:

  • 成员变量列表有4个:hobby、objc、_nickName、_name
  • 属性列表有2个: nickName、name

成员变量的存储(ivars)

由上面的属性列表分析可得出 property_list 中只有 属性,没有 成员变量,那么问题来了, 成员变量存储在哪里 ?为什么会有这种情况?

通过 objc4-781最新源码 查看 objc_classbits属性 中存储数据的类 class_rw_t 的结构发现,除了 methods、properties、protocols 方法,还有一个 ro 方法,其返回类型是 class_ro_t ,如下图所示:

点击进去查看 class_ro_t 定义,如下图:

发现其中有一个 ivars属性 ,我们可以做如下猜测:是否成员变量就存储在这个 ivar_list_t 类型的 ivars 属性中呢?

下面我们通过 lldb 的调试来验证一下:

由上图lld调试可知:

  • 通过 p *$7 打印成员变量列表,count = 2,我们知道 class_ro_t 中包含两个成员变量 sexname

  • 通过 bits --> data() -->ro() --> ivars 获取 成员变量 列表,除了包括 成员变量,还包括 属性 定义的 成员变量

  • 通过 @property 定义的属性,也会存储在 bits 属性中,通过 bits --> data() --> properties() --> list 获取属性列表,其中只存储 属性

类方法的存储

我们由前文探索方法列表可知,在 method_list没有类方法只有实例方法,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下

我们在 OC底层原理09:isa走向&继承分析 中,曾提及了 元类,类对象的 isa 指向就是 元类,元类是用来 存储类相关信息 的,所以我们猜测:是否 类方法 是否存储在 源类bits方法 中呢?可以通过 lldb 命令来验证我们的猜测。下图是 lldb 命令的调试流程:

由上图lld调试可知:

  • 类的 实例方法 存储在 类的bits属性 中,通过 bits --> methods() --> list 获取实例方法列表,例如 ZJPerson类 的 实例方法 sayHello 就存储在 ZJPerson类bits属性 中,类中的方法列表除了 包括实例方法,还包括系统自动生成的 属性set方法get方法cxx_destruct方法

  • 类的 类方法 存储 在元类bits属性 中,通过元类 bits --> methods() --> list 获取类方法列表,例如ZJPerson中的类方法 sayBye 就存储在ZJPerson类的 元类(名称也是ZJPerson)的 bits属性

  • Post title:OC底层原理10:类 & 类结构分析
  • Post author:张建
  • Create time:2020-09-24 21:58:37
  • Post link:https://redefine.ohevan.com/2020/09/24/OC底层原理/OC底层原理10:类 & 类结构分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.