找回密码
 会员注册
查看: 17|回复: 0

iOS大解密:玄之又玄的KVO

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-29 22:55:34 | 显示全部楼层 |阅读模式
导读:大多数 iOS 开发人员对 KVO 的认识只局限于 isa 指针交换这一层,而 KVO 的实现细节却鲜为人知。如果自己也仿照 KVO 基础原理来实现一套类 KVO 操作且独立运行时会发现一切正常,然而一旦你的实现和系统的 KVO 实现同时作用在同一个实例上那么各种各样诡异的 bug 和 crash 就会层出不穷。这究竟是为什么呢?此类问题到底该如何解决呢?接下来我们将尝试从汇编层面来入手以层层揭开 KVO 的神秘面纱......1. 缘起 AspectsSDMagicHook 开源之后很多小伙伴在问“ 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:pushq%rbp->0x10e5b0fc3:movq%rsp,%rbp0x10e5b0fc6:pushq%r150x10e5b0fc8:pushq%r140x10e5b0fca:pushq%r130x10e5b0fcc:pushq%r120x10e5b0fce:pushq%rbx0x10e5b0fcf:subq$0x48,%rsp0x10e5b0fd3:movl%edx,-0x2c(%rbp)0x10e5b0fd6:movq%rsi,%r150x10e5b0fd9:movq%rdi,%r130x10e5b0fdc:callq0x10e7cc882;symbolstubforbject_getClass0x10e5b0fe1:movq%rax,%rdi0x10e5b0fe4:callq0x10e7cc88e;symbolstubforbject_getIndexedIvars0x10e5b0fe9:movq%rax,%rbx0x10e5b0fec:leaq0x20(%rbx),%r140x10e5b0ff0:movq%r14,%rdi0x10e5b0ff3:callq0x10e7cca26;symbolstubfor:pthread_mutex_lock0x10e5b0ff8:movq0x18(%rbx),%rdi0x10e5b0ffc:movq%r15,%rsi0x10e5b0fff:callq0x10e7cb472;symbolstubfor:CFDictionaryGetValue0x10e5b1004:movq0x36329d(%rip),%rsi;"copyWithZone:"0x10e5b100b:xorl%edx,%edx0x10e5b100d:movq%rax,%rdi0x10e5b1010:callq*0x2b2862(%rip);(void*)0x000000010eb89d80bjc_msgSend0x10e5b1016:movq%rax,%r120x10e5b1019:movq%r14,%rdi0x10e5b101c:callq0x10e7cca32;symbolstubfor:pthread_mutex_unlock0x10e5b1021:cmpb$0x0,0x60(%rbx)0x10e5b1025:je0x10e5b1066;0x10e5b1027:movq0x36439a(%rip),%rsi;"willChangeValueForKey:"0x10e5b102e:movq0x2b2843(%rip),%r14;(void*)0x000000010eb89d80bjc_msgSend0x10e5b1035:movq%r13,%rdi0x10e5b1038:movq%r12,%rdx0x10e5b103b:callq*%r140x10e5b103e:movq(%rbx),%rdi0x10e5b1041:movq%r15,%rsi0x10e5b1044:callq0x10e7cc2b2;symbolstubfor:class_getMethodImplementation0x10e5b1049:movq%r13,%rdi0x10e5b104c:movq%r15,%rsi0x10e5b104f:movl-0x2c(%rbp),%edx0x10e5b1052:callq*%rax0x10e5b1054:movq0x364385(%rip),%rsi;"didChangeValueForKey:"0x10e5b105b:movq%r13,%rdi0x10e5b105e:movq%r12,%rdx0x10e5b1061:callq*%r140x10e5b1064:jmp0x10e5b10be;0x10e5b1066:movq0x2b22eb(%rip),%rax;(void*)0x00000001120b9070:_NSConcreteStackBlock0x10e5b106d:leaq-0x68(%rbp),%r90x10e5b1071:movq%rax,(%r9)0x10e5b1074:movl$0xc2000000,%eax;imm=0xC20000000x10e5b1079:movq%rax,0x8(%r9)0x10e5b107d:leaq0xf5d(%rip),%rax;___NSSetIntValueAndNotify_block_invoke0x10e5b1084:movq%rax,0x10(%r9)0x10e5b1088:leaq0x2b7929(%rip),%rax;__block_descriptor_tmp.770x10e5b108f:movq%rax,0x18(%r9)0x10e5b1093:movq%rbx,0x28(%r9)0x10e5b1097:movq%r15,0x30(%r9)0x10e5b109b:movq%r13,0x20(%r9)0x10e5b109f:movl-0x2c(%rbp),%eax0x10e5b10a2:movl%eax,0x38(%r9)0x10e5b10a6:movq0x364fab(%rip),%rsi;"_changeValueForKey:key:key:usingBlock:"0x10e5b10ad:xorl%ecx,%ecx0x10e5b10af:xorl%r8d,%r8d0x10e5b10b2:movq%r13,%rdi0x10e5b10b5:movq%r12,%rdx0x10e5b10b8:callq*0x2b27ba(%rip);(void*)0x000000010eb89d80bjc_msgSend0x10e5b10be:movq0x362f73(%rip),%rsi;"release"0x10e5b10c5:movq%r12,%rdi0x10e5b10c8:callq*0x2b27aa(%rip);(void*)0x000000010eb89d80bjc_msgSend0x10e5b10ce:addq$0x48,%rsp0x10e5b10d2:popq%rbx0x10e5b10d3:popq%r120x10e5b10d5:popq%r130x10e5b10d7:popq%r140x10e5b10d9:popq%r150x10e5b10db:popq%rbp0x10e5b10dc:retq上面这段汇编代码翻译为伪代码大致如下:typedefstruct{ClassoriginalClass;//offset0x0ClassKVOClass;//offset0x8CFMutableSetRefmset;//offset0x10CFMutableDictionaryRefmdict;//offset0x18pthread_mutex_t*lock;//offset0x20void*sth1;//offset0x28void*sth2;//offset0x30void*sth3;//offset0x38void*sth4;//offset0x40void*sth5;//offset0x48void*sth6;//offset0x50void*sth7;//offset0x58boolflag;//offset0x60}SDTestKVOClassIndexedIvars;typedefstruct{Classisa;//offset0x0intflags;//offset0x8intreserved;IMPinvoke;//offset0x10void*descriptor;//offset0x18void*captureVar1;//offset0x20void*captureVar2;//offset0x28void*captureVar3;//offset0x30intcaptureVar4;//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{//生成blockSDTestStackBlockblock={};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)的方式实现对被观察的原方法的调用,否则就用默认实现为 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:pushq%rbp0x10bf27fe2:movq%rsp,%rbp0x10bf27fe5:pushq%rbx0x10bf27fe6:pushq%rax0x10bf27fe7:movq%rdi,%rbx0x10bf27fea:movq0x28(%rbx),%rax0x10bf27fee:movq0x30(%rbx),%rsi0x10bf27ff2:movq(%rax),%rdi0x10bf27ff5:callq0x10c1422b2;symbolstubfor:class_getMethodImplementation0x10bf27ffa:movq0x20(%rbx),%rdi0x10bf27ffe:movq0x30(%rbx),%rsi0x10bf28002:movl0x38(%rbx),%edx0x10bf28005:addq$0x8,%rsp0x10bf28009:popq%rbx0x10bf2800a:popq%rbp0x10bf2800b: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 调试截图如下:断点到 object_setClass 之后,我们再验证看下寄存器 rdi、rsi 里面的参数打印出来分别是、NSKVONotifying_Test不错,我们现在已经成功定位到 KVO 的 isa 交换现场了,然而为了找到 KVO 类的生成的地方我们还需要沿着调用栈向前回溯,最终我们定位到 KVO 类的生成函数_NSKVONotifyingCreateInfoWithOriginalClass,其汇编代码如下:Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:->0x10c557d79:pushq%rbp0x10c557d7a:movq%rsp,%rbp0x10c557d7d:pushq%r150x10c557d7f:pushq%r140x10c557d81:pushq%r120x10c557d83:pushq%rbx0x10c557d84:subq$0x20,%rsp0x10c557d88:movq%rdi,%r140x10c557d8b:movq0x2b463e(%rip),%rax;(void*)0x000000011012d070:__stack_chk_guard0x10c557d92:movq(%rax),%rax0x10c557d95:movq%rax,-0x28(%rbp)0x10c557d99:xorl%eax,%eax0x10c557d9b:callq0x10c55b452;NSKeyValueObservingAssertRegistrationLockHeld0x10c557da0:movq%r14,%rdi0x10c557da3:callq0x10c7752b8;symbolstubfor:class_getName0x10c557da8:movq%rax,%r120x10c557dab:movq%r12,%rdi0x10c557dae:callq0x10c775ba0;symbolstubfor:strlen0x10c557db3:movq%rax,%rbx0x10c557db6:addq$0x10,%rbx0x10c557dba:movq%rbx,%rdi0x10c557dbd:callq0x10c775666;symbolstubfor:malloc0x10c557dc2:movq%rax,%r150x10c557dc5:leaq0x29d604(%rip),%rsi;_NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix0x10c557dcc:movq$-0x1,%rcx0x10c557dd3:movq%r15,%rdi0x10c557dd6:movq%rbx,%rdx0x10c557dd9:callq0x10c77510e;symbolstubfor:__strlcpy_chk0x10c557dde:movq$-0x1,%rcx0x10c557de5:movq%r15,%rdi0x10c557de8:movq%r12,%rsi0x10c557deb:movq%rbx,%rdx0x10c557dee:callq0x10c775108;symbolstubfor:__strlcat_chk0x10c557df3:movl$0x68,%edx0x10c557df8:movq%r14,%rdi0x10c557dfb:movq%r15,%rsi0x10c557dfe:callq0x10c775762;symbolstubforbjc_allocateClassPair0x10c557e03:movq%rax,%rbx0x10c557e06:testq%rbx,%rbx0x10c557e09:je0x10c557f17;0x10c557e0f:movq%rbx,%rdi0x10c557e12:callq0x10c775816;symbolstubforbjc_registerClassPair0x10c557e17:movq%r15,%rdi0x10c557e1a:callq0x10c7754ec;symbolstubfor:free0x10c557e1f:movq%rbx,%rdi0x10c557e22:callq0x10c77588e;symbolstubforbject_getIndexedIvars0x10c557e27:movq%rax,%r150x10c557e2a:movq%r14,(%r15)0x10c557e2d:movq%rbx,0x8(%r15)0x10c557e31:movq0x2b4748(%rip),%rdx;(void*)0x000000010d7fd1f8:kCFCopyStringSetCallBacks0x10c557e38:xorl%edi,%edi0x10c557e3a:xorl%esi,%esi0x10c557e3c:callq0x10c774778;symbolstubfor:CFSetCreateMutable0x10c557e41:movq%rax,0x10(%r15)0x10c557e45:movq0x2b49e4(%rip),%rcx;(void*)0x000000010d7f6bb8:kCFTypeDictionaryValueCallBacks0x10c557e4c:xorl%edi,%edi0x10c557e4e:xorl%esi,%esi0x10c557e50:xorl%edx,%edx0x10c557e52:callq0x10c774454;symbolstubfor:CFDictionaryCreateMutable0x10c557e57:movq%rax,0x18(%r15)0x10c557e5b:leaq-0x38(%rbp),%rbx0x10c557e5f:movq%rbx,%rdi0x10c557e62:callq0x10c775a3e;symbolstubfor:pthread_mutexattr_init0x10c557e67:movl$0x2,%esi0x10c557e6c:movq%rbx,%rdi0x10c557e6f:callq0x10c775a44;symbolstubfor:pthread_mutexattr_settype0x10c557e74:leaq0x20(%r15),%rdi0x10c557e78:movq%rbx,%rsi0x10c557e7b:callq0x10c775a20;symbolstubfor:pthread_mutex_init0x10c557e80:movq%rbx,%rdi0x10c557e83:callq0x10c775a38;symbolstubfor:pthread_mutexattr_destroy0x10c557e88:cmpq$-0x1,0x3824a0(%rip);_NSKVONotifyingCreateInfoWithOriginalClass.onceToken+70x10c557e90:jne0x10c557fa4;0x10c557e96:movq(%r15),%rdi0x10c557e99:movq0x366528(%rip),%rsi;"willChangeValueForKey:"0x10c557ea0:callq0x10c7752b2;symbolstubfor:class_getMethodImplementation0x10c557ea5:movb$0x1,%cl0x10c557ea7:cmpq0x38248a(%rip),%rax;_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange0x10c557eae:jne0x10c557ec9;0x10c557eb0:movq(%r15),%rdi0x10c557eb3:movq0x366526(%rip),%rsi;"didChangeValueForKey:"0x10c557eba:callq0x10c7752b2;symbolstubfor:class_getMethodImplementation0x10c557ebf:cmpq0x38247a(%rip),%rax;_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange0x10c557ec6:setne%cl0x10c557ec9:movb%cl,0x60(%r15)0x10c557ecd:movq0x36715c(%rip),%rsi;"_isKVOA"0x10c557ed4:leaq0x1ff(%rip),%rdx;NSKVOIsAutonotifying0x10c557edb:xorl%ecx,%ecx0x10c557edd:movq%r15,%rdi0x10c557ee0:callq0x10c558057;NSKVONotifyingSetMethodImplementation0x10c557ee5:movq0x365154(%rip),%rsi;"dealloc"0x10c557eec:leaq0x1ef(%rip),%rdx;NSKVODeallocate0x10c557ef3:xorl%ecx,%ecx0x10c557ef5:movq%r15,%rdi0x10c557ef8:callq0x10c558057;NSKVONotifyingSetMethodImplementation0x10c557efd:movq0x36519c(%rip),%rsi;"class"0x10c557f04:leaq0x433(%rip),%rdx;NSKVOClass0x10c557f0b:xorl%ecx,%ecx0x10c557f0d:movq%r15,%rdi0x10c557f10:callq0x10c558057;NSKVONotifyingSetMethodImplementation0x10c557f15:jmp0x10c557f84;0x10c557f17:cmpq$-0x1,0x382409(%rip);_NSKVONotifyingCreateInfoWithOriginalClass.kvoLog+70x10c557f1f:jne0x10c557fbc;0x10c557f25:movq0x3823f4(%rip),%r14;_NSKVONotifyingCreateInfoWithOriginalClass.kvoLog0x10c557f2c:movl$0x10,%esi0x10c557f31:movq%r14,%rdi0x10c557f34:callq0x10c7758e2;symbolstubfors_log_type_enabled0x10c557f39:testb%al,%al0x10c557f3b:je0x10c557f79;0x10c557f3d:movq%rsp,%rbx0x10c557f40:movq%rsp,%rax0x10c557f43:leaq-0x10(%rax),%r80x10c557f47:movq%r8,%rsp0x10c557f4a:movl$0x8200102,-0x10(%rax);imm=0x82001020x10c557f51:movq%r15,-0xc(%rax)0x10c557f55:leaq-0x63f5c(%rip),%rdi0x10c557f5c:leaq0x296c1d(%rip),%rcx;"KVOfailedtoallocateclasspairforname%s,automatickey-valueobservingwillnotworkforthisclass"0x10c557f63:movl$0x10,%edx0x10c557f68:movl$0xc,%r9d0x10c557f6e:movq%r14,%rsi0x10c557f71:callq0x10c7751aa;symbolstubfor:_os_log_error_impl0x10c557f76:movq%rbx,%rsp0x10c557f79:movq%r15,%rdi0x10c557f7c:callq0x10c7754ec;symbolstubfor:free0x10c557f81:xorl%r15d,%r15d0x10c557f84:movq0x2b4445(%rip),%rax;(void*)0x000000011012d070:__stack_chk_guard0x10c557f8b:movq(%rax),%rax0x10c557f8e:cmpq-0x28(%rbp),%rax0x10c557f92:jne0x10c557fd4;0x10c557f94:movq%r15,%rax0x10c557f97:leaq-0x20(%rbp),%rsp0x10c557f9b:popq%rbx0x10c557f9c:popq%r120x10c557f9e:popq%r140x10c557fa0:popq%r150x10c557fa2:popq%rbp0x10c557fa3:retq0x10c557fa4:leaq0x382385(%rip),%rdi;_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce0x10c557fab:leaq0x2b9886(%rip),%rsi;__block_literal_global.80x10c557fb2:callq0x10c7753d8;symbolstubfor:dispatch_once0x10c557fb7:jmp0x10c557e96;0x10c557fbc:leaq0x382365(%rip),%rdi;_NSKVONotifyingCreateInfoWithOriginalClass.onceToken0x10c557fc3:leaq0x2b982e(%rip),%rsi;__block_literal_global0x10c557fca:callq0x10c7753d8;symbolstubfor:dispatch_once0x10c557fcf:jmp0x10c557f25;0x10c557fd4:callq0x10c775102;symbolstubfor:__stack_chk_fail翻译成伪代码如下:typedefstruct{ClassoriginalClass;//offset0x0ClassKVOClass;//offset0x8CFMutableSetRefmset;//offset0x10CFMutableDictionaryRefmdict;//offset0x18pthread_mutex_t*lock;//offset0x20void*sth1;//offset0x28void*sth2;//offset0x30void*sth3;//offset0x38void*sth4;//offset0x40void*sth5;//offset0x48void*sth6;//offset0x50void*sth7;//offset0x58boolflag;//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;//offset0x0ClassKVOClass;//offset0x8CFMutableSetRefmset;//offset0x10CFMutableDictionaryRefmdict;//offset0x18pthread_mutex_t*lock;//offset0x20void*sth1;//offset0x28void*sth2;//offset0x30void*sth3;//offset0x38void*sth4;//offset0x40void*sth5;//offset0x48void*sth6;//offset0x50void*sth7;//offset0x58boolflag;//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 即可,代码实现如下:一般情况下在 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:的实现,代码如下:至此,“先调用 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 类信息。这样一来,当我们再次对 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 的方法调度正常工作了。代码如下:总结KVO 的本质其实就是基于被观察的实例的 isa 生成一个新的类并在这个类的 extra 空间中存放各种和 KVO 操作相关的关键数据,然后这个新的类以一个中间人的角色借助 extra 空间中存放各种数据完成复杂的方法调度。系统的 KVO 实现比较复杂,很多函数的调用层次也比较深,我们一开始不妨从整个函数调用栈的末端层层向前梳理出主要的操作路径,在对 KVO 操作有个大致的了解之后再从全局的角度正向全面分析各个流程和细节。我们正是借助这种方式实现了对 KVO 的快速了解和认识。至此,一个良好兼容 native-KVO 的 custom-KVO 就全部完成了。回头来看,这个解决方案其实还是过于 tricky 了,不过这也只能是在 iOS 系统的各种限制下的无奈的选择了。我们不提倡随意使用类似的 tricky 操作,更多是想要通过这个例子向大家介绍一下 KVO 的本质以及我们分析和解决问题的思路。如果各位读者可以从中汲取一些灵感,那么这篇文章“倒也算是不负恩泽”,倘若大家可以将这篇文章介绍到的思路和方法用于处理自己开发中的遇到的各种疑难杂症“那便真真是极好的了”!更多分享开源 | Objective-C & Swift 最轻量级 Hook 方案今日头条 Android '秒' 级编译速度优化字节跳动分布式表格存储系统的演进字节跳动自研强一致在线 KV &表格存储实践 - 上篇字节跳动 -飞书音视频Mobile团队本团队主要服务于飞书音视频产品,在产品性能、稳定性等用户体验,研发流程,编译优化,架构方向上不断优化和深入探索,以满足产品快速迭代的同时,保持较高的用户体验。我们长期在上海招聘 Android / iOS以及全栈平台架构方向的同学,想深入交流或者需要部门内推、投递简历的可以联系邮箱:qiuzehui@bytedance.com(注明 : 字节跳动-飞书Mobile直推)。欢迎关注「字节跳动技术团队」点击阅读原文,快来加入我们吧!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-16 02:44 , Processed in 0.691400 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表