iOS开发KVO实现细节解密

2022-08-08 15:12:49

目录导读1.缘起Aspects1.1SDMagicHook的API设计更加友好灵活1.2SDMagicHook解决了Aspects未能解决的KVO冲突难题2.从汇编层面探索...

目录
导读
1. 缘起 ASPects
1.1 SDMagicHook 的 API 设计更加友好灵活
1.2 SDMagicHook 解决了 Aspects 未能解决的 KVO 冲突难题
2. 从汇编层面探索 KVO 本质
3. 如何解决 custom-KVO 导致的 native-KVO Crash
4. 如何解决 native-KVO 导致 custom-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 兼容问题的描述:

iOS开发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 操作:

iOS开发KVO实现细节解密

当我们给 num 赋值时,可以看到断点命中了 KVO 类自定义的 setNum:的实现即_NSSetIntValueAndNotify 函数

iOS开发KVO实现细节解密

那么_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>:callq0x10e7cc882;symbolstubfor:object_getClass
0x10e5b0fe1<+31>:movq%rax,%rdi
0x10e5b0fe4<+34>:callq0x10e7cc88e;symbolstubfor:object_getIndexedIvars
0x10e5b0fe9<+39>:movq%rax,%rbx
0x10e5b0fec<+42>:leaq0x20(%rbx),%r14
0x10e5b0ff0<+46>:movq%r14,%rdi
0x10e5b0ff3<+49>:callq0x10e7cca26;symbolstubfor:pthread_mutex_lock
0x10e5b0ff8<+54>:movq0x18(%rbx),%rdi
0x10e5b0ffc<+58>:movq%r15,%rsi
0x10e5b0fff<+61>:callq0x10e7cb472;symbolstubfor:CFDictionaryGetValue
0x10e5b1004<+66>:movq0x36329d(%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>:callq0x10e7cca32;symbolstubfor:pthread_mutex_unlock
0x10e5b1021<+95>:cmpb$0x0,0x60(%rbx)
0x10e5b1025<+99>:je0x10e5b1066;<+164>
0x10e5b1027<+101>:movq0x36439a(%rip),%rsi;"willChangeValueForKey:"
0x10e5b102e<+108>:movq0x2b2843(%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>:callq0x10e7cc2b2;symbolstubfor:class_getMethodImplementation
0x10e5b1049<+135>:movq%r13,%rdi
0x10e5b104c<+138>:movq%r15,%rsi
0x10e5b104f<+141>:movl-0x2c(%rbp),%edx
0x10e5b1052<+144>:callq*%rax
0x10e5b1054<+146>:movq0x364385(%rip),%rsi;"didChangeValueForKey:"
0x10e5b105b<+153>:movq%r13,%rdi
0x10e5b105e<+156>:movq%r12,%rdx
0x10e5b1061<+159>:callq*%r14
0x10e5b1064<+162>:jmp0x10etByHoaKrwK5b10be;<+252>
0x10e5b1066<+164>:movq0x2b22eb(%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>:leaq0xf5d(%rip),%rax;___NSSetIntValueAndNotify_block_invoke
0x10e5b1084<+194>:movq%rax,0x10(%r9)
0x10e5b1088<+198>:leaq0x2b7929(%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>:movq0x364fab(%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>:movq0x362f73(%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

上面这段汇编代码翻译为伪代码大致如下:

typedefstruct{
ClassoriginalClass;//offset0x0
ClassKVOClass;//offset0x8
CFMutableSetRefmset;//offset0x10
CFMutableDictionaryRefmdict;//offset0x18
pthread_mutex_t*lock;//offset0x20
void*sth1;//offset0x28
void*sth2;//offset0x30
void*sth3;//offset0x38
void*sth4;//offset0x40
void*sth5;//offset0x48
void*sth6;//offset0x50
void*sth7;//offset0x58
boolflag;//offset0x60
}SDTestKVOClassIndexedIvars;
typedefstruct{
Classisa;//offset0x0
intflags;//offset0x8
intreserved;
IMPinvoke;//offset0x10
void*descriptor;//offset0x18
void*captureVar1;//offset0pythonx20
void*captureVar2;//offset0x28
void*captureVarandroid3;//offset0x30
intcaptureVar4;//offset0x38
}SDTestStackBlock;
void_NSSetIntValueAndNotify(idobj,SELsel,intnumber){
Classcls=object_getClass(obj);
//获取类实例关联的信息
SDTestKVOClassIndexedIvars*indexedIvars=object_getIndexedIvars(cls);
pthread_mutex_lock(indexedIvars->lock);
NSString*str=(NSString*)CFDictionaryGetValue(indexedIvars->mdict,sel);
str=[strcopyWithZone:nil];
pthread_mutex_unlock(indexedIvars->lock);
if(indexedIvars->flag){
[objwillChangeValueForKey:str];
((void(*)(idobj,SELsel,intnumber))class_getMethodImplementation(indexedIvars->originalClass,sel))(obj,sel,number);
[objdidChangeValueForKey:str];
}else{
//生成block
SDTestStackBlockblock={};
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:strkey:nilkey:nilusingBlock:&SDTestStackBlock];
}
}

这段代码的大致意思是说首先通过 object_getIndexedIvars(cls)获取到 KVO 类的 indexedIvars,如果 indexedIvars->flag 为 true 即开发者自己重写实现过 willChangeValueForKey:

或者 didChangeValueForKey:方法的话就直接以 class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number)的方http://www.cppcns.com式实现对被观察的原方法的调用,否则就用默认实现为 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 又是如何实现的呢?

首先我们看下 NSSetIntValueAndNotify_block_invoke 的汇编实现:

Foundation`___NSSetIntValueAndNotify_block_invoke:
->0x10bf27fe1<+0>:pushq%rbp
0x10bf27fe2<+1>:movq%rsp,%rbp
0x10bf27fe5<+4>:pushq%rbx
0x10bf27fe6<+5>:pushq%rax
0x10bf27fe7<+6>:movq%rdi,%rbx
0x10bf27fea<+9>:movq0x28(%rbx),%rax
0x10bf27fee<+13>:movq0x30(%rbx),%rsi
0x10bf27ff2<+17>:movq(%rax),%rdi
0x10bf27ff5<+20>:callq0x10c1422b2;symbolstubfor:class_getMethodImplementation
0x10bf27ffa<+25>:movq0x20(%rbx),%rdi
0x10bf27ffe<+29>:movq0x30(%rbx),%rsi
0x10bf28002<+33>:movl0x38(%rbx),%edx
0x10bf28005<+36>:addq$0x8,%rsp
0x10bf28009<+40>:popq%rbx
0x10bf2800a<+41>:popq%rbp
0x10bf2800b<+42>:jmpq*%rax

___NSSetIntValueAndNotify_block_invoke 翻译成伪代码如下:

void___NSSetIntValueAndNotify_block_invoke(SDTestStackBlock*block){
SDTestKVOClassIndexedIvars*indexedIvars=block->captureVar2;
SELmethodSel=block->captureVar3;
IMPimp=class_getMethodImplementation(indexedIvars->originalClass);
idobj=block->captureVar1;
SELsel=block->captureVar3;
intnum=block->captureVar4;
imp(obj,sel,num);
}

这个 block 的内部实现其实就是从 KVO 类的 indexedIvars 里取到原始类,然后根据 sel 从原始类中取出原始的方法实现来执行并最终完成了一次 KVO 调用。我们发现整个 KVO 运作过程中 KVO 类的 indexedIvars 是一个贯穿 KVO 流程始末的关键数据,那么这个 indexedIvars 是何时生成的呢?

indexedIvars 里又包含哪些数据呢?想要弄清楚这个问题,我们就必须从 KVO 的源头看起,我们知道既然 KVO 要用到 isa 交换那么最终肯定要调用到 object_setClass 方法,这里我们不妨以 object_setClass 函数为线索,通过设置条件符号断点来追踪 object_setClass 的调用,lldb 调试截图如下:

iOS开发KVO实现细节解密

断点到 object_setClass 之后,我们再验证看下寄存器 rdi、rsi 里面的参数打印出来分别是

<Test: 0x600003df01b0>、NSKVONotifying_Test

iOS开发KVO实现细节解密

不错,我们现在已经成功定位到 KVO 的 isa 交换现场了,然而为了找到 KVO 类的生成的地方我们还需要沿着调用栈向前回溯,最终我们定位到 KVO 类的生成函数_NSKVONotifyingCreateInfoWithOriginalClass

其汇编代码如下:

Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:
->0x10c557d79<+0>:pushq%rbp
0x10c557d7a<+1>:movq%rsp,%rbp
0x10c557d7d<+4>:pushq%r15
0x10c557d7f<+6>:pushq%r14
0x10c557d81<+8>:pushq%r12
0x10c557d83<+10>:pushq%rbx
0x10c557d84<+11>:subq$0x20,%rsp
0x10c557d88<+15>:movq%rdi,%r14
0x10c557d8b<+18>:movq0x2b463e(%rip),%rax;(void*)0x000000011012d070:__stack_chk_guard
0x10c557d92<+25>:movq(%rax),%rax
0x10c557d95<+28>:movq%rax,-0x28(%rbp)
0x10c557d99<+32>:xorl%eax,%eax
0x10c557d9b<+34>:callq0x10c55b452;NSKeyValueObservingAssertRegistrationLockHeld
0x10c557da0<+39>:movq%r14,%rdi
0x10c557da3<+42>:callq0x10c7752b8;symbolstubfor:class_getName
0x10c557da8<+47>:movq%rax,%r12
0x10c557dab<+50>:movq%r12,%rdi
0x10c557dae<+53>:callq0x10c775ba0;symbolstubfor:strlen
0x10c557db3<+58>:movq%rax,%rbx
0x10c557db6<+61>:addq$0x10,%rbx
0x10c557dba<+65>:movq%rbx,%rdi
0x10c557dbd<+68>:callq0x10c775666;symbolstubfor:malloc
0x10c557dc2<+73>:movq%rax,%r15
0x10c557dc5<+76>:leaq0x29d604(%rip),%rsi;_NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix
0x10c557dcc<+83>:movq$-0x1,%rcx
0x10c557dd3<+90>:movq%r15,%rdi
0x10c557dd6<+93>:movq%rbx,%rdx
0x10c557dd9<+96>:callq0x10c77510e;symbolstubfor:__strlcpy_chk
0x10c557dde<+101>:movq$-0x1,%rcx
0x10c557de5<+108>:movq%r15,%rdi
0x10c557de8<+111>:movq%r12,%rsi
0x10c557deb<+114>:movq%rbx,%rdx
0x10c557dee<+117>:callq0x10c775108;symbolstubfor:__strlcat_chk
0x10c557df3<+122>:movl$0x68,%edx
0x10c557df8<+127>:movq%r14,%rdi
0x10c557dfb<+130>:movq%r15,%rsi
0x10c557dfe<+133>:callq0x10c775762;symbolstubfor:objc_allocateClassPair
0x10c557e03<+138>:movq%rax,%rbx
0x10c557e06<+141>:testq%rbx,%rbx
0x10c557e09<+144>:je0x10c557f17;<+414>
0x10c557e0f<+150>:movq%rbx,%rdi
0x10c557e12<+153>:callq0x10c775816;symbolstubfor:objc_registerClassPair
0x10c557e17<+158>:movq%r15,%rdi
0x10c557e1a<+161>:callq0x10c7754ec;symbolstubfor:free
0x10c557e1f<+166>:movq%rbx,%rdi
0x10c557e22<+169>:callq0x10c77588e;symbolstubfor:object_getIndexedIvars
0x10c557e27<+174>:movq%rax,%r15
0x10c557e2a<+177>:movq%r14,(%r15)
0x10c557e2d<+180>:movq%rbx,0x8(%r15)
0x10c557e31<+184>:movq0x2b4748(%rip),%rdx;(void*)0x000000010d7fd1f8:kCFCopyStringSetCallBacks
0x10c557e38<+191>:xorl%edi,%edi
0x10c557e3a<+193>:xorl%esi,%esi
0x10c557e3c<+195>:callq0x10c774778;symbolstubfor:CFSetCreateMutable
0x10c557e41<+200>:movq%rax,0x10(%r15)
0x10c557e45<+204>:movq0x2b49e4(%rip),%rcx;(void*)0x000000010d7f6bb8:kCFTypeDictionaryValueCallBacks
0x10c557e4c<+211>:xorl%edi,%edi
0x10c557e4e<+213>:xorl%esi,%esi
0x10c557e50<+215>:xorl%edx,%edx
0x10c557e52<+217>:callq0x10c774454;symbolstubfor:CFDictionaryCreateMutable
0x10c557e57<+222>:movq%rax,0x18(%r15)
0x10c557e5b<+226>:leaq-0x38(%rbp),%rbx
0x10c557e5f<+230>:movq%rbx,%rdi
0x10c557e62<+233>:callq0x10c775a3e;symbolstubfor:pthread_mutexattr_init
0x10c557e67<+238>:movl$0x2,%esi
0x10c557e6c<+243>:movq%rbx,%rdi
0x10c557e6f<+246>:callq0x10c775a44;symbolstubfor:pthread_mutexattr_settype
0x10c557e74<+251>:leaq0x20(%r15),%rdi
0x10c557e78<+255>:movq%rbx,%rsi
0x10c557e7b<+258>:callq0x10c775a20;symbolstubfor:pthread_mutex_init
0x10c557e80<+263>:movq%rbx,%rdi
0x10c557e83<+266>:callq0x10c775a38;symbolstubfor:pthread_mutexattr_destroy
0x10c557e88<+271>:cmpq$-0x1,0x3824a0(%rip);_NSKVONotifyingCreateInfoWithOriginalClass.onceToken+7
0x10c557e90<+279>:jne0x10c557fa4;<+555>
0x10c557e96<+285>:movq(%r15),%rdi
0x10c557e99<+288>:movq0x366528(%rip),%rsi;"willChangeValueForKey:"
0x10c557ea0<+295>:callq0x10c7752b2;symbolstubfor:class_getMethodImplementation
0x10c557ea5<+300>:movb$0x1,%cl
0x10c557ea7<+302>:cmpq0x38248a(%rip),%rax;_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange
0x10c557eae<+309>:jne0x10c557ec9;<+336>
0x10c557eb0<+311>:movq(%r15),%rdi
0x10c557eb3<+314>:movq0x366526(%rip),%rsi;"didChangeValueForKey:"
0x10c557eba<+321>:callq0x10c7752b2;symbolstubfor:class_getMethodImplementation
0x10c557ebf<+326>:cmpq0x38247a(%rip),%rax;_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange
0x10c557ec6<+333>:setne%cl
0x10c557ec9<+336>:movb%cl,0x60(%r15)
0x10c557ecd<+340>:movq0x36715c(%rip),%rsi;"_isKVOA"
0x10c557ed4<+347>:leaq0x1ff(%rip),%rdx;NSKVOIsAutonotifying
0x10c557edb<+354>:xorl%ecx,%ecx
0x10c557edd<+356>:movq%r15,%rdi
0x10c557ee0<+359>:callq0x10c558057;NSKVONotifyingSetMethodImplementation
0x10c557ee5<+364>:movq0x365154(%rip),%rsi;"dealloc"
0x10c557eec<+371>:leaq0x1ef(%rip),%rdx;NSKVODeallocate
0x10c557ef3<+378>:xorl%ecx,%ecx
0x10c557ef5<+380>:movq%r15,%rdi
0x10c557ef8<+383>:callq0x10c558057;NSKVONotifyingSetMethodImplementation
0x10c557efd<+388>:movq0x36519c(%rip),%rsi;"class"
0x10c557f04<+395>:leaq0x433(%rip),%rdx;NSKVOClass
0x10c557f0b<+402>:xorl%ecx,%ecx
0x10c557f0d<+404>:movq%r15,%rdi
0x10c557f10<+407>:callq0x10c558057;NSKVONotifyingSetMethodImplementation
0x10c557f15<+412>:jmp0x10c557f84;<+523>
0x10c557f17<+414>:cmpq$-0x1,0x382409(%rip);_NSKVONotifyingCreateInfoWithOriginalClass.kvoLog+7
0x10c557f1f<+422>:jne0x10c557fbc;<+579>
0x10c557f25<+428>:movq0x3823f4(%rip),%r14;_NSKVONotifyingCreateInfoWithOriginalClass.kvoLog
0x10c557f2c<+435>:movl$0x10,%esi
0x10c557f31<+440>:movq%r14,%rdi
0x10c557f34<+443>:callq0x10c7758e2;symbolstubfor:os_log_type_enabled
0x10c557f39<+448>:testb%al,%al
0x10c557f3b<+450>:je0x10c557f79;<+512>
0x10c557f3d<+452>:movq%rsp,%rbx
0x10c557f40<+455>:movq%rsp,%rax
0x10c557f43<+458>:leaq-0x10(%rax),%r8
0x10c557f47<+462>:movq%r8,%rsp
0x10c557f4a<+465>:movl$0x8200102,-0x10(%rax);imm=0x8200102
0x10c557f51<+472>:movq%r15,-0xc(%rax)
0x10c557f55<+476>:leaq-0x63f5c(%rip),%rdi
0x10c557f5c<+483>:leaq0x296c1d(%rip),%rcx;"KVOfailedtoallocateclasspairforname%s,automatickey-valueobservingwillnotworkforthisclass"
0x10c557f63<+490>:movl$0x10,%edx
0x10c557f68<+495>:movl$0xc,%r9d
0x10c557f6e<+501>:movq%r14,%rsi
0x10c557f71<+504>:callq0x10c7751aa;symbolstubfor:_os_log_error_impl
0x10c557f76<+509>:movq%rbx,%rsp
0x10c557f79<+512>:movq%r15,%rdi
0x10c557f7c<+515>:callq0x10c7754ec;symbolstubfor:free
0x10c557f81<+520>:xorl%r15d,%r15d
0x10c557f84<+523>:movq0x2b4445(%rip),%rax;(void*)0x000000011012d070:__stack_chk_guard
0x10c557f8b<+530>:movq(%rax),%rax
0x10c557f8e<+533>:cmpq-0x28(%rbp),%rax
0x10c557f92<+537>:jne0x10c557fd4;<+603>
0x10c557f94<+539>:movq%r15,%rax
0x10c557f97<+542>:leaq-0x20(%rbp),%rsp
0x10c557f9b<+546>:popq%rbx
0x10c557f9c<+547>:popq%r12
0x10c557f9e<+549>:popq%r14
0x10c557fa0<+551>:popq%r15
0x10c557fa2<+553>:popq%rbp
0x10c557fa3<+554>:retq
0x10c557fa4<+555>:leaq0x382385(%rip),%rdi;_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce
0x10c557fab<+562>:leaq0x2b9886(%rip),%rsi;__block_literal_global.8
0x10c557fb2<+569>:callq0x10c7753d8;symbolstubfor:dispatch_once
0x10c557fb7<+574>:jmp0x10c557e96;<+285>
0x10c557fbc<+579>:leaq0x382365(%rip),%rdi;_NSKVONotifyingCreateInfoWithOriginalClass.onceToken
0x10c557fc3<+586>:leaq0x2b982e(%rip),%rsi;__block_literal_global
0x10c557fca<+593>:callq0x10c7753d8;symbolstubfor:dispatch_once
0x10c557fcf<+598>:jmp0x10c557f25;<+428>
0x10c557fd4<+603>:callq0x10c775102;symbolstubfor:__stack_chk_fail

翻译成伪代码如下:

typedefstruct{
ClassoriginalClass;//offset0x0
ClassKVOClass;//offset0x8
CFMutableSetRefmset;//offset0x10
CFMutableDictionaryRefmdict;//offset0x18
pthread_mutex_t*lock;//offset0x20
void*sth1;//offset0x28
void*sth2;//offset0x30
void*sth3;//offset0x38
void*sth4;//offset0x40
void*sth5;//offset0x48
void*sth6;//offset0x50
void*sth7;//offset0x58
boolflag;//offset0x60
}SDTestKVOClassIndexedIvars;
Class_NSKVONotifyingCreateInfoWithOriginalClass(ClassoriginalClass){
constchar*clsName=class_getName(originalClass);
size_tlen=strlen(clsName);
len+=0x10;
char*newClsName=malloc(len);
constchar*prefix="NSKVONotifying_";
__strlcpy_chk(newClsName,prefix,len);
__strlcat_chk(newClsName,clsName,len,-1);
ClassnewCls=objc_allocateClassPair(originalClass,newClsName,0x68);
if(newCls){
objc_registerClassPair(newCls);
SDTestKVOClassIndexedIvars*indexedIvars=object_getIndexedIvars(newCls);
indexedIvars->originalClass=originalClass;
indexedIvars->KVOClass=newCls;
CFMutableSetRefmset=CFSetCreateMutable(nil,0,kCFCopyStringSetCallBacks);
indexedIvars->mset=mset;
CFMutableDictionaryRefmdict=CFDictionaryCreateMutable(nil,0,nil,kCFTypeDictionaryValueCallBacks);
indexedIvars->mdict=mdict;
pthread_mutex_init(indexedIvars->lock);
staticdispatch_once_tonceToken;
dispatch_once(&onceToken,^{
boolflag=true;
IMPwillChangeValueForKeyImp=class_getMethodImplementation(indexedIvars->originalClass,@selector(willChangeValueForKey:));
IMPdidChangeValueForKeyImp=class_getMethodImplementation(indexedIvars->originalClass,@selector(didChangeValueForKey:));
if(willChangeValueForKeyImp==_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange&&didChangeValueForKeyImp==_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange){
flag=false;
}
indexedIvars->flag=flag;
NSKVONotifyingSetMethodImplementation(indexedIvars,@selector(_isKVOA),NSKVOIsAutonotifying,nil)
NSKVONotifyingSetMethodImplementation(indexedIvars,@selector(dealloc),NSKVODeallocate,nil)
NSKVONotifyingSetMethodImplementation(indexedIvars,@selector(class),NSKVOClass,nil)
});
}else{
//错误处理过程省略......
returnnil
}
returnnewCls;
}

通过_NSKVONotifyingCreateInfoWithOriginalClass 的这段伪代码你会发现我们之前频繁提到 indexedIvars 原来就是在这里初始化生成的。

objc_allocateClassPair 在 runtime.h 中的声明为 Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) ,苹果对 extraBytes 参数的解释为“The number of bytes to allocate for indexed ivars at the end of the class and metaclass objects.”

这就是说当我们在通过 objc_allocateClassPair 来生成一个新的类时可以通过指定 extraBytes 来为此类开辟额外的空间用于存储一些数据。系统在生成 KVO 类时会额外分配 0x68 字节的空间,其具体内存布局和用途我用一个结构体描述如下:

typedefstruct{
ClassoriginalClass;//offset0x0
ClassKVOClass;//offset0x8
CFMutableSetRefmset;//offset0x10
CFMutableDictionaryRefmdict;//offset0x18
pthread_mutex_t*lock;//offset0x20
void*sth1;//offset0x28
void*sth2;//offset0x30
void*sth3;//offset0x38
void*sth4;//offset0x40
void*sth5;//offset0x48
void*sth6;//offset0x50
void*sth7;//offset0x58
boolflag;//offset0x60
}SDTestKVOClassIndexedIvars;

3. 如何解决 custom-KVO 导致的 native-KVO Crash

读到这里相信你对 KVO 实现细节有了大致的了解,然后我们再回到最初的问题,为什么“先调用 native-KVO 再调用 custom-KVO,custom-KVO 运行正常,native-KVO 会 crash”呢?我们还以上面提到过的 Test 类为例说明一下:

首先用 Test 类实例化了一个实例 test,然后对 test 的 num 属性进行 native-KVO 操作,这时 test 的 isa 指向了 NSKVONotifying_Test 类。

然后我们再对 test 进行 custom-KVO 操作,这时我们的 custom-KVO 会基于 NSKVONotifying_Test 类再生成一个新的子类 SD_NSKVONotifying_Test_abcd,此时问题就来了,如果我们没有仿照 native-KVO 的做法额外分配 0x68 字节的空间用于存储 KVO 关键信息,那么当我们向 test 发送 setNum:消息然后 setNum:方法调用 super 实现走到了 KVO 的_NSSetIntValueAndNotify 方法时还按照 SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(cls)方式来获取 KVO 信息并尝试获取从中获取数据时发生异常导致 crash。

找到问题的根源之后我们就可以见招拆招,我们可以仿照 native-KVO 的做法在生成 SD_NSKVONotifying_Test_abcd 也额外分配 0x68 自己的空间,然后当要进行 custom-KVO 操作时将 NSKVONotifying_Test 的 indexedIvars 拷贝一份到 SD_NSKVONotifying_Test_abcd 即可,代码实现如下:

iOS开发KVO实现细节解密

iOS开发KVO实现细节解密

一般情况下在 native-KVO 的基础上再做 custom-KVO 的话拷贝完 native-KVO 类的 indexedIvars 到 custom-KVO 类上就可以了,而我们的 SDMagicHook 只做到这些还不够,因为 SDMagicHook 在生成的新类上以消息转发的形式来调度方法,这样一来问题瞬间就变得更为复杂。举例说明如下:

由于用到消息转发,我们会将 SD_NSKVONotifying_Test_abcd 的setNum:对应的实现指向_objc_msgForward,然后生成一个新的 SEL__sd_B_abcd_setNum:来指向其子类的原生实现,在我们这个例子中就是 NSKVONotifying_TestsetNum:实现的即void _NSSetIntValueAndNotify(id obj, SEL sel, int number)函数。
当 test 实例收到setNum:消息时会先触发消息转发机制,然后 SDMagicHook 的消息调度系统会最终通过向 test 实例发送一个__sd_B_abcd_setNum:消息来实现对被 Hook 的原生方法的回调,而现在__sd_B_abcd_setNum:对应的实现函数正是void _NSSetIntValueAndNotify(id obj, SEL sel, int number),所以__sd_B_abcd_setNum:就会被作为 sel 参数传递到_NSSetIntValueAndNotify函数。
然后当_NSSetIntValueAndNotify函数内部尝试从 indexedIvars 拿到原始类 Test 然后从 Test 上查找__sd_B_abcd_setNum:对应的方法并调用时由于找不到对应函数实现而发生 crash。为解决这个问题,我们还需要为 Test 类新增一个__sd_B_abcd_setNum:方法并将其实现指向setNum:的实现,代码如下:

iOS开发KVO实现细节解密

iOS开发KVO实现细节解密

至此,“先调用 native-KVO 再调用 custom-KVO,custom-KVO 运行正常,native-KVO 会 crash”这个问题就可以顺利解决了。

4. 如何解决 native-KVO 导致 custom-KVO 失效的问题

目前还剩下一个问题“先调用 native-KVO 再调用 custom-KVO 再调用 native-KVO,native-KVO 运行正常,custom-KVO 失效,无 crash”。

为什么会出现这个问题呢?这次我们依然以 Test 类为例,首先用 Test 类实例化了一个实例 test,然后对 test 的 num 属性进行 native-KVO 操作,这时 test 的 isa 指向了 NSKVONotifying_Test 类。

然后我们再对 test 进行 custom-KVO 操作,这时我们的 custom-KVO 会基于 NSKVONotifying_Test 类再生成一个新的子类 SD_NSKVONotifying_Test_abcd,这时如果再对 test 的 num 属性进行 native-KVO 操作就会惊奇地发现 test 的 isa 又重新指向了 NSKVONotifying_Test 类然后 custom-KVO 就全部失效了。

WHY?!!原来 native-KVO 会持有一个全局的字典:

_NSKeyValueContainerClassForIsa.NSKeyValueContainerClassPerOriginalClass 以 KVO 操作的原类为 key 和 NSKeyValueContainerClass 实例为 value 存储 KVO 类信息。

iOS开发KVO实现细节解密

这样一来,当我们再次对 test 实例进行 KVO 操作时,native-KVO 就会以 Test 类为 key 从 NSKeyValueContainerClassPerOriginalClass 中查找到之前存储的 NSKeyValueContainerClass 并从中直接获取 KVO 类 NSKVONotifying_Test 然后调用 object_setclass 方法设置到 test 实例上然后 custom-KVO 就直接失效了。

想要解决这个问题,我想到了两种思路:

1.修改 NSKVONotifying_Test 相关 KVO 数据

2.hook 拦截系统的 setclass 操作。然后仔细一想方案 1 是不可取的,因为 NSKVONotifying_Test 的相关数据是被所有 Test 类的实例在进行 KVO 操作时共享的,任何改动都有可能对 Test 类实例的 KVO 产生全局影响。

所以,我们就需要借助 FishHook 来 hook 系统的 object_setclass 函数,当系统以 NSKVONotifying_Test 为参数对一个实例进行 setclass 操作时,我们检查如果当前的 isa 指针是 SD_NSKVONotifying_Test_abcd 且 SD_NSKVONotifying_Test_abcd 继承自系统的 NSKVONotifying_Test 时就跳过此次 setclass 操作。

但是这样做还不够,因为 custom-KVO 采用了特殊的消息转发机制来调度被 hook 的方法,如果先进行 custom-KVO 然后在进行 native-KVO 就会导致被观察属性被重复调用。

所以,我们在对一个实例进行首次 custom-KVO 操作之前先进行 native-KVO,这样一来就可以保证我们的 custom-KVO 的方法调度正常工作了。

代码如下:

iOS开发KVO实现细节解密

iOS开发KVO实现细节解密

总结

KVO 的本质其实就是基于被观察的实例的 isa 生成一个新的类并在这个类的 extra 空间中存放各种和 KVO 操作相关的关键数据,然后这个新的类以一个中间人的角色借助 extra 空间中存放各种数据完成复杂的方法调度。

系统的 KVO 实现比较复杂,很多函数的调用层次也比较深,我们一开始不妨从整个函数调用栈的末端层层向前梳理出主要的操作路径,在对 KVO 操作有个大致的了解之后再从全局的角度正向全面分析各个流程和细节。我们正是借助这种方式实现了对 KVO 的快速了解和认识。

至此,一个良好兼容 native-KVO 的 custom-KVO 就全部完成了。回头来看,这个解决方案其实还是过于 tricky 了,不过这也只能是在 iOS 系统的各种限制下的无奈的选择了。我们不提倡随意使用类似的 tricky 操作,更多是想要通过这个例子向大家介绍一下 KVO 的本质以及我们分析和解决问题的思路。

如果各位读者可以从中汲取一些灵感,那么这篇文章“倒也算是不负恩泽”,倘若大家可以将这篇文章介绍到的思路和方法用于处理自己开发中的遇到的各种疑难杂症“那便真真是极好的了”!更多关于iOS开发KVO细节的资料请关注我们其它相关文章!