iOS开发进阶:Runtime

基础部分

基本术语

SEL : 选择器,指方法的名字

1
typedef struct objc_selector *SEL;

IMP :指针,指向方法的具体实现地址

1
typedef void (*IMP)(void /* id, SEL, ... */ ); 

id :表示任意的OC类型

1
2
3
4
/// 代表OC类
typedef struct objc_class *Class;
/// 执行类实例的指针
typedef struct objc_object *id;

Classobjc_class 结构体类型的指针。 idobjc_object 结构体类型的指针。id 表示对象,Class 表示类。

isa : 指向实例所属的类

1
2
3
4
struct objc_object {
isa_t isa;
//...
};
  1. 实例的 isa 指向 class,当调用对象方法时,通过 isa 找到对应的类,找到对应的方法进行调用。如果没有通过 superclass 查找父类。
  2. 类的 isa 指向 meta-class,当调用类方法时,通过 isa 找到对应的元类,找到对应的方法进行调用。如果没有通过 superclass 查找父类。

Method

1
2
3
4
5
6
7
8
9
typedef struct method_t *Method;
struct method_t {
// 不同类的方法选择器可以是相同的
// typedef struct objc_selector *SEL;
// objc_selector 未开源,猜测应该与Char型相关。
SEL name; // 函数名,类似C语言字符串,@selector()和 sel_registerName()获取字符串。
const char *types; //编码(返回值类型、参数类型)使用Type Encode
IMP imp;//指向函数的指针(函数地址)
};

获取方法列表:

1
2
// cls : 类,outCount: 方法数量
Method * class_copyMethodList(Class cls, unsigned int *outCount)

Ivar : 实例变量

1
2
3
4
5
6
7
8
9
typedef struct ivar_t *Ivar;
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
};

Category :分类

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct category_t *Category;

// 分类结构体
struct category_t {
const char *name; // 名称
classref_t cls;
struct method_list_t *instanceMethods; // 实例方法列表
struct method_list_t *classMethods; // 类方法列表
struct protocol_list_t *protocols; // 协议列表
struct property_list_t *instanceProperties; // 属性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
};

objc_property_t : 实例属性

1
2
3
4
5
6
typedef struct property_t *objc_property_t;

struct property_t {
const char *name;
const char *attributes;
};

获取属性列表:

1
Ivar * class_copyIvarList(Class cls, unsigned int *outCount)

Cache : 缓存

1
2
3
4
5
6
7
8
9
10
11
12
// 缓存曾经调用过的方法,提高查找速率
struct cache_t {
struct bucket_t *_buckets; // 散列表, SLE :IMP
mask_t _mask; //散列表的长度 - 1
mask_t _occupied; // 已经缓存的方法数量,散列表的长度使大于已经缓存的数量的。
};
struct bucket_t {
cache_key_t _key; //SEL作为Key
IMP _imp; // 函数的内存地址
};
typedef uintptr_t cache_key_t; // ---> unsigned long 类型
typedef uint32_t mask_t;

objc_object : 对象

1
2
3
struct objc_object {
isa_t isa;
};

objc_class : 类

1
2
3
4
5
6
7
8
9
10
11
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() {
return bits.data(); // &FAST_DATA_MASK 获取地址值
}
// ...
};

继承自 objc_object, 所以 objc_class 也是对象,有成员变量 isa

元类

元类是类对象的类。objc_class 继承自 objc_object 它也包含 isa指针,类自身也是对象,称为类对象。类对象对应的类就称为元类。实例对象的 isa 指针指向所对应的类,类的 isa 指针指向元类。

method_t

