源码先锋

源码先锋

iOS大解密:玄之又玄的KVO

admin 155 49
1.缘起Aspects

SDMagicHook开源之后很多小伙伴在问“SDMagicHook和Aspects的区别是什么?”,我在GitHub上找到Aspects了解之后发现Aspects也是以isa交换为基础原理进行的hook操作,但是两者在具体实现和API设计上也有一些区别,另外SDMagicHook还解决了Aspects未能解决的KVO冲突难题。

1.1SDMagicHook的API设计更加友好灵活

SDMagicHook和Aspects的具体异同分析见。

1.2SDMagicHook解决了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,%rbp0x10e5b0fc6+4:pushq%r150x10e5b0fc8+6:pushq%r140x10e5b0fca+8:pushq%r130x10e5b0fcc+10:pushq%r120x10e5b0fce+12:pushq%rbx0x10e5b0fcf+13:subq$0x48,%rsp0x10e5b0fd3+17:movl%edx,-0x2c(%rbp)0x10e5b0fd6+20:movq%rsi,%r150x10e5b0fd9+23:movq%rdi,%r130x10e5b0fdc+26:callq0x10e7cc882;symbolstubfor:object_getClass0x10e5b0fe1+31:movq%rax,%rdi0x10e5b0fe4+34:callq0x10e7cc88e;symbolstubfor:object_getIndexedIvars0x10e5b0fe9+39:movq%rax,%rbx0x10e5b0fec+42:leaq0x20(%rbx),%r140x10e5b0ff0+46:movq%r14,%rdi0x10e5b0ff3+49:callq0x10e7cca26;symbolstubfor:pthread_mutex_lock0x10e5b0ff8+54:movq0x18(%rbx),%rdi0x10e5b0ffc+58:movq%r15,%rsi0x10e5b0fff+61:callq0x10e7cb472;symbolstubfor:CFDictionaryGetValue0x10e5b1004+66:movq0x36329d(%rip),%rsi;"copyWithZone:"0x10e5b100b+73:xorl%edx,%edx0x10e5b100d+75:movq%rax,%rdi0x10e5b1010+78:callq*0x2b2862(%rip);(void*)0x000000010eb89d80:objc_msgS0x10e5b1016+84:movq%rax,%r120x10e5b1019+87:movq%r14,%rdi0x10e5b101c+90:callq0x10e7cca32;symbolstubfor:pthread_mutex_unlock0x10e5b1021+95:cmpb$0x0,0x60(%rbx)0x10e5b1025+99:je0x10e5b1066;+1640x10e5b1027+101:movq0x36439a(%rip),%rsi;"willChangeValueForKey:"0x10e5b102e+108:movq0x2b2843(%rip),%r14;(void*)0x000000010eb89d80:objc_msgS0x10e5b1035+115:movq%r13,%rdi0x10e5b1038+118:movq%r12,%rdx0x10e5b103b+121:callq*%r140x10e5b103e+124:movq(%rbx),%rdi0x10e5b1041+127:movq%r15,%rsi0x10e5b1044+130:callq0x10e7cc2b2;symbolstubfor:class_getMethodImplementation0x10e5b1049+135:movq%r13,%rdi0x10e5b104c+138:movq%r15,%rsi0x10e5b104f+141:movl-0x2c(%rbp),%edx0x10e5b1052+144:callq*%rax0x10e5b1054+146:movq0x364385(%rip),%rsi;"didChangeValueForKey:"0x10e5b105b+153:movq%r13,%rdi0x10e5b105e+156:movq%r12,%rdx0x10e5b1061+159:callq*%r140x10e5b1064+162:jmp0x10e5b10be;+2520x10e5b1066+164:movq0x2b22eb(%rip),%rax;(void*)0x00000001120b9070:_NSConcreteStackBlock0x10e5b106d+171:leaq-0x68(%rbp),%r90x10e5b1071+175:movq%rax,(%r9)0x10e5b1074+178:movl$0xc2000000,%eax;imm=0xC20000000x10e5b1079+183:movq%rax,0x8(%r9)0x10e5b107d+187:leaq0xf5d(%rip),%rax;___NSSetIntValueAndNotify_block_invoke0x10e5b1084+194:movq%rax,0x10(%r9)0x10e5b1088+198:leaq0x2b7929(%rip),%rax;__block_descriptor_+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),%eax0x10e5b10a2+224:movl%eax,0x38(%r9)0x10e5b10a6+228:movq0x364fab(%rip),%rsi;"_changeValueForKey:key:key:usingBlock:"0x10e5b10ad+235:xorl%ecx,%ecx0x10e5b10af+237:xorl%r8d,%r8d0x10e5b10b2+240:movq%r13,%rdi0x10e5b10b5+243:movq%r12,%rdx0x10e5b10b8+246:callq*0x2b27ba(%rip);(void*)0x000000010eb89d80:objc_msgS0x10e5b10be+252:movq0x362f73(%rip),%rsi;"release"0x10e5b10c5+259:movq%r12,%rdi0x10e5b10c8+262:callq*0x2b27aa(%rip);(void*)0x000000010eb89d80:objc_msgS0x10e5b10ce+268:addq$0x48,%rsp0x10e5b10d2+272:popq%rbx0x10e5b10d3+273:popq%r120x10e5b10d5+275:popq%r130x10e5b10d7+277:popq%r140x10e5b10d9+279:popq%r150x10e5b10db+281:popq%rbp0x10e5b10dc+282: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={};=_NSConcreteStackBlock;=0xC2000000;=___NSSetIntValueAndNotify_block_invoke;=__block_descriptor_tmp;=indexedIvars;=sel;=obj;=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:strkey:nilkey:nilusingBlock:SDTestStackBlock]调用。看到这里你或许会有个疑问:伪代码中通过object_getIndexedIvars(cls)获取到的indexedIvars是什么信息呢?=___NSSetIntValueAndNotify_block_invoke又是如何实现的呢?首先我们看下NSSetIntValueAndNotify_block_invoke的汇编实现:

