iOS大解密:玄之又玄的KVO (上)
导读:
大多数 iOS 开发人员对 KVO 的认识只局限于 isa 指针交换这一层,而 KVO 的实现细节却鲜为人知。
如果自己也仿照 KVO 基础原理来实现一套类 KVO 操作且独立运行时会发现一切正常,然而一旦你的实现和系统的 KVO 实现同时作用在同一个实例上那么各种各样诡异的 bug 和 crash 就会层出不穷。
这究竟是为什么呢?此类问题到底该如何解决呢?接下来我们将尝试从汇编层面来入手以层层揭开 KVO 的神秘面纱......
1. 缘起 Aspects
SDMagicHook 开源之后很多小伙伴在问“ SDMagicHook 和 Aspects 的区别是什么?”,我在 GitHub 上找到 Aspects 了解之后发现 Aspects 也是以 isa 交换为基础原理进行的 hook 操作,但是两者在具体实现和 API 设计上也有一些区别,另外 SDMagicHook 还解决了 Aspects 未能解决的 KVO 冲突难题。
1.1 SDMagicHook 的 API 设计更加友好灵活
SDMagicHook 和 Aspects 的具体异同分析见:https://github.com/larksuite/SDMagicHook/issues/3。
1.2 SDMagicHook 解决了 Aspects 未能解决的 KVO 冲突难题
在 Aspects 的 readme 中我还注意到了这样一条关于 KVO 兼容问题的描述:
SDMagicHook 会不会有同样的问题呢?测试了一下发现 SDMagicHook 果然也中招了,而且其实此类问题的实际情况要比 Aspects 作者描述的更为复杂和诡异,问题的具体表现会随着系统 KVO(以下简称 native-KVO)和自己实现的类 KVO(custom-KVO)的调用顺序和次数的不同而各异,具体如下:
- 先调用 custom-KVO 再调用 native-KVO,native-KVO 和 custom-KVO 都运行正常
- 先调用 native-KVO 再调用 custom-KVO,custom-KVO 运行正常,native-KVO 会 crash
- 先调用 native-KVO 再调用 custom-KVO 再调用 native-KVO,native-KVO 运行正常,custom-KVO 失效,无 crash
目前,SDMagicHook 已经解决了上面提到的各类问题,具体的实现方案我将在下文中详细介绍。
2. 从汇编层面探索 KVO 本质
想要弄明白这个问题首先需要研究清楚系统的 KVO 到底是如何实现的,而系统的 KVO 实现又相当复杂,我们该从哪里入手呢?想要弄清楚这个问题,我们首先需要了解下当对被 KVO 观察的目标属性进行赋值操作时到底发生了什么。这里我们以自建的 Test 类为例来说明,我们对 Test 类实例的 num 属性进行 KVO 操作:
当我们给 num 赋值时,可以看到断点命中了 KVO 类自定义的 setNum:的实现即_NSSetIntValueAndNotify 函数
那么_NSSetIntValueAndNotify 的内部实现是怎样的呢?我们可以从汇编代码中发现一些蛛丝马迹:
Foundation`_NSSetIntValueAndNotify:
0x10e5b0fc2 <+0>: pushq %rbp
-> 0x10e5b0fc3 <+1>: movq %rsp, %rbp
0x10e5b0fc6 <+4>: pushq %r15
0x10e5b0fc8 <+6>: pushq %r14
0x10e5b0fca <+8>: pushq %r13
0x10e5b0fcc <+10>: pushq %r12
0x10e5b0fce <+12>: pushq %rbx
0x10e5b0fcf <+13>: subq $0x48, %rsp
0x10e5b0fd3 <+17>: movl %edx, -0x2c(%rbp)
0x10e5b0fd6 <+20>: movq %rsi, %r15
0x10e5b0fd9 <+23>: movq %rdi, %r13
0x10e5b0fdc <+26>: callq 0x10e7cc882 ; symbol stub for: object_getClass
0x10e5b0fe1 <+31>: movq %rax, %rdi
0x10e5b0fe4 <+34>: callq 0x10e7cc88e ; symbol stub for: object_getIndexedIvars
0x10e5b0fe9 <+39>: movq %rax, %rbx
0x10e5b0fec <+42>: leaq 0x20(%rbx), %r14
0x10e5b0ff0 <+46>: movq %r14, %rdi
0x10e5b0ff3 <+49>: callq 0x10e7cca26 ; symbol stub for: pthread_mutex_lock
0x10e5b0ff8 <+54>: movq 0x18(%rbx), %rdi
0x10e5b0ffc <+58>: movq %r15, %rsi
0x10e5b0fff <+61>: callq 0x10e7cb472 ; symbol stub for: CFDictionaryGetValue
0x10e5b1004 <+66>: movq 0x36329d(%rip), %rsi ; "copyWithZone:"
0x10e5b100b <+73>: xorl %edx, %edx
0x10e5b100d <+75>: movq %rax, %rdi
0x10e5b1010 <+78>: callq *0x2b2862(%rip) ; (void *)0x000000010eb89d80: objc_msgSend
0x10e5b1016 <+84>: movq %rax, %r12
0x10e5b1019 <+87>: movq %r14, %rdi
0x10e5b101c <+90>: callq 0x10e7cca32 ; symbol stub for: pthread_mutex_unlock
0x10e5b1021 <+95>: cmpb $0x0, 0x60(%rbx)
0x10e5b1025 <+99>: je 0x10e5b1066 ; <+164>
0x10e5b1027 <+101>: movq 0x36439a(%rip), %rsi ; "willChangeValueForKey:"
0x10e5b102e <+108>: movq 0x2b2843(%rip), %r14 ; (void *)0x000000010eb89d80: objc_msgSend
0x10e5b1035 <+115>: movq %r13, %rdi
0x10e5b1038 <+118>: movq %r12, %rdx
0x10e5b103b <+121>: callq *%r14
0x10e5b103e <+124>: movq (%rbx), %rdi
0x10e5b1041 <+127>: movq %r15, %rsi
0x10e5b1044 <+130>: callq 0x10e7cc2b2 ; symbol stub for: class_getMethodImplementation
0x10e5b1049 <+135>: movq %r13, %rdi
0x10e5b104c <+138>: movq %r15, %rsi
0x10e5b104f <+141>: movl -0x2c(%rbp), %edx
0x10e5b1052 <+144>: callq *%rax
0x10e5b1054 <+146>: movq 0x364385(%rip), %rsi ; "didChangeValueForKey:"
0x10e5b105b <+153>: movq %r13, %rdi
0x10e5b105e <+156>: movq %r12, %rdx
0x10e5b1061 <+159>: callq *%r14
0x10e5b1064 <+162>: jmp 0x10e5b10be ; <+252>
0x10e5b1066 <+164>: movq 0x2b22eb(%rip), %rax ; (void *)0x00000001120b9070: _NSConcreteStackBlock
0x10e5b106d <+171>: leaq -0x68(%rbp), %r9
0x10e5b1071 <+175>: movq %rax, (%r9)
0x10e5b1074 <+178>: movl $0xc2000000, %eax ; imm = 0xC2000000
0x10e5b1079 <+183>: movq %rax, 0x8(%r9)
0x10e5b107d <+187>: leaq 0xf5d(%rip), %rax ; ___NSSetIntValueAndNotify_block_invoke
0x10e5b1084 <+194>: movq %rax, 0x10(%r9)
0x10e5b1088 <+198>: leaq 0x2b7929(%rip), %rax ; __block_descriptor_tmp.77
0x10e5b108f <+205>: movq %rax, 0x18(%r9)
0x10e5b1093 <+209>: movq %rbx, 0x28(%r9)
0x10e5b1097 <+213>: movq %r15, 0x30(%r9)
0x10e5b109b <+217>: movq %r13, 0x20(%r9)
0x10e5b109f <+221>: movl -0x2c(%rbp), %eax
0x10e5b10a2 <+224>: movl %eax, 0x38(%r9)
0x10e5b10a6 <+228>: movq 0x364fab(%rip), %rsi ; "_changeValueForKey:key:key:usingBlock:"
0x10e5b10ad <+235>: xorl %ecx, %ecx
0x10e5b10af <+237>: xorl %r8d, %r8d
0x10e5b10b2 <+240>: movq %r13, %rdi
0x10e5b10b5 <+243>: movq %r12, %rdx
0x10e5b10b8 <+246>: callq *0x2b27ba(%rip) ; (void *)0x000000010eb89d80: objc_msgSend
0x10e5b10be <+252>: movq 0x362f73(%rip), %rsi ; "release"
0x10e5b10c5 <+259>: movq %r12, %rdi
0x10e5b10c8 <+262>: callq *0x2b27aa(%rip) ; (void *)0x000000010eb89d80: objc_msgSend
0x10e5b10ce <+268>: addq $0x48, %rsp
0x10e5b10d2 <+272>: popq %rbx
0x10e5b10d3 <+273>: popq %r12
0x10e5b10d5 <+275>: popq %r13
0x10e5b10d7 <+277>: popq %r14
0x10e5b10d9 <+279>: popq %r15
0x10e5b10db <+281>: popq %rbp
0x10e5b10dc <+282>: retq
上面这段汇编代码翻译为伪代码大致如下:
typedef struct {
Class originalClass; // offset 0x0
Class KVOClass; // offset 0x8
CFMutableSetRef mset; // offset 0x10
CFMutableDictionaryRef mdict; // offset 0x18
pthread_mutex_t *lock; // offset 0x20
void *sth1; // offset 0x28
void *sth2; // offset 0x30
void *sth3; // offset 0x38
void *sth4; // offset 0x40
void *sth5; // offset 0x48
void *sth6; // offset 0x50
void *sth7; // offset 0x58
bool flag; // offset 0x60
} SDTestKVOClassIndexedIvars;
typedef struct {
Class isa; // offset 0x0
int flags; // offset 0x8
int reserved;
IMP invoke; // offset 0x10
void *descriptor; // offset 0x18
void *captureVar1; // offset 0x20
void *captureVar2; // offset 0x28
void *captureVar3; // offset 0x30
int captureVar4; // offset 0x38
} SDTestStackBlock;
void _NSSetIntValueAndNotify(id obj, SEL sel, int number) {
Class cls = object_getClass(obj);
// 获取类实例关联的信息
SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(cls);
pthread_mutex_lock(indexedIvars->lock);
NSString *str = (NSString *)CFDictionaryGetValue(indexedIvars->mdict, sel);
str = [str copyWithZone:nil];
pthread_mutex_unlock(indexedIvars->lock);
if (indexedIvars->flag) {
[obj willChangeValueForKey:str];
((void(*)(id obj, SEL sel, int number))class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number);
[obj didChangeValueForKey:str];
} else {
// 生成block
SDTestStackBlock block = {};
block.isa = _NSConcreteStackBlock;
block.flags = 0xC2000000;
block.invoke = ___NSSetIntValueAndNotify_block_invoke;
block.descriptor = __block_descriptor_tmp;
block.captureVar2 = indexedIvars;
block.captureVar3 = sel;
block.captureVar1 = obj;
block.captureVar4 = number;
[obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock];
}
}
这段代码的大致意思是说首先通过 object_getIndexedIvars(cls)获取到 KVO 类的 indexedIvars,如果 indexedIvars->flag 为 true 即开发者自己重写实现过 willChangeValueForKey:或者 didChangeValueForKey:方法的话就直接以 class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number)的方式实现对被观察的原方法的调用,否则就用默认实现为 NSSetIntValueAndNotify_block_invoke 的栈 block 并捕获 indexedIvars、被 KVO 观察的实例、被观察属性对应的 SEL、赋值参数等所有必要参数并将这个 block 作为参数传递给 [obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock]调用。看到这里你或许会有个疑问:伪代码中通过 object_getIndexedIvars(cls)获取到的 indexedIvars 是什么信息呢?block.invoke = ___ NSSetIntValueAndNotify_block_invoke 又是如何实现的呢?
篇幅过长 分上下2篇