思考:
- 讲一下 OC 的消息机制
- 消息转发机制流程
- 什么是 Runtime?平时项目中有用过么?
- Runtime 的具体应用
打印结果分别是什么?
1234567891011121314151617181920212223242526272829303132333435//打印1@interface Person : NSObject@end@implementation Person@end@interface Student : Person@end@implementation Student- (instancetype)init{self = [super init];if (self) {NSLog(@"[self class] = %@", [self class]);NSLog(@"[super class] = %@", [super class]);NSLog(@"[self superclass] = %@", [self superclass]);NSLog(@"[super superclass] = %@", [super superclass]);}return self;}@end//打印2int main(int argc, const char * argv[]) {@autoreleasepool {BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];BOOL res3 = [[Person class] isKindOfClass:[Person class]];BOOL res4 = [[Person class] isMemberOfClass:[Person class]];NSLog(@"%d %d %d %d", res1, res2, res3, res4);}return 0;}以下代码能不能执行成功?如果可以,打印结果是什么?
12345678910111213141516171819@interface Person : NSObject@property (nonatomic, copy) NSString *name;- (void)print;@end@implementation Person- (void)print {NSLog(@"my name's %@", self.name);}@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];id cls = [Person class];void *obj = &cls;[(__bridge id)obj print];}@end
Objective-C 是一门动态性比较强的编程语言,跟 C、C++ 等语言有着很大的不同,Objective-C 的动态性是由 Runtime API 来支撑的,Runtime API 提供的接口基本都是 C 语言的,源码由 C\C++\汇编语言编写。
isa 详解
学习 Runtime,首先要了解它底层的一些常用数据结构,比如 isa 指针。在 arm64 架构之前,isa 就是一个普通的指针,存储着 Class、Meta-Class 对象的内存地址。从 arm64 架构开始,对 isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
位运算
|
|
打印结果:
tall(1个字节)+ rich(1个字节)+ handsome(1个字节)+ isa(8个字节)= 11个字节。根据内存对齐原则,person 的内存大小是16个字节。
因为 tall、rich 和 handsome 都是 BOOL 类型,它们的值只有0和1,所以可以用3个二进制位来存储他们的值。
设计
定义一个 char 类型的变量 _tallRichHandsome 用来存储3个 BOOL 类型变量的值:
_tallRichHandsome 占1个字节(8位:0b 0000 0000
),让它最右边的3位(0b00000111
)分别存储 tall、rich 和 handsome:
取值
- 按位与运算符(
&
)
定义:参加运算的两个数据,按二进制位进行“与”运算。
运算规则:0&0=0
,0&1=0
,1&0=0
,1&1=1
。
总结:两位同时为1,结果才为1,否则结果为0。
因为“与”运算可以获取到特定位的值,所以可以通过“与”运算分别获取三个变量的值:
初始化 _tallRichHandsome
获取 tall(_tallRichHandsome & 0b00000001
)
获取 rich(_tallRichHandsome & 0b00000010
)
获取 handsome(_tallRichHandsome & 0b00000100
)
代码实现:
打印结果:
因为返回的是 BOOL 类型,而“与”运算取出的是有值(0b00000001
、0b00000100
)和0(0b00000000
),所以为了可以获取到 YES 和 NO,可以在“与”运算的结果前加!!
取反两次:
掩码
上面👆的实现太抽象,可以使用掩码增加可读性:
直接使用二进制定义掩码会更直观:
使用位移运算符,简化代码:
- 左移运算符(
<<
)
定义:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
最终实现:
设值
按位或运算符(
|
)
定义:参加运算的两个对象,按二进制位进行“或”运算。
运算规则:0|0=0
,0|1=1
,1|0=1
,1|1=1
。
总结:参加运算的两个对象只要有一个为1,其值为1。取反运算符 (
~
)
定义:参加运算的一个数据,按二进制进行“取反”运算。
运算规则:~1=0
,~0=1
。
总结:对一个二进制数按位取反,即将0变1,1变0。
设置 YES 时,跟 _tallRichHandsome 进行按位“或”运算,修改特定位置的值。
设置 NO 时,先对 rich 的二进制数按位取反,再跟 _tallRichHandsome 进行按位“与”运算。
初始化 _tallRichHandsome
设置 tall 为 YES(_tallRichHandsome |= 0b00000001
)
设置 rich 为 NO(_tallRichHandsome &= ~0b00000010
)
设置 handsome 为 YES(_tallRichHandsome |= 0b00000001
)
代码实现:
打印结果:
位域
位域,C 语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
机构体的第一个成员变量在结构体内存的最右边一个二进制位,其它变量依次从左往右排。
使用位域增加可读性。定义结构体 _tallRichHandsome,成员变量 tall,并通过“:
”设置 tall 在内存中只占1位。
打印结果:
在断点1处查看 _tallRichHandsome 的内存:
内存中的“01”是十六进制的,转成二进制就是0b 0000 0001
。即 tall:YES。
在断点2处查看 ret 的内存:
内存中的“ff”是十六进制的(0xFF),转成二进制就是0b11111111
,转成十进制是255(无符号)或-1(有符号)。这是因为 tall 原本是一个二进制位,即 tall:0b1,而返回值要求的是 BOOL 类型的值(8位:0b00000000
),所以在返回时 tall 强转成了一个8位的值:
因为在系统中整数是以补码形式存放的,所以要想找到打印结果为“-1”的原因需要先算出 0b11111111
的原码。
- 补码求原码
如果补码的符号位为“0”,表示是一个正数,其原码就是补码。
如果补码的符号位为“1”,表示是一个负数,那么求给定的这个补码的补码就是要求的原码。
因为 tall 是一个 char 类型的整型变量,是有符号的(LLVM),所以此时的 0b11111111
是有符号的。
因为 0b11111111
的最高位是符号位为“1”,表示是一个负数,所以该位不变,仍为“1”。其余七位取反后为 0b10000000
;再加1,所以是 0b10000001
,十进制就是 -1。所以 tall 在设置为 YES 时,打印结果是 -1。
如果将 tall 设置为 NO 的话,_tallRichHandsome 和 ret 的内存都是 0b00000000
。
综上所述,在对 tall 进行修改时,会有两个返回值“-1”和“0”。为了保证返回的结果正确,可以使用上面👆提到过的取反两次!!
。
最终实现:
定义结构体 _tallRichHandsome,同时定义成员变量 tall、rich 和 handsome,并通过“:”设置她们在内存中只占1位:
打印结果:
_tallRichHandsome 的二级制就是0b00000111
,tall 是第一个成员变量在最右边,然后依次从左往右排。
共用体(union)
struct
struct 结构体里的成员变量各自拥有一块内存,单独存在:
定义一个结构体 Date,内部有三个 int 类型的成员变量 year、month 和 day:
打印结果:
因为三个变量各自拥有自己的内存,所以打印结果各不相同。
union
union 共用体里的成员变量共用一块内存,共用体的内存大小以成员变量的最大内存为准:
定义共用体 Date,内部有三个 int 类型的变量 year、month 和 day:
打印结果:
因为三个变量共用一块内存,所以三个变量访问的内存是同一块内存地址。
定义共用体 Date,内部有一个 int 类型的变量 year 和一个 char 类型的变量 month:
打印结果:
实现
将位运算和位域结合在一起定义一个共用体,用位运算读取/写入变量的值,用位域增加可读性:
打印结果:
这里定义的 tall、rich 和 handsome 都是占1个二进制位的,如果想要修改它们占二进制位的个数,bits 也要修改为相应的定义:
tall、rich 和 handsome 都是占4个二进制位,那 bits 就需要定义成 int 类型(4个字节),掩码也需要占4个二进制位:
掩码也可以写成:
或者
isa
在源码 objc4-781 中查找 isa 的定义。
找到 OC 对象的结构体 objc_object:
isa_t
可以看到 isa 是一个 isa_t 类型的变量,Jump To Definition -> isa_t:
位域中是一个宏 ISA_BITFIELD,ISA_BITFIELD 在 __arm64__
(真机) 和 __x86_64__
(mac电脑/模拟器) 架构有不同的定义:
将宏 ISA_BITFIELD
替换掉,保留真机(arm64)代码,可以看到一个比较完整的 isa_t:
因为 isa 指针的定义区分 __arm64__
(真机)和 __x86_64__
(mac/模拟器),所以需要用真机运行项目才能看到 ISA_BITFIELD
正确的成员变量的值:
查看 person 对象的 isa 指针的内存:
转成二进制:
位域
- nonpointer
0,代表普通的指针,isa 只存储着 Class、Meta-Class 对象的内存地址
1,代表优化过,isa 使用位域存储更多的信息 - has_assoc
是否有设置过关联对象,如果没有,释放时会更快 - has_cxx_dtor
是否有 C++ 的析构函数(.cxx_destruct),如果没有,释放时会更快 - shiftcls
存储着 Class、Meta-Class 对象的内存地址信息 - magic
用于在调试时分辨对象是否未完成初始化 - weakly_referenced
是否有被弱引用指向过,如果没有,释放时会更快 - deallocating
对象是否正在释放 - extra_rc
里面存储的值是引用计数器减1 - has_sidetable_rc
引用计数器是否过大无法存储在 isa 中,如果为1,那么引用计数会存储在一个叫 SideTable 的类的属性中
nonpointer:占1个二进制位,在最低位为1(第0位)。
has_assoc(has_associate):占1个二进制位,为0(第1位)。
has_cxx_dtor:占1个二进制位,为0(第2位)。
shiftcls:占33个二进制位(从第3位到第35位)。
magic:占6个二进制位(从第36位到第41位)。magic 的值可以从宏 ISA_MAGIC_VALUE
看到(1a)。magic == 1a 表示初始化成功。
weakly_referenced:占1个二进制位,为0(第42位)。
deallocating:占1个二进制位,为0(第43位)。
has_sidetable_rc:占1个二进制位,为0(第44位)。
extra_rc(extra_retain_count):占19个二进制位,为0(从第45位到63位)。
has_assoc 和 weakly_referenced
has_assoc 和 weakly_referenced 标记的是曾经是否设置过,如果添加了 __weak 和关联对象再移除掉,这两个变量的值依然是1:
查看内存:
如果没有,释放时会更快
如果 has_assoc、has_cxx_dtor 和 weakly_referenced 为0,即没有添加过关联对象、没有析构函数和没有被弱引用指向过,会让实例对象的释放变得更快。这点可以从源码里看出来。
销毁实列对象的方法 objc_destructInstance:
可以看到在销毁实例对象的方法里,判断了有没有析构函数和关联对象,如果有的话需要先处理析构函数和关联对象。
Jump To Definition -> clearDeallocating:
Jump To Definition -> clearDeallocating_slow:
在 clearDeallocating()
方法里判断了是否有弱引用指向过,如果有的话需要在 clearDeallocating_slow()
方法里处理 weakly_referenced。
ISA_MASK
通过 isa & ISA_MASK
能够取出 shiftcls 的值(Class、Meta-Class 对象的内存地址信息)。因为 ISA_MASK
最后面三位都是0,所以获取到的 Class、Meta-Class 对象的内存地址的最后三位肯定也为0。ISA_MASK
:
证明:
打印结果:
可以看到打印结果的最后一位都是”8“或”0“,即”1000
“或”0000
“。所以 Class、Meta-Class 对象的内存地址的最后三为为0。
位运算补充
用左移定义枚举的成员变量,用”或“运算传入多个值,用”与“运算获取传入的都有哪些值:
打印结果:
Class 的结构
类对象和元类对象都是 Class 类型的对象,元类对象是一种特殊的类对象。
在源码 objc4-781 中查找 objc_class 的定义。
结构图:
结构图里出现的 rw
和 ro
分别表示 readwrite 和 readonly。
class_rw_t
class_rw_t 里面的 methods、properties、protocols 是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
类的信息在编译时是放在 class_ro_t 里的,在程序运行时,会将类的 class_ro_t 里的信息和分类的信息(注意顺序)合并起来放到 class_rw_t 里。找到合并分类信息的方法 realizeClassWithoutSwift()
:
可以看到在处理分类的信息之前,先从类里取出了类信息 ro,然后初始化了 rw,再将 ro 保存到 rw 里。
method_array_t
methods 是用 method_array_t 定义的,method_array_t 是一个 list_array_tt 类型的二维数组,method_array_t 里存储的是数组 method_list_t,数组 method_list_t 里存储的是 method_t:
如果是类对象,methods 里保存的是对象方法,如果是元类对象,methods 里保存的是类方法。
property_array_t
properties 是用 property_array_t 定义的,property_array_t 是一个 list_array_tt 类型的二维数组,property_array_t 里存储的是数组 property_t,数组 property_t 存储的是 property_t:
protocol_array_t
protocols 是用 protocol_array_t 定义的,protocol_array_t 是一个 list_array_tt 类型的二维数组,protocol_array_t 里存储的是数组 protocol_ref_t,数组 protocol_ref_t 存储的是 protocol_ref_t:
class_ro_t
class_ro_t 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的初始内容。
method_list_t、ivar_list_t 和 property_list_t
|
|
method_t
method_t 是对方法\函数的封装。
IMP
IMP
代表函数的具体实现:
|
|
断点1打印 imp:
断点2查看 -(void)test
的内存(选择 Debug -> Debug Workflow -> Always Show Disassembly):
从打印结果可以看到,imp 指向的内存地址就是 -(void)test
方法的内存地址。
SEL
SEL
代表方法\函数名,一般叫做选择器,底层结构跟 char *
类似。
- 可以通过
@selector()
和sel_registerName()
获得。 - 可以通过
sel_getName()
和NSStringFromSelector()
转成字符串。 - 不同类中相同名字的方法,所对应的方法选择器是相同的
下面的代码需要用到 ClassInfo.h,并且需要真机运行:
打印结果:
types
types 包含了函数返回值、参数编码的字符串。
下面的代码需要用到 ClassInfo.h,并且需要真机运行,将 main.m 改成 main.mm:
例1:
断点1出打印 types:
“v16@0:8” 是类型编码:v
:返回值类型 void,16
:参数占的字节数之和(id(8个字节) + SEL(8个字节)),@
:第一个参数的类型 id,0
:第一个参数内存的开始位置,:
:第二个参数的类型 SEL,8
:第二个参数内存的开始位置(id 占了8个字节)。
下面的代码需要用到 ClassInfo.h,并且需要真机运行,将 main.m 改成 main.mm:
例2:
断点1出打印 types::
“i24@0:8i16f20” 是类型编码:i
:范围值类型 int,24
:参数占的字节数之和(id(8个字节) + SEL(8个字节)+ int(4个字节)+ float(4个字节)),@
:第一个参数的类型 id,0
:第一个参数内存的开始位置,:
:第二个参数的类型 SEL,8
:第二个参数内存的开始位置(id 占了8个字节),i
:第三个参数的类型 int,16
:第三个参数的开始位置(id 占了8个字节 + SEL 占了8个字节),f
:第四个参数的类型 float,20
:第四个参数的开始位置(id 占了8个字节 + SEL 占了8个字节 + int 占了4个字节)。
Type Encoding
Type Encodings 是 iOS 中提供的一个叫做 @encode 的指令,可以将具体的类型表示成字符串编码。
|
|
打印结果:
方法缓存
Class 内部结构中有个方法缓存 cache(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。
缓存查找:objc-cache.mm -> bucket_t * cache_t::find(cache_key_t k, id receiver)
cache_t 里通过 _buckets 缓存方法,通过 _mask 计算索引,通过 _occupied 统计已经缓存的方法的数量。_buckets 里缓存的是 bucke_t 结构体:
_mask
_mask 的值是散列表的长度-1,保证“与”运算的结果不会超出散列表的长度(&_mask <= _mask),即计算出的索引不会越界。
假设 _mask = 0b0000 1000:
散列表(哈希表)的实现逻辑:
1、实现一个方法1可以计算出索引;
2、实现一个方法2可以解决索引冲突(如:对索引减 1 计算出新的索引值);
使用求余 %
也可以实现散列表(哈希表),通过求余计算出的索引也可以保证不越界。
_buckets
_buckets 在初始化时的空间大小是指定好的,并且内部的数据都是 NULL(空间换时间)。如果 _buckets 里的数据满了,_buckets 会将数据清空 -> 扩容x2(一倍)-> 重新缓存。
先通过 mask_t begin = cache_hash(sel, m)
计算出索引 begin:
如果 begin 处没有值,缓存。
如果 begin 处有值,是当前需要缓存的方法,表示已经缓存过了直接返回。
如果 begin 处有值,不是当前需要缓存的方法,通过 (i = cache_next(i, m)
计算出新的索引,如果新的索引不等于 begin 则重新判断,如果新的索引等于 begin 则去扩容(bad_cache())。
__arm64__
下的 cache_next 方法:
空间换时间
散列表(哈希表)遍历元素的效率比数组高的原因是牺牲了一定的空间换取了时间。
例1:
|
|
打印结果:
断点处查看 _mask 和 _occupied:
索引 | 缓存的方法 |
---|---|
0 | bucket_t(_key = @selector(init), _imp) |
1 | bucket_t(_key = @selector(testPerson), _imp) |
2 | NULL |
3 | NULL |
第一次调用 [person testPerson]
即 objc_msgSend(objc_getClass("Person"), sel_registerName("testPerson"))
向 person 实例对象发送一条 sel_registerName("testPerson")
消息,person 会通过 isa 找到 Person 类对象查找 -(void)testPerson
方法,先查 cache(_buckets),没查到,再通过 bits 找到 class_rw_t 里的 methods 查,查到后返回。(如果没有找到,再通过 superclass 找到父类的类对象继续查找(查找方式相同)。假设在查找到基类的类对象时找到了 -(void)testPerson
方法,实列对象 person 会把 -(void)testPerson
方法缓存到 _buckets 里然后返回。)
在缓存 @selector(testPerson)
方法时,先计算出索引(1),然后检查索引处是否有值,没值,将 @selector(testPerson)
缓存到对象的索引处。
第二次调用 [person testPerson]
会先去实例对象 person 的 _buckets 里找,找到对应的索引处的值判断是否是当前方法 @selector(testPerson)
,如果是就直接返回。(如果不是就将索引减 1 继续在 _buckets 里查找,找到了就直接返回。如果找了一圈还没有找到,会同第一次一样去类对象和父类的类对象查找,找到后缓存到 _buckets 里并返回。)
例2:
|
|
打印结果:
断点处查看 _mask 和 _occupied:
索引 | 缓存的方法 |
---|---|
0 | bucket_t(_key = @selector(testStudent2), _imp) |
1 | NULL |
2 | NULL |
3 | bucket_t(_key = @selector(testStudent), _imp) |
4 | NULL |
5 | NULL |
6 | NULL |
7 | NULL |
在缓存 @selector(testStudent)
方法时,_buckets 的空间不够了,_buckets 清空数据 -> 扩容x2(8) -> 重新缓存。先计算出索引(3),然后检查索引处是否有值,没值,将 @selector(testPerson)
缓存到对象的索引处。
在缓存 @selector(testStudent2)
方法时,先计算出索引(0),然后检查索引处是否有值,没值,将 @selector(testStudent2)
缓存到对象的索引处。(如果索引值与 @selector(testStudent)
相同(3),检查到索引处有值,然后将索引减 1 获取到新的索引(2),再检查新的索引处是否有值,没值,将 @selector(testStudent2)
缓存到对象的索引处。)
例3
|
|
打印结果:
断点处查看 _mask 和 _occupied:
索引 | 缓存的方法 |
---|---|
0 | NULL |
1 | bucket_t(_key = @selector(studentTest), _imp) |
2 | NULL |
3 | NULL |
4 | NULL |
5 | bucket_t(_key = @selector(personTest), _imp) |
6 | NULL |
7 | NULL |
调用 [teacher teacherTest]
即 objc_msgSend(objc_getClass("Teacher"), sel_registerName("teacherTest"))
向 teacher 实例对象发送一条 sel_registerName("teacherTest")
消息,teacher 会通过 isa 找到 Teacher 类对象查找 -(void)teacherTest
方法,先查 cache(_buckets),没查到,再通过 bits 找到 class_rw_t 里的 methods 查,查到后缓存到 _buckets 里并返回。
调用 [teacher studentTest]
即 objc_msgSend(objc_getClass("Teacher"), sel_registerName("studentTest"))
向 teacher 实例对象发送一条 sel_registerName("studentTest")
消息,teacher 会通过 isa 找到 Teacher 类对象查找 -(void)studentTest
方法,先查 cache(_buckets),没查到,再通过 bits 找到 class_rw_t 里的 methods 查,没查到。 Teacher 类对象通过 superclass 找到父类 Student 类对象,并在 Student 类对象的 _buckets 里查找,没找到,再通过到 class_rw_t 里查找,查到后缓存到 Teacher 类对象的 _buckets 里并返回。
调用 [teacher personTest]
即 objc_msgSend(objc_getClass("Teacher"), sel_registerName("personTest"))
向 teacher 实例对象发送一条 sel_registerName("personTest")
消息,teacher 会通过 isa 找到 Teacher 类对象查找,先查找 _buckets,没查到,再到 class_rw_t 里的方法列表 methods 查找,没查到。Teacher 类对象会通过 superclass 找到父类 Student 类对象,并在 Student 类对象的 _buckets 里查找,没找到,再到 class_rw_t 里查找,没查到。 Student 类对象会通过 superclass 找到父类 Person 类对象,并在 Person 类对象的 _buckets 里查找,没查到,再到 class_rw_t 里查找,查到后缓存到 Teacher 类对象的 _buckets 里并返回。
小结
- 先查当前类对象的缓存 _buckets,再查当前类对象的方法列表 class_rw_t -> methods;
- 先查父类类对象的缓存 _buckets,再查父类类对象的方法列表 class_rw_t -> methods;
- 在当前类对象的缓存 _buckets 里查到后直接返回;
- 在当前类对象的方法列表 class_rw_t -> methods 里查到后,先缓存到当前类对象的 _buckets 里,再返回;
- 在父类类对象的缓存 _buckets 里查到后,先缓存到当前类对象的 _buckets 里,再返回;
- 在父类类对象的方法列表 class_rw_t -> methods 里查到后,先缓存到当前类对象的 _buckets 里,再返回;
objc_msgSend
OC 中的方法调用,其实都是转换为 objc_msgSend 函数的调用。objc_msgSend 的执行流程可以分为三大阶段,即消息发送、动态方法解析和消息转发。
objc_msgSend 执行流程
_objc_msgSend 的入口在汇编文件 objc-msg-arm64.s 里。runtime 的实现是用 c、c++ 和汇编语言组成的,对于一些调用频次比较高的方法一般使用汇编语言实现。对于 _objc_msgSend 等方法,为了提高效率都是使用汇编语言实现的。
在源码 objc4-781 中查找 _objc_msgSend 的实现。
_objc_msgSend
ENTRY 的定义,ENTRY 是一个宏:
_objc_msgSend 的定义,从 ENTRY 开始,到 END_ENTRY 结束:
_objc_msgSend 涉及相关方法的实现
_lookUpImpOrForward
👉 _lookUpImpOrForward 的实现在 objc-runtime-new.mm 文件。老版本的 runtime 源码在这里调用的是 __class_lookupMethodAndLoadCache3
,_class_lookupMethodAndLoadCache3
函数里调用的才是 lookUpImpOrForward:
_lookUpImpOrForward 是一个通过 c 语言实现的函数(对于函数名,汇编语言转 c 语言需要去掉一个“_
”)。
消息发送相关方法实现
resolveMethod_locked()
动态方法解析相关方法实现
__objc_msgForward_impcache
消息转发相关方法的实现
👉 __objc_msgForward_impcache 方法的实现在汇编文件 objc-msg-arm64.s
👉 _objc_forward_handler 的实现在 C 语言文件 objc-runtime.mm。
这里的 _objc_forward_handler 指针存储的是 objc_defaultForwardHandler 的函数地址。因为 _objc_forward_handler 没有开源,所以看不到其具体的内部实现,即无法知道该方法在消息转发阶段具体做了什么。在报错信息里可以看到消息转发最后调用了 __forwarding__
方法:
报错信息:
通过反编译可以看到 _objc_forward_handler 的具体实现,这里有一份根据汇编代码翻译成的 C 语言伪代码 __forwarding__.c
:
消息发送
- receiver 通过 isa 指针找到 receiverClass,receiverClass 通过superclass 指针找到 superClass
- 如果是从class_rw_t中查找方法
已经排序的,二分查找
没有排序的,遍历查找
流程解析:
- 首先判断 receiver 是否为空,如果 receiver 为空直接退出,如果 receiver 不为空则到 receiverClass 的 cache 中查找方法;
- 从 receiverClass 的 cache 中查找方法,找到了方法,则调用方法结束查找。没找到方法,则从 receiverClass 的 class_rw_t 中查找方法;
- 从 receiverClass 的 class_rw_t 中查找方法,找到了方法,则将方法缓存到 receiverClass 的 cache 中,并调用方法结束查找。没有找到方法,则从 superclass 的 cache 中查找方法;
- 从 superclass 的 cache 中查找方法,找到了方法,将方法缓存到 receiverClass 的 cache 中,并调用方法结束查找。没找到方法,则从 superclass 的 class_rw_t 中查找方法;
- 从 superclass 的 class_rw_t 中查找方法,找到了方法,则将方法缓存到 receiverClass 的 cache 中,并调用方法结束查找。没有找到方法,则判断上层是否还有 superclass;
- 判断上层是否还有 superclass,有,则回到第4步。没有,则开始动态方法解析;
动态方法解析
开发者可以实现以下方法,来动态添加方法实现
12+ (BOOL)resolveInstanceMethod:(SEL)sel;+ (BOOL)resolveClassMethod:(SEL)sel;动态解析过后,会重新走“消息发送”的流程(“从 receiverClass的cache 中查找方法”这一步开始执行)
动态添加对象方法
|
|
打印结果:
Method 是指向结构体 method_t 的指针,即 struct objc_method == struct method_t,所以 class_getInstanceMethod(self, @selector(other))
返回的是结构体 method_t。Method 的定义:
证明:
打印结果:
动态添加C语言函数
|
|
动态添加类方法
|
|
打印结果:
消息转发
- 开发者可以在 forwardInvocation: 方法中自定义任何逻辑
- 以上方法都有对象方法、类方法2个版本(前面可以是加号+,也可以是减号-)
对象方法的消息转发
-forwardingTargetForSelector: 方法
-forwardingTargetForSelector:
方法有返回值时,返回值调用方法:
打印结果:
-methodSignatureForSelector: 方法
-forwardingTargetForSelector:
方法没有返回值时,会调用 -methodSignatureForSelector:
方法获取类型编码:
打印结果:
如果 -methodSignatureForSelector:
方法没有返回类型编码,则会报错:
从调用栈可以看到停留在了 doesNotRecognizeSelector:
方法:
类型编码的另一种返回方式:
因为 Student 实现了 -(void)test:(int)age
方法,所以调用 Student 的 methodSignatureForSelector:
方法可以返回 -(void)test:(int)age
方法的类型编码。
类方法的消息转发
+forwardingTargetForSelector: 方法
在 +forwardingTargetForSelector:
方法里返回类对象:
打印结果:
在 +forwardingTargetForSelector:
方法里返回实列对象:
打印结果:
+methodSignatureForSelector: 方法
+forwardingTargetForSelector:
方法没有返回值时,会调用 +methodSignatureForSelector:
方法获取类型编码:
打印结果:
[Person test] 的本质是 objc_msgSend([Person class], @selector(test)),会先走一遍“消息发送”流程。因为 Person 没有实现 -(void)test
方法,所以
NSInvocation
NSInvocation 封装了一个方法调用,包括:方法调用者、方法名、方法参数和返回值(类型编码决定 NSInvocation 的方法参数和返回值)。
anInvocation.target 方法调用者
anInvocation.selector 方法名
[anInvocation getArgument:NULL atIndex:0] 方法参数
示例代码:
👉 通过 getArgument:atIndex:
方法获取参数:
打印结果:
因为 -(void)test:(int)age
的 C 语言实现是 void test(id self, SEL _cmd, int age)
,一共有三个参数,参数顺序:receiver、selector 和 other argument,所以参数 age 的下标是 2。
👉 调用 invokeWithTarget:
方法,将消息转发给 Student 的实例对象:
打印结果:
在调用 invokeWithTarget:
方法前,anInvocation 的 target 是 person 对象,selector 是 -(void)test:(int)age
方法,参数是 15。在调用 invokeWithTarget:
方法后, anInvocation 的 target 就变成了 student 对象了。相当于向 student 对象发送了一条“test:”消息 objc_msgSend([[Student alloc] init], @selector(test:))
。
👉 调用 getReturnValue:
方法获取返回值:
打印结果:
@synthesize、@dynamic
@synthesize 会自动生成属性 age 的成员变量 _age,同时生成属性 age 的 setter 和 getter 方法的实现。现在的 xcode 都是默认生成了,不用手写了。
打印结果:
@dynamic 是告诉编译器不需要自动生成属性 age 的成员变量 _age,也不需要生成属性 age 的 setter 和 getter 方法的实现。
报错:unrecognized selector sent to instance
使用动态方法解析解决这个问题:
打印结果:
小结
forwardingTargetForSelector:
、methodSignatureForSelector:
和forwardInvocation:
方法本身并没有区分对象方法和类方法,但是在 _objc_forward_handler 的实现中,receiver (实列对象/类对象)会调用对应的方法(对象方法/类方法),所以实现的方法类型需要跟返回的类型统一。消息转发中,不要在意方法是对象方法还是类方法,本质还是 objc_msgSend 的消息接收者和方法名(实例对象 - 对象方法,类对象 - 类方法)。
super 的本质
|
|
打印结果:
这里的 self 就是 person 实例对象,[self class]
返回的是 person 实例对象的类对象,[self superclass]
返回的是父类 Person 的类对象。
思考:
super 代表的是 student 的父类 person,那么 [super class]
应该就等于 [person class]
,打印结果应该是 Person,而 [super class]
的打印结果却是 Student❓同样的 [super superclass]
应该就等于 [person superclass]
,打印结果应该是 NSObject,而 [super superclass]
打印结果却是 Person❓
objc_super 结构体
定义一个 -(void)run
方法:
通过终端命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Student.m
生成 c++ 代码 Student.cpp,查看 -(void)run
方法的 c++ 实现:
简化后的 [super run]
:
可以看到 [super run]
的底层实现调用的是 objc_msgSendSuper()
方法,第一个参数是 __rw_objc_super
结构体,第二参数是 @selector(run)
。所以 super 的本质就是 __rw_objc_super
结构体:
__rw_objc_super
结构体是在编译时生成的,并将参数传入 objc_msgSendSuper() 方法。但是 objc_msgSendSuper() 在定义时该位置的参数是一个 objc_super 结构体:
因为 __rw_objc_super
和 objc_super 的结构基本一致,所以 __rw_objc_super
结构体算是一个自定的 objc_super,作为参数传给 objc_msgSendSuper()。
因为现在使用的是 __OBJC2__
,所以 objc_super 可以简化为:
可以看到 objc_super 结构体的第一个参数是消息接收者,第二个参数是消息接收者的父类。所以 [super run]
的底层实现就是:
objc_msgSendSuper() 方法
通过终端命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Student.m
生成的 c++ 代码 Student.cpp,查看 [super run]
的 c++ 实现是调用的 objc_msgSendSuper() 方法。
关于 [super run]
调用的方法 objc_msgSendSuper():
objc_msgSendSuper:向实例对象的 super 发送带有简单返回值的消息。第一个参数:super,第二个参数:op(方法的选择器 SEL)。
- super:是一个指向
objc_super
结构体的指针,在结构体的内部有接收消息的实例对象和开始搜索方法实现的类对象(从该类对象开始搜索方法的实现)。 - op:SEL 类型的指针。传递方法的选择器(@selector(run)),该方法就是要处理的消息。
通过注释,可以知道 objc_msgSendSuper() 有两个参数 super 和 SEL,其中 super 里有一个消息接收者和一个类对象。objc_msgSendSuper() 会从 super 里的类对象开始查找 SEL,找到后交给 super 里的消息接收者处理。
综上所述:[super run]
:设置消息接收者是 self(student 实例对象),并从 Person 类对象开始查找 -(void)run
方法:
[super method]
流程图:
objc_msgSendSuper2()
查看汇编代码
在 [super superclass];
处添加断点,选择 Debug -> Debug Workflow -> Always Show Disassembly:
可以看到 [super class]
和 [super superclass]
底层实现调用的并不是 objc_msgSendSuper()
方法而是 objc_msgSendSuper2()
方法。
objc_msgSendSuper2()
方法的具体实现在汇编文件 objc-msg-arm64.s 里:
也可以通过选择 Product -> Perform Action -> Assemble “Student.m” 将 OC 代码转成汇编代码,搜索 “:21”(第21行)找到具体的代码实现:
右边的注释代表的是 Student.m 第21行(:21)。在这里也可以看到 [super run]
调用的是 _objc_msgSendSuper2
方法。
查看 LLVM 的中间代码(IR)
Objective-C 在变为机器代码之前,会被 LLVM 编译器转换为中间代码(Intermediate Representation)。
语法 | 简介 |
---|---|
@ | 全局变量 |
% | 局部变量 |
alloca | 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存 |
i32 | 32位4字节的整数 |
align | 对齐 |
load | 读出 |
store | 写入 |
icmp | 两个整数值比较,返回布尔值 |
br | 选择分支,根据条件来转向 label,不根据条件跳转的话类似 goto |
label | 代码标签 |
call | 调用函数 |
|
|
使用以下命令行指令生成中间代码 Person.ll:
找到对应的方法实现:
小结
通过终端命令行生成的 c++ 代码中 [super run]
的底层实现是 objc_msgSendSuper()
方法,通过查看汇编代码和 LLVM 的中间代码可以看到 [super run]
的底层实现是 objc_msgSendSuper2()
方法。这个问题还是之前提到过的,通过终端命令生成的编译文件很运行时生成的编译文件相比,在某些细节的地方还是有区别的。不过并不影响理解具体的实现逻辑。
self、class 和 superclass 的底层实现
self、class 和 superclass 的底层实现在源码 objc4-781 的 NSObject.mm 文件。
[super class]
设置的消息接收者是 self(student 实例对象),并从 Person 类对象开始查找对象方法 -(Class)class
。在 Person 类对象里没有找到,通过 superclass 指针找到父类的类对象 NSObject,在 NSObject 类对象里找到了对象方法 -(Class)class
后交给消息接收者处理。因为是由消息接收者处理对象方法 -(Class)class
,所以对象方法 - (Class)class
的参数 self 自然就是消息接收者本身了。所以 [super class]
最终的返回值就是 Student([student class]
)。
[super superclass]
设置的消息接收者是 self(student 实例对象),并从 Person 类对象开始查找对象方法 -(Class)superclass
。在 Person 类对象里没有找到,通过 superclass 指针找到父类的类对象 NSObject,在 NSObject 类对象里找到了对象方法 -(Class)superclass
后交给消息接收者处理。因为是由消息接收者处理对象方法 -(Class)superclass
,所以对象方法 - (Class)superclass
的参数 self 自然就是消息接收者本身了。所以 [super superclass]
的返回值就是 Person([student superclass]
)。
现在再看这四个方法,就很清晰了:
isKindOfClass: 和 isMemberOfClass:
isKindOfClass:
和 isMemberOfClass:
的底层实现在源码 objc4-781 的 NSObject.mm 文件。
-isMemberOfClass:
:获取 self 的类对象与传入的 cls 进行比较。+isMemberOfClass:
:因为自身是类方法,所以这里是拿 self->ISA()(元类)作为 tcls 与传入的 cls 进行比较。+isKindOfClass:
:方法内部是一个 for 循环,因为自身是类方法,所以这里是拿 self->ISA()(元类)与传入的 cls 进行比较。如果不相等再遍历 tcls 的 superclass 与传入的 cls 进行比较。遍历过程中有一个相等就结束遍历返回 YES,遍历结束后没有找到相等的类就返回 NO。-isKindOfClass:
:方法内部是一个 for 循环,先获取到 self 的类对象 tcls 与传入的 cls 进行比较。如果不相等再遍历 tcls 的 superclass 与传入的 cls 进行比较。遍历过程中有一个相等就结束遍历返回 YES,遍历结束后没有找到相等的类就返回 NO。
+isMemberOfClass: 和 +isKindOfClass:
|
|
打印结果:
打印结果解析:+isKindOfClass:
方法,先通过 self->ISA() 找到 NSObject 元类对象,不等于 NSObject 类对象。再通过 NSObject 元类对象的 superclass 指针找到 NSObject 类对象,等于 NSObject 类对象。所以结果等于 YES:
+isMemberOfClass:
方法,通过 self->ISA() 找到 NSObject 元类对象,不等于 NSObject 类对象。所以结果等于 NO:
+isKindOfClass:
方法,先通过 self->ISA() 找到 Person 元类对象,不等于 Person 类对象。再通过 Person 元类对象的 superclass 指针找到 NSObject 元类对象,不等于 Person 类对象。再通过 NSObject 元类对象的 superclass 指针找到 NSObject 类对象,不等于 Person 类对象。所以结果等于 NO:
+isMemberOfClass:
方法,通过 self->ISA() 找到 Person 元类对象,不等于 Person 类对象。所以结果等于 NO:
小结
因为当类对象调用类方法 +isMemberOfClass:
和 +isKindOfClass:
时,是拿类对象的元类对象跟传入的对象做对比,所以除 NSObject 外,其它类对象调用这两个类方法时,右边传入的至少应该是元类对象才有意义。
-isMemberOfClass: 和 -isKindOfClass:
|
|
打印结果:
-isKindOfClass:
方法,通过 [self class] 找到 NSObject 类对象,所以结果等于 YES:
-isMemberOfClass:
方法,通过 [self class] 找到 NSObject 类对象,所以结果等于 YES:
-isKindOfClass:
方法,通过 [self class] 找到 Person 类对象,所以结果等于 YES:
-isMemberOfClass:
方法,通过 [self class] 找到 Person 类对象,所以结果等于 YES:
小结
因为当实例对象调用对象方法 -isMemberOfClass:
和 -isKindOfClass:
时,是拿实例对象的类对象跟传入的对象做对比,所以在实例对象调用这两个对象方法时,右边传入的至少应该是类对象才有意义。
对象方法的调用原理
正常调用
打印结果:
指针 person 存储着 person 实例对象的地址(person 实例对象的 isa 地址),而 person 实例对象的 isa 指针里存储着 Person 类对象的地址(Person 类对象的 isa 地址)。[person run]
是通过 person 实例对象的 isa 指针找到 Person 类对象查找 -(void)run
方法,-(void)run
方法内部的 self 就是消息接收者(person 实例对象)。person 实例对象内部存储着 isa 指针和成员变量,self->_name
是从 isa 的地址开始在 person 实例对象的内存里向下查找成员变量 _name。
自定义调用
打印结果:
思考:
[(__bridge id)obj run]
为什么能够调用成功?- 为什么 self.name 变成了 ViewController?
局部变量的内存分配在栈空间,从高地址到低地址:
打印结果:
从打印结果可以看到 a、b、c、d 的内存分配顺序是从高地址到低地址。
[(__bridge id)obj run]
图解:
图中可以看到 obj、cls 和 self 的内存都分配在栈空间,[UIViewController class]
在全局区,self 的地址值最大,obj 的地址值最小。self 和 [UIViewController class]
来自 [super viewDidLoad]
的结构体 __rw_objc_super
,__rw_objc_super
也是一个临时变量:
这一点可以通过打印内存进行验证(x/4g
:打印4个数据,每个数据8个字节):
从打印结果可以看到,依次是 Person 类对象、
注释掉 [super viewDidLoad]
就会报坏内存访问的错误:
修改 ViewController.m 实现,添加成员变量 test:
打印结果:
指针 obj 存储着 cls 的地址,而 cls 存储着 Person 类对象的地址(Person 类对象的 isa 地址)。[(__bridge id)obj run]
是通过 cls 找到 Person 类对象查找 -(void)run
方法,-(void)run
方法内部的 self 就是消息接收者 obj。从 obj 的内存开始在 obj 所在的内存中向下查找成员变量 _name,最后找到的却是 cls 下面的局部变量 test:
从图中可以看到 obj、cls 和 test 三个变量的内存都分配在栈空间,test 的地址值最大,obj 的地址值最小。
[(__bridge id)obj run]
为什么能够调用成功?
因为指针 obj 存储着 cls 的地址,而 cls 存储着 Person 类对象的地址(Person 类对象的 isa 地址),所以[(__bridge id)obj run]
是通过 cls 找到 Person 类对象查找-(void)run
方法(这里的 cls 相当于 person 实例对象的 isa)。- 为什么 self.name 变成了 ViewController?
因为-(void)run
方法内部的 self 就是消息接收者 obj,obj->_name
是在 obj 所在的内存中从 obj 的地址开始向下查找成员变量 _name,而 obj 所在的内存(栈区)向下找到的是 cls 下面的指针 self(ViewController 实例对象),所以最终的打印结果是<ViewController: 0x7fd88fb0a400>
。
Runtime API
类
方法 | 注释 |
---|---|
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) |
动态创建一个类(参数:父类,类名,额外的内存空间) |
void objc_registerClassPair(Class cls) |
注册一个类(要在类注册之前添加成员变量) |
void objc_disposeClassPair(Class cls) |
销毁一个类 |
Class object_getClass(id obj) |
获取 isa 指向的 Class |
Class object_setClass(id obj, Class cls) |
设置 isa 指向的 Class |
BOOL object_isClass(id obj) |
判断一个 OC 对象是否为 Class |
BOOL class_isMetaClass(Class cls) |
判断一个 Class 是否为元类 |
Class class_getSuperclass(Class cls) |
获取父类 |
|
|
打印结果:
成员变量
方法 | 注释 |
---|---|
Ivar class_getInstanceVariable(Class cls, const char *name) |
获取一个实例变量信息 |
Ivar *class_copyIvarList(Class cls, unsigned int *outCount) |
拷贝实例变量列表(最后需要调用free释放) |
void object_setIvar(id obj, Ivar ivar, id value) |
设置成员变量的值 |
id object_getIvar(id obj, Ivar ivar) |
获取成员变量的值 |
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types) |
动态添加成员变量(已经注册的类是不能动态添加成员变量的) |
const char *ivar_getName(Ivar v) |
获取成员变量的名字 |
const char *ivar_getTypeEncoding(Ivar v) |
获取成员变量的类型编码 |
|
|
在使用 object_setIvar()
方法设置 int 类型的成员变量时,因为数值不能直接转为对象,所以先将数值转成指针(指针是用来存值的),再将指针转成 id 类型的对象。
打印结果:
属性
方法 | 注释 |
---|---|
objc_property_t class_getProperty(Class cls, const char *name) |
获取一个属性 |
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) |
拷贝属性列表 |
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount) |
动态添加属性 |
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount) |
动态替换属性 |
const char *property_getName(objc_property_t property) |
获取属性名 |
const char *property_getAttributes(objc_property_t property) |
获取属性的真实类型 |
test3()
实现:
打印结果:
T 后面是该属性的数据类型。V 后面是该属性的变量名称。N 是属性的非原子属性 nonatomic 的标识。C 是属性的 copy 标识。
关于 property_getAttributes()
获取到的结果,可以参考 Declared Properties。
方法
方法 | 注释 |
---|---|
Method class_getInstanceMethod(Class cls, SEL name) |
获得一个实例方法 |
Method class_getClassMethod(Class cls, SEL name) |
获得一个类方法 |
IMP class_getMethodImplementation(Class cls, SEL name) |
根据 Class 和 SEL 获取方法的 imp 指针 |
IMP method_setImplementation(Method m, IMP imp) |
修改方法的 imp 指针 |
void method_exchangeImplementations(Method m1, Method m2) |
交换方法的 imp 指针 |
Method *class_copyMethodList(Class cls, unsigned int *outCount) |
拷贝方法列表(最后需要调用free释放) |
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) |
动态添加方法 |
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) |
动态替换方法 |
SEL method_getName(Method m) |
获取方法名 |
IMP method_getImplementation(Method m) |
根据 Method 获取方法的 imp 指针 |
const char *method_getTypeEncoding(Method m) |
获取方法的类型编码 |
unsigned int method_getNumberOfArguments(Method m) |
根据 Method 获取方法的参数个数 |
char *method_copyReturnType(Method m) |
根据 Method 获取方法的返回值的类型编码(带有copy的需要调用free去释放) |
char *method_copyArgumentType(Method m, unsigned int index) |
根据 Method 获取方法的索引位置的参数的类型编码(带有copy的需要调用free去释放) |
const char *sel_getName(SEL sel) |
根据 SEL 获取方法的名称 |
SEL sel_registerName(const char *str) |
根据方法名注册 SEL |
IMP imp_implementationWithBlock(id block) |
用 block 作为方法实现 |
id imp_getBlock(IMP anImp) |
根据 block 的 IMP 生成 block 对象 |
BOOL imp_removeBlock(IMP anImp) |
移除 imp_implementationWithBlock() 生成的 IMP |
|
|
打印结果:
拓展
class_rw_ext_t
class_rw_ext_t
里保存着 class_rw_t
的方法列表、属性列表、协议列表和 class_ro_t
等信息。
动态添加的属性存到哪了?
查看 class_addProperty()
的实现
查看 class_addProperty()
的 Runtime 实现可以看到,class_addProperty()
方法内部调用的是 _class_addProperty()
方法。_class_addProperty()
方法内部先判断了方法是否已经存在,又判断了是否要替换。如果即不存在也不替换,就保存到 class_rw_ext_t
的 properties 里,即类对象的属性列表里。(class_rw_ext_t:class_read_write_extension_table,即 class_rw_t 的拓展表)。
动态添加的方法存到哪了?
查看 class_addMethod()
的实现
查看 class_addMethod()
的 Runtime 实现可以看到 class_addMethod()
方法内部调用的是 addMethod()
方法。addMethod()
方法内部先判断了方法是否已经存在,又判断了是否要替换。如果即不存,就保存到 class_rw_ext_t
的 methods 里,即类对象的方法列表里。(class_rw_ext_t:class_read_write_extension_table,即 class_rw_t 的拓展表)。
method_exchangeImplementations 的实现原理
|
|
method_exchangeImplementations 的底层实现是交换了两个方法的 imp
指针,然后调用 flushCaches()
方法清空了方法缓存。
应用
查看私有成员变量
修改 UITextField 占位文字的颜色:
方案一:正常的 OC 代码
方案二:使用 KVC 修改(iOS 13 后禁用)
报错:
方案三:使用 Runtime(还不如 OC 省事)
字典转模型
|
|
打印结果:
归档解档
|
|
打印结果:
替换方法实现
class_replaceMethod
打印结果:
拦截所有按钮的点击事件
使用 method_exchangeImplementations 交换系统方法实现方法拦截:
打印结果:
拦截数组添加数据方法
数组的 addObject:
和 insertObject:
方法调用的都是 insertObject:atIndex:
方法。拦截 insertObject:atIndex:
方法,可以解决添加空数据导致的崩溃。
类簇:NSData、NSArray、NSDictionary 和 NSString。它们的类并不一样,比如 NSMutableArray 的类是 __NSArrayM
。
使用 method_exchangeImplementations 交换系统方法实现方法拦截:
拦截字典添加数据方法
字典赋值的 setObject:forKey:
方法最终调用的是 setObject:forKeyedSubscript:
。拦截 setObject:forKeyedSubscript:
方法,可以解决添加空数据导致的崩溃。
使用 method_exchangeImplementations 交换系统方法实现方法拦截:
总结
讲一下 OC 的消息机制
OC 中的方法调用其实都是转成了 objc_msgSend 函数的调用,给 receiver(方法调用者)发送了一条消息(selector(方法名))。
objc_msgSend 底层有三大阶段:- 消息发送:先调在当前类的 cache 里找,再到当前类的 methods 里找。如果在当前类没有找到,再遍历父类查找,先在父类的 cache 里找,再到父类的 methods 里找。
- 动态方法解析:在当前类及其父类里没有找到方法时,会调用
resolveInstanceMethod:
或者resolveClassMethod:
方法动态添加方法。 - 消息转发:如果没有动态添加方法,会调用
forwardingTargetForSelector:
方法获取可以处理消息的对象。如果没有实现forwardingTargetForSelector:
方法或者该方法返回的是 nil,会调用methodSignatureForSelector:
方法获取类型编码,在获取类型编码成功后再调用forwardInvocation:
方法进行自定义操作。如果没有实现methodSignatureForSelector:
方法或者该方法返回的是 nil,会调用doesNotRecognizeSelector:
方法终止流程。
消息转发机制流程
如果没有动态添加方法,会调用forwardingTargetForSelector:
方法获取可以处理消息的对象。如果没有实现forwardingTargetForSelector:
方法或者该方法返回的是 nil,会调用methodSignatureForSelector:
方法获取类型编码,在获取类型编码成功后再调用forwardInvocation:
方法进行自定义操作。如果没有实现methodSignatureForSelector:
方法或者该方法返回的是 nil,会调用doesNotRecognizeSelector:
方法终止流程。什么是 Runtime ?平时项目中有用过么?
OC 是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。OC 的动态性就是由 Runtime 来支撑和实现的,Runtime 是一套 C 语言的 API,封装了很多动态性相关的函数,平时编写的OC代码,底层都是转换成了 Runtime API 进行调用。具体应用
1、利用关联对象(AssociatedObject)给分类添加属性;
2、遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档);
3、交换方法实现(交换系统的方法);
4、利用消息转发机制解决方法找不到的异常问题;
……