1
2
3
4
5
6
7
8
9
10
// method_t是对方法/函数的封装
struct method_t {
// 不同类的方法选择器可以是相同的
// typedef struct objc_selector *SEL;
// objc_selector 未开源,猜测应该与Char型相关。
SEL name; // 函数名,类似C语言字符串,@selector()和 sel_registerName()获取。
const char *types; //编码(返回值类型、参数类型)使用Type Encode
IMP imp;//指向函数的指针(函数地址)
//...
};
  1. SEL:函数名,通过@selector()sel_registerName()获取。通过 sel_getName()NSStringFromSelector()转成字符串。
  2. types: 表示返回值类型和参数类型,使用 Type Encodings - NSHipster,另外,iOS提供了一个叫 @encode的指令,可以将具体的类型表示成字符串编码。

category_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct category_t {
const char *name; // 名称
classref_t cls; // 所属类
struct method_list_t *instanceMethods; // 实例方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

分类的底层结构 category_t 结构体,里面存储着分类的对象方法、类方法、属性、协议信息等。
在程序运行时,运行时系统会将分类的数据,合并到类信息中(类对象或者元类对象中)。
category不同于extension,前者运行时合并信息,后者编译时就已经合并。

分类中有load方法吗?load方法的调用顺序?

分类中存在 load 方法,在运行时加载类、分类的时候调用。
另外,load方法可以继承,通常情况下系统会自动调用,无需手动。
先调用类的,按照编译顺序调用。调用子类的之前先调用父类的。在调用分类的 load方法,按照编译顺序(先编译先调用)。

提到 load 方法,还有一个叫 initialize方法。

initialize 方法会在类第一接收到消息时调用。先调用父类,在调用子类。
initialize 是通过 objc_msgSend() 方法实现的。

不一样的 isa

网上很多关于 isa 的资料,发现大部分都是旧版的 isa 结构。所以学习新的运行时源码整理下面的笔记。

旧版 runtimeisa,在苹果提供的运行时源码 runtime.h 文件中,isa 定义如下:

1
2
typedef struct objc_class *Class;
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

isaobjc_class 结构体类型的指针。

源码文件 objc_runtime_new.h/mm。 新版 isa 此时不再是 objc_class 结构体类型,修改成了 isa_t 类型。 这段代码是运行时系统定义的对象结构体。

1
2
3
4
struct objc_object {
isa_t isa;
// ...
};

另外,上面也提及到类的结构体 objc_class 继承自 objc_object 结构体,那么 objc_class 也包含 isa_t 类型的变量。

1
2
3
4
5
6
7
8
9
10
11
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() {
return bits.data(); // &FAST_DATA_MASK 获取地址值
}
// ...
};

接下来我们看看 isa_t 究竟是什么?不过在此之前先复习一下共用体。

共用体

首先回顾一下 共用体 特点,通过对比结构体来理解共用体。

  1. 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙)。
  2. 共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

上面提到共用体的特性:共用体占用的内存等于最大的成员占用的内存。这就说明共用体中的成员是共用一段内存,修改其中的值势必会修改其他的值。

下面通过一段代码理解共用体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
union data {
int n; // 4
char ch; // 1
short f; // 2
};

int main(int argc, const char * argv[]) {
union data a;
printf("%lu, %lu\n", sizeof(a), sizeof(union data)); // 大小为 4

a.n = 0x40;
// %X 16进制输出; %hX 16进制输出short int(2字节)
printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 40, @, 40

a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 39, 9, 39

a.f = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 2059, Y, 2059

a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 3E25AD54, T, AD54
return 0;
}

输出结果:

1
2
3
4
5
4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

共用体的内存结构。

在每次存入值时,共用体中成员的值都会发生改变。

isa_t 共用体

首先,运行时源代码 isa_t 的定义如下:

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
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
# if __arm64__ // arm64位系统
# define ISA_MASK 0x0000000ffffffff8ULL //用来取出33位内存地址使用(&)操作
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; //0:代表普通指针,1:表示优化过的,可以存储更多信息。
uintptr_t has_assoc : 1; //是否设置过关联对象。如果没设置过,释放会更快
uintptr_t has_cxx_dtor : 1; //是否有C++的析构函数
uintptr_t shiftcls : 33; // 存储着Class、Meta-Class对象的内存地址信息
uintptr_t magic : 6; //用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; //是否有被弱引用指向过
uintptr_t deallocating : 1; //是否正在释放
uintptr_t has_sidetable_rc : 1; //引用计数器是否过大无法存储在ISA中。如果为1,那么引用计数会存储在一个叫做SideTable的类的属性中
uintptr_t extra_rc : 19; //里面存储的值是引用计数器减1

# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
// ....
};

参考上面的注释。

获取ISA

新版的 isa 获取真实的内存地址需要进行一次位运算。

1
2
3
4
5
# if __arm64__ // arm64位系统
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__ // arm32为系统
# define ISA_MASK 0x00007ffffffffff8ULL
# endif

获取真实内存地址 (Class)(isa.bits & ISA_MASK);

验证上面结论:

1
2
3
NSObject *objc = [[NSObject alloc] init];

Class objcClass = [NSObject class];

设置断点,通过 LLDB 命令:

1
2
3
4
(lldb) p/x objc->isa
(Class) $1 = 0x001dffff8da8a141 NSObject
(lldb) p/x objcClass
(Class) $2 = 0x00007fff8da8a140 NSObject

objc实例对象的isa指向的地址:0x001dffff8da8a141;objcClass的指向的地址值:0x00007fff8da8a140。 通过一次位运算 0x001dffff8da8a141 & ISA_MASK 等到 0x00007fff8da8a140

窥探 objc_class 结构

上面基础术语中给出了 objc_class 结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
12
#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct objc_class : objc_object {
// Class ISA;
Class superclass; //父类指针
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用于获取地址

class_rw_t *data() {
return bits.data(); // &FAST_DATA_MASK 获取地址值
}
// ...
};
  1. bits 获取地址,通过 &FAST_DATA_MASK 运算获取。
  2. superclass : 指向父类的指针。
  3. cache : 方法缓存,后面详细讲解。
  4. isa : 指向元类。
  5. data: 指向 class_rw_t 结构体类型。

class_rw_t 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_rw_t {

uint32_t flags;
uint32_t version;

const class_ro_t *ro; // 指向只读的结构体,存放类初始信息

/*
这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
methods中,存储 method_list_t ----> method_t
二维数组,method_list_t --> method_t
这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
*/
method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
property_array_t properties; // 属性列表
protocol_array_t protocols; //协议列表

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;
};

methodspropertiesprotocols都是二维数组,包含类的初始内容、分类的内容和协议等。

class_ro_t 结构体:

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
struct class_ro_t {
uint32_t flags; // 判断是否为元类
uint32_t instanceStart;
uint32_t instanceSize; // 实例对象占用的内存空间
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name; //类名
/*
一维数组,只读的,包含的是类的初始信息
*/
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

进阶部分

KVO

KVO实现原理

iOS是如何实现对一个对象的 KVO

通过运行时系统动态生成一个子类,并且让实例对象的 isa 指向这个全新的子类。当实例对象的属性被修改时,通过调用 FoundationNSSetXXXValueAndNotify函数。实现如下:

  1. willChangeValueForKey:
  2. 父类原来的Setter
  3. didChangeValueForKey:

内部会调用监听器的监听方法:observeValueForKeyPath:ofObject:change:context:

如果想要手动触发 KVO 就需要手动调用,willChangeValueForKey:didChangeValueForKey:方法。 直接修改成员变量不会触发KVO,但是通过属性可以触发 KVO

关联对象

有时需要给分类添加成员变量,通常使用 关联对象 间接实现。

1
2
3
objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects

推荐用法: 使用get方法的@selector作为key。

1
2
3
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))

实现原理

参考文章:OS底层原理总结 - 关联对象实现原理

objc_msgSend、动态方法解析、消息转发

OC 中调用方法,其实都是转成 objc_msgSend 方法。它的执行流程分为三步:

  1. 消息发送
  2. 动态方法解析
  3. 消息转发

消息发送

方法查找逻辑

动态方法解析

消息转发

参考