iOS开发进阶:Runtime
iOS开发进阶:Runtime
镇长基础部分
基本术语
SEL : 选择器,指方法的名字
1 | typedef struct objc_selector *SEL; |
IMP :指针,指向方法的具体实现地址
1 | typedef void (*IMP)(void /* id, SEL, ... */ ); |
id :表示任意的OC类型
1 | /// 代表OC类 |
Class
是objc_class
结构体类型的指针。id
是objc_object
结构体类型的指针。id
表示对象,Class
表示类。
isa : 指向实例所属的类
1 | struct objc_object { |
- 实例的
isa
指向class
,当调用对象方法时,通过isa
找到对应的类,找到对应的方法进行调用。如果没有通过superclass
查找父类。 - 类的
isa
指向meta-class
,当调用类方法时,通过isa
找到对应的元类,找到对应的方法进行调用。如果没有通过superclass
查找父类。
Method
1 | typedef struct method_t *Method; |
获取方法列表:
1 | // cls : 类,outCount: 方法数量 |
Ivar : 实例变量
1 | typedef struct ivar_t *Ivar; |
Category :分类
1 | typedef struct category_t *Category; |
objc_property_t : 实例属性
1 | typedef struct property_t *objc_property_t; |
获取属性列表:
1 | Ivar * class_copyIvarList(Class cls, unsigned int *outCount) |
Cache : 缓存
1 | // 缓存曾经调用过的方法,提高查找速率 |
objc_object : 对象
1 | struct objc_object { |
objc_class : 类
1 | struct objc_class : objc_object { |
继承自
objc_object
, 所以objc_class
也是对象,有成员变量isa
。
元类
元类是类对象的类。objc_class
继承自 objc_object
它也包含 isa
指针,类自身也是对象,称为类对象。类对象对应的类就称为元类。实例对象的 isa
指针指向所对应的类,类的 isa
指针指向元类。
method_t
1 | // method_t是对方法/函数的封装 |
SEL
:函数名,通过@selector()
和sel_registerName()
获取。通过sel_getName()
和NSStringFromSelector()
转成字符串。types
: 表示返回值类型和参数类型,使用 Type Encodings - NSHipster,另外,iOS提供了一个叫@encode
的指令,可以将具体的类型表示成字符串编码。
category_t
1 | struct category_t { |
分类的底层结构 category_t
结构体,里面存储着分类的对象方法、类方法、属性、协议信息等。
在程序运行时,运行时系统会将分类的数据,合并到类信息中(类对象或者元类对象中)。category
不同于extension
,前者运行时合并信息,后者编译时就已经合并。
分类中有load
方法吗?load
方法的调用顺序?
分类中存在 load
方法,在运行时加载类、分类的时候调用。
另外,load
方法可以继承,通常情况下系统会自动调用,无需手动。
先调用类的,按照编译顺序调用。调用子类的之前先调用父类的。在调用分类的 load
方法,按照编译顺序(先编译先调用)。
提到 load
方法,还有一个叫 initialize
方法。
initialize
方法会在类第一接收到消息时调用。先调用父类,在调用子类。initialize
是通过 objc_msgSend()
方法实现的。
不一样的 isa
网上很多关于 isa
的资料,发现大部分都是旧版的 isa
结构。所以学习新的运行时源码整理下面的笔记。
旧版 runtime
中 isa
,在苹果提供的运行时源码 runtime.h
文件中,isa
定义如下:
1 | typedef struct objc_class *Class; |
isa
是objc_class
结构体类型的指针。
源码文件 objc_runtime_new.h/mm
。 新版 isa
此时不再是 objc_class
结构体类型,修改成了 isa_t
类型。 这段代码是运行时系统定义的对象结构体。
1 | struct objc_object { |
另外,上面也提及到类的结构体 objc_class
继承自 objc_object
结构体,那么 objc_class
也包含 isa_t
类型的变量。
1 | struct objc_class : objc_object { |
接下来我们看看 isa_t
究竟是什么?不过在此之前先复习一下共用体。
共用体
首先回顾一下 共用体
特点,通过对比结构体来理解共用体。
- 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙)。
- 共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
上面提到共用体的特性:共用体占用的内存等于最大的成员占用的内存。这就说明共用体中的成员是共用一段内存,修改其中的值势必会修改其他的值。
下面通过一段代码理解共用体。
1 | union data { |
输出结果:
1 | 4, 4 |
共用体的内存结构。
在每次存入值时,共用体中成员的值都会发生改变。
isa_t
共用体
首先,运行时源代码 isa_t
的定义如下:
1 | union isa_t { |
参考上面的注释。
获取ISA
新版的 isa
获取真实的内存地址需要进行一次位运算。
1 | # if __arm64__ // arm64位系统 |
获取真实内存地址 (Class)(isa.bits & ISA_MASK);
验证上面结论:
1 | NSObject *objc = [[NSObject alloc] init]; |
设置断点,通过 LLDB
命令:
1 | (lldb) p/x objc->isa |
objc实例对象的isa指向的地址:0x001dffff8da8a141
;objcClass的指向的地址值:0x00007fff8da8a140
。 通过一次位运算 0x001dffff8da8a141 & ISA_MASK
等到 0x00007fff8da8a140
。
窥探 objc_class
结构
上面基础术语中给出了 objc_class
结构体的定义:
1 |
|
bits
获取地址,通过&FAST_DATA_MASK
运算获取。superclass
: 指向父类的指针。cache
: 方法缓存,后面详细讲解。isa
: 指向元类。data
: 指向class_rw_t
结构体类型。
class_rw_t
结构体:
1 | struct class_rw_t { |
methods
、properties
、protocols
都是二维数组,包含类的初始内容、分类的内容和协议等。
class_ro_t
结构体:
1 | struct class_ro_t { |
进阶部分
KVO
KVO实现原理
iOS
是如何实现对一个对象的 KVO
?
通过运行时系统动态生成一个子类,并且让实例对象的 isa
指向这个全新的子类。当实例对象的属性被修改时,通过调用 Foundation
的 NSSetXXXValueAndNotify
函数。实现如下:
willChangeValueForKey:
- 父类原来的Setter
didChangeValueForKey:
内部会调用监听器的监听方法:observeValueForKeyPath:ofObject:change:context:
。
如果想要手动触发 KVO
就需要手动调用,willChangeValueForKey:
和 didChangeValueForKey:
方法。 直接修改成员变量不会触发KVO
,但是通过属性可以触发 KVO
。
关联对象
有时需要给分类添加成员变量,通常使用 关联对象
间接实现。
1 | objc_setAssociatedObject |
推荐用法: 使用get方法的@selector作为key。
1 | objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) |
实现原理
参考文章:OS底层原理总结 - 关联对象实现原理
objc_msgSend、动态方法解析、消息转发
在 OC
中调用方法,其实都是转成 objc_msgSend
方法。它的执行流程分为三步:
- 消息发送
- 动态方法解析
- 消息转发