Foundation`___NSSetIntValueAndNotify_block_invoke:-0x10bf27fe1+0:pushq%rbp0x10bf27fe2+1:movq%rsp,%rbp0x10bf27fe5+4:pushq%rbx0x10bf27fe6+5:pushq%rax0x10bf27fe7+6:movq%rdi,%rbx0x10bf27fea+9:movq0x28(%rbx),%rax0x10bf27fee+13:movq0x30(%rbx),%rsi0x10bf27ff2+17:movq(%rax),%rdi0x10bf27ff5+20:callq0x10c1422b2;symbolstubfor:class_getMethodImplementation0x10bf27ffa+25:movq0x20(%rbx),%rdi0x10bf27ffe+29:movq0x30(%rbx),%rsi0x10bf28002+33:movl0x38(%rbx),%edx0x10bf28005+36:addq$0x8,%rsp0x10bf28009+40:popq%rbx0x10bf2800a+41:popq%rbp0x10bf2800b+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调试截图如下:

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

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

Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:-0x10c557d79+0:pushq%rbp0x10c557d7a+1:movq%rsp,%rbp0x10c557d7d+4:pushq%r150x10c557d7f+6:pushq%r140x10c557d81+8:pushq%r120x10c557d83+10:pushq%rbx0x10c557d84+11:subq$0x20,%rsp0x10c557d88+15:movq%rdi,%r140x10c557d8b+18:movq0x2b463e(%rip),%rax;(void*)0x000000011012d070:__stack_chk_guard0x10c557d92+25:movq(%rax),%rax0x10c557d95+28:movq%rax,-0x28(%rbp)0x10c557d99+32:xorl%eax,%eax0x10c557d9b+34:callq0x10c55b452;NSKeyValueObservingAssertRegistrationLockHeld0x10c557da0+39:movq%r14,%rdi0x10c557da3+42:callq0x10c7752b8;symbolstubfor:class_getName0x10c557da8+47:movq%rax,%r120x10c557dab+50:movq%r12,%rdi0x10c557dae+53:callq0x10c775ba0;symbolstubfor:strlen0x10c557db3+58:movq%rax,%rbx0x10c557db6+61:addq$0x10,%rbx0x10c557dba+65:movq%rbx,%rdi0x10c557dbd+68:callq0x10c775666;symbolstubfor:malloc0x10c557dc2+73:movq%rax,%r150x10c557dc5+76:leaq0x29d604(%rip),%rsi;_+83:movq$-0x1,%rcx0x10c557dd3+90:movq%r15,%rdi0x10c557dd6+93:movq%rbx,%rdx0x10c557dd9+96:callq0x10c77510e;symbolstubfor:__strlcpy_chk0x10c557dde+101:movq$-0x1,%rcx0x10c557de5+108:movq%r15,%rdi0x10c557de8+111:movq%r12,%rsi0x10c557deb+114:movq%rbx,%rdx0x10c557dee+117:callq0x10c775108;symbolstubfor:__strlcat_chk0x10c557df3+122:movl$0x68,%edx0x10c557df8+127:movq%r14,%rdi0x10c557dfb+130:movq%r15,%rsi0x10c557dfe+133:callq0x10c775762;symbolstubfor:objc_allocateClassPair0x10c557e03+138:movq%rax,%rbx0x10c557e06+141:testq%rbx,%rbx0x10c557e09+144:je0x10c557f17;+4140x10c557e0f+150:movq%rbx,%rdi0x10c557e12+153:callq0x10c775816;symbolstubfor:objc_registerClassPair0x10c557e17+158:movq%r15,%rdi0x10c557e1a+161:callq0x10c7754ec;symbolstubfor:free0x10c557e1f+166:movq%rbx,%rdi0x10c557e22+169:callq0x10c77588e;symbolstubfor:object_getIndexedIvars0x10c557e27+174:movq%rax,%r150x10c557e2a+177:movq%r14,(%r15)0x10c557e2d+180:movq%rbx,0x8(%r15)0x10c557e31+184:movq0x2b4748(%rip),%rdx;(void*)0x000000010d7fd1f8:kCFCopyStringSetCallBacks0x10c557e38+191:xorl%edi,%edi0x10c557e3a+193:xorl%esi,%esi0x10c557e3c+195:callq0x10c774778;symbolstubfor:CFSetCreateMutable0x10c557e41+200:movq%rax,0x10(%r15)0x10c557e45+204:movq0x2b49e4(%rip),%rcx;(void*)0x000000010d7f6bb8:kCFTypeDictionaryValueCallBacks0x10c557e4c+211:xorl%edi,%edi0x10c557e4e+213:xorl%esi,%esi0x10c557e50+215:xorl%edx,%edx0x10c557e52+217:callq0x10c774454;symbolstubfor:CFDictionaryCreateMutable0x10c557e57+222:movq%rax,0x18(%r15)0x10c557e5b+226:leaq-0x38(%rbp),%rbx0x10c557e5f+230:movq%rbx,%rdi0x10c557e62+233:callq0x10c775a3e;symbolstubfor:pthread_mutexattr_init0x10c557e67+238:movl$0x2,%esi0x10c557e6c+243:movq%rbx,%rdi0x10c557e6f+246:callq0x10c775a44;symbolstubfor:pthread_mutexattr_settype0x10c557e74+251:leaq0x20(%r15),%rdi0x10c557e78+255:movq%rbx,%rsi0x10c557e7b+258:callq0x10c775a20;symbolstubfor:pthread_mutex_init0x10c557e80+263:movq%rbx,%rdi0x10c557e83+266:callq0x10c775a38;symbolstubfor:pthread_mutexattr_destroy0x10c557e88+271:cmpq$-0x1,0x3824a0(%rip);_+70x10c557e90+279:jne0x10c557fa4;+5550x10c557e96+285:movq(%r15),%rdi0x10c557e99+288:movq0x366528(%rip),%rsi;"willChangeValueForKey:"0x10c557ea0+295:callq0x10c7752b2;symbolstubfor:class_getMethodImplementation0x10c557ea5+300:movb$0x1,%cl0x10c557ea7+302:cmpq0x38248a(%rip),%rax;_+309:jne0x10c557ec9;+3360x10c557eb0+311:movq(%r15),%rdi0x10c557eb3+314:movq0x366526(%rip),%rsi;"didChangeValueForKey:"0x10c557eba+321:callq0x10c7752b2;symbolstubfor:class_getMethodImplementation0x10c557ebf+326:cmpq0x38247a(%rip),%rax;_+333:setne%cl0x10c557ec9+336:movb%cl,0x60(%r15)0x10c557ecd+340:movq0x36715c(%rip),%rsi;"_isKVOA"0x10c557ed4+347:leaq0x1ff(%rip),%rdx;NSKVOIsAutonotifying0x10c557edb+354:xorl%ecx,%ecx0x10c557edd+356:movq%r15,%rdi0x10c557ee0+359:callq0x10c558057;NSKVONotifyingSetMethodImplementation0x10c557ee5+364:movq0x365154(%rip),%rsi;"dealloc"0x10c557eec+371:leaq0x1ef(%rip),%rdx;NSKVODeallocate0x10c557ef3+378:xorl%ecx,%ecx0x10c557ef5+380:movq%r15,%rdi0x10c557ef8+383:callq0x10c558057;NSKVONotifyingSetMethodImplementation0x10c557efd+388:movq0x36519c(%rip),%rsi;"class"0x10c557f04+395:leaq0x433(%rip),%rdx;NSKVOClass0x10c557f0b+402:xorl%ecx,%ecx0x10c557f0d+404:movq%r15,%rdi0x10c557f10+407:callq0x10c558057;NSKVONotifyingSetMethodImplementation0x10c557f15+412:jmp0x10c557f84;+5230x10c557f17+414:cmpq$-0x1,0x382409(%rip);_+70x10c557f1f+422:jne0x10c557fbc;+5790x10c557f25+428:movq0x3823f4(%rip),%r14;_+435:movl$0x10,%esi0x10c557f31+440:movq%r14,%rdi0x10c557f34+443:callq0x10c7758e2;symbolstubfor:os_log_type_enabled0x10c557f39+448:testb%al,%al0x10c557f3b+450:je0x10c557f79;+5120x10c557f3d+452:movq%rsp,%rbx0x10c557f40+455:movq%rsp,%rax0x10c557f43+458:leaq-0x10(%rax),%r80x10c557f47+462:movq%r8,%rsp0x10c557f4a+465:movl$0x8200102,-0x10(%rax);imm=0x82001020x10c557f51+472:movq%r15,-0xc(%rax)0x10c557f55+476:leaq-0x63f5c(%rip),%rdi0x10c557f5c+483:leaq0x296c1d(%rip),%rcx;"KVOfailedtoallocateclasspairforname%s,automatickey-valueobservingwillnotworkforthisclass"0x10c557f63+490:movl$0x10,%edx0x10c557f68+495:movl$0xc,%r9d0x10c557f6e+501:movq%r14,%rsi0x10c557f71+504:callq0x10c7751aa;symbolstubfor:_os_log_error_impl0x10c557f76+509:movq%rbx,%rsp0x10c557f79+512:movq%r15,%rdi0x10c557f7c+515:callq0x10c7754ec;symbolstubfor:free0x10c557f81+520:xorl%r15d,%r15d0x10c557f84+523:movq0x2b4445(%rip),%rax;(void*)0x000000011012d070:__stack_chk_guard0x10c557f8b+530:movq(%rax),%rax0x10c557f8e+533:cmpq-0x28(%rbp),%rax0x10c557f92+537:jne0x10c557fd4;+6030x10c557f94+539:movq%r15,%rax0x10c557f97+542:leaq-0x20(%rbp),%rsp0x10c557f9b+546:popq%rbx0x10c557f9c+547:popq%r120x10c557f9e+549:popq%r140x10c557fa0+551:popq%r150x10c557fa2+553:popq%rbp0x10c557fa3+554:retq0x10c557fa4+555:leaq0x382385(%rip),%rdi;_+562:leaq0x2b9886(%rip),%rsi;__block_literal_+569:callq0x10c7753d8;symbolstubfor:dispatch_once0x10c557fb7+574:jmp0x10c557e96;+2850x10c557fbc+579:leaq0x382365(%rip),%rdi;_+586:leaq0x2b982e(%rip),%rsi;__block_literal_global0x10c557fca+593:callq0x10c7753d8;symbolstubfor:dispatch_once0x10c557fcf+598:jmp0x10c557f25;+4280x10c557fd4+603: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==_==_){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在中的声明为Class_Nullableobjc_allocateClassPair(Class_Nullablesuperclass,constchar*_Nonnullname,size_textraBytes),苹果对extraBytes参数的解释为“Thenumberofbytestoallocateforindexedivarsattheoftheclassandmetaclassobjects.”,这就是说当我们在通过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-KVOCrash

读到这里相信你对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(idobj,SELsel,intnumber)函数。当test实例收到setNum:消息时会先触发消息转发机制,然后SDMagicHook的消息调度系统会最终通过向test实例发送一个__sd_B_abcd_setNum:消息来实现对被Hook的原生方法的回调,而现在__sd_B_abcd_setNum:对应的实现函数正是void_NSSetIntValueAndNotify(idobj,SELsel,intnumber),所以__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会持有一个全局的字典_以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-CSwift最轻量级Hook方案

字节跳动分布式表格存储系统的演进

字节跳动自研强一致在线KV表格存储实践-上篇