首页 » 通讯 » iOS大年夜解密:玄之又玄的KVO_操作_实例

iOS大年夜解密:玄之又玄的KVO_操作_实例

admin 2024-11-30 23:34:18 0

扫一扫用手机浏览

文章目录 [+]

大多数 iOS 开拓职员对 KVO 的认识只局限于 isa 指针交流这一层,而 KVO 的实现细节却鲜为人知。

如果自己也仿照 KVO 根本事理来实现一套类 KVO 操作且独立运行时会创造统统正常,然而一旦你的实现和系统的 KVO 实现同时浸染在同一个实例上那么各种各样诡异的 bug 和 crash 就会层出不穷。

iOS大年夜解密:玄之又玄的KVO_操作_实例 通讯

这究竟是为什么呢?此类问题到底该如何办理呢?接下来我们将考试测验从汇编层面来入手以层层揭开 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 又是如何实现的呢? 首先我们看下 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>: movq 0x28(%rbx), %rax 0x10bf27fee <+13>: movq 0x30(%rbx), %rsi 0x10bf27ff2 <+17>: movq (%rax), %rdi 0x10bf27ff5 <+20>: callq 0x10c1422b2 ; symbol stub for: class_getMethodImplementation 0x10bf27ffa <+25>: movq 0x20(%rbx), %rdi 0x10bf27ffe <+29>: movq 0x30(%rbx), %rsi 0x10bf28002 <+33>: movl 0x38(%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; SEL methodSel = block->captureVar3; IMP imp = class_getMethodImplementation(indexedIvars->originalClass); id obj = block->captureVar1; SEL sel = block->captureVar3; int num = 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 %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>: movq 0x2b463e(%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>: callq 0x10c55b452 ; NSKeyValueObservingAssertRegistrationLockHeld 0x10c557da0 <+39>: movq %r14, %rdi 0x10c557da3 <+42>: callq 0x10c7752b8 ; symbol stub for: class_getName 0x10c557da8 <+47>: movq %rax, %r12 0x10c557dab <+50>: movq %r12, %rdi 0x10c557dae <+53>: callq 0x10c775ba0 ; symbol stub for: strlen 0x10c557db3 <+58>: movq %rax, %rbx 0x10c557db6 <+61>: addq $0x10, %rbx 0x10c557dba <+65>: movq %rbx, %rdi 0x10c557dbd <+68>: callq 0x10c775666 ; symbol stub for: malloc 0x10c557dc2 <+73>: movq %rax, %r15 0x10c557dc5 <+76>: leaq 0x29d604(%rip), %rsi ; _NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix 0x10c557dcc <+83>: movq $-0x1, %rcx 0x10c557dd3 <+90>: movq %r15, %rdi 0x10c557dd6 <+93>: movq %rbx, %rdx 0x10c557dd9 <+96>: callq 0x10c77510e ; symbol stub for: __strlcpy_chk 0x10c557dde <+101>: movq $-0x1, %rcx 0x10c557de5 <+108>: movq %r15, %rdi 0x10c557de8 <+111>: movq %r12, %rsi 0x10c557deb <+114>: movq %rbx, %rdx 0x10c557dee <+117>: callq 0x10c775108 ; symbol stub for: __strlcat_chk 0x10c557df3 <+122>: movl $0x68, %edx 0x10c557df8 <+127>: movq %r14, %rdi 0x10c557dfb <+130>: movq %r15, %rsi 0x10c557dfe <+133>: callq 0x10c775762 ; symbol stub for: objc_allocateClassPair 0x10c557e03 <+138>: movq %rax, %rbx 0x10c557e06 <+141>: testq %rbx, %rbx 0x10c557e09 <+144>: je 0x10c557f17 ; <+414> 0x10c557e0f <+150>: movq %rbx, %rdi 0x10c557e12 <+153>: callq 0x10c775816 ; symbol stub for: objc_registerClassPair 0x10c557e17 <+158>: movq %r15, %rdi 0x10c557e1a <+161>: callq 0x10c7754ec ; symbol stub for: free 0x10c557e1f <+166>: movq %rbx, %rdi 0x10c557e22 <+169>: callq 0x10c77588e ; symbol stub for: object_getIndexedIvars 0x10c557e27 <+174>: movq %rax, %r15 0x10c557e2a <+177>: movq %r14, (%r15) 0x10c557e2d <+180>: movq %rbx, 0x8(%r15) 0x10c557e31 <+184>: movq 0x2b4748(%rip), %rdx ; (void )0x000000010d7fd1f8: kCFCopyStringSetCallBacks 0x10c557e38 <+191>: xorl %edi, %edi 0x10c557e3a <+193>: xorl %esi, %esi 0x10c557e3c <+195>: callq 0x10c774778 ; symbol stub for: CFSetCreateMutable 0x10c557e41 <+200>: movq %rax, 0x10(%r15) 0x10c557e45 <+204>: movq 0x2b49e4(%rip), %rcx ; (void )0x000000010d7f6bb8: kCFTypeDictionaryValueCallBacks 0x10c557e4c <+211>: xorl %edi, %edi 0x10c557e4e <+213>: xorl %esi, %esi 0x10c557e50 <+215>: xorl %edx, %edx 0x10c557e52 <+217>: callq 0x10c774454 ; symbol stub for: CFDictionaryCreateMutable 0x10c557e57 <+222>: movq %rax, 0x18(%r15) 0x10c557e5b <+226>: leaq -0x38(%rbp), %rbx 0x10c557e5f <+230>: movq %rbx, %rdi 0x10c557e62 <+233>: callq 0x10c775a3e ; symbol stub for: pthread_mutexattr_init 0x10c557e67 <+238>: movl $0x2, %esi 0x10c557e6c <+243>: movq %rbx, %rdi 0x10c557e6f <+246>: callq 0x10c775a44 ; symbol stub for: pthread_mutexattr_settype 0x10c557e74 <+251>: leaq 0x20(%r15), %rdi 0x10c557e78 <+255>: movq %rbx, %rsi 0x10c557e7b <+258>: callq 0x10c775a20 ; symbol stub for: pthread_mutex_init 0x10c557e80 <+263>: movq %rbx, %rdi 0x10c557e83 <+266>: callq 0x10c775a38 ; symbol stub for: pthread_mutexattr_destroy 0x10c557e88 <+271>: cmpq $-0x1, 0x3824a0(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken + 7 0x10c557e90 <+279>: jne 0x10c557fa4 ; <+555> 0x10c557e96 <+285>: movq (%r15), %rdi 0x10c557e99 <+288>: movq 0x366528(%rip), %rsi ; "willChangeValueForKey:" 0x10c557ea0 <+295>: callq 0x10c7752b2 ; symbol stub for: class_getMethodImplementation 0x10c557ea5 <+300>: movb $0x1, %cl 0x10c557ea7 <+302>: cmpq 0x38248a(%rip), %rax ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange 0x10c557eae <+309>: jne 0x10c557ec9 ; <+336> 0x10c557eb0 <+311>: movq (%r15), %rdi 0x10c557eb3 <+314>: movq 0x366526(%rip), %rsi ; "didChangeValueForKey:" 0x10c557eba <+321>: callq 0x10c7752b2 ; symbol stub for: class_getMethodImplementation 0x10c557ebf <+326>: cmpq 0x38247a(%rip), %rax ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange 0x10c557ec6 <+333>: setne %cl 0x10c557ec9 <+336>: movb %cl, 0x60(%r15) 0x10c557ecd <+340>: movq 0x36715c(%rip), %rsi ; "_isKVOA" 0x10c557ed4 <+347>: leaq 0x1ff(%rip), %rdx ; NSKVOIsAutonotifying 0x10c557edb <+354>: xorl %ecx, %ecx 0x10c557edd <+356>: movq %r15, %rdi 0x10c557ee0 <+359>: callq 0x10c558057 ; NSKVONotifyingSetMethodImplementation 0x10c557ee5 <+364>: movq 0x365154(%rip), %rsi ; "dealloc" 0x10c557eec <+371>: leaq 0x1ef(%rip), %rdx ; NSKVODeallocate 0x10c557ef3 <+378>: xorl %ecx, %ecx 0x10c557ef5 <+380>: movq %r15, %rdi 0x10c557ef8 <+383>: callq 0x10c558057 ; NSKVONotifyingSetMethodImplementation 0x10c557efd <+388>: movq 0x36519c(%rip), %rsi ; "class" 0x10c557f04 <+395>: leaq 0x433(%rip), %rdx ; NSKVOClass 0x10c557f0b <+402>: xorl %ecx, %ecx 0x10c557f0d <+404>: movq %r15, %rdi 0x10c557f10 <+407>: callq 0x10c558057 ; NSKVONotifyingSetMethodImplementation 0x10c557f15 <+412>: jmp 0x10c557f84 ; <+523> 0x10c557f17 <+414>: cmpq $-0x1, 0x382409(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog + 7 0x10c557f1f <+422>: jne 0x10c557fbc ; <+579> 0x10c557f25 <+428>: movq 0x3823f4(%rip), %r14 ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog 0x10c557f2c <+435>: movl $0x10, %esi 0x10c557f31 <+440>: movq %r14, %rdi 0x10c557f34 <+443>: callq 0x10c7758e2 ; symbol stub for: os_log_type_enabled 0x10c557f39 <+448>: testb %al, %al 0x10c557f3b <+450>: je 0x10c557f79 ; <+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>: leaq 0x296c1d(%rip), %rcx ; "KVO failed to allocate class pair for name %s, automatic key-value observing will not work for this class" 0x10c557f63 <+490>: movl $0x10, %edx 0x10c557f68 <+495>: movl $0xc, %r9d 0x10c557f6e <+501>: movq %r14, %rsi 0x10c557f71 <+504>: callq 0x10c7751aa ; symbol stub for: _os_log_error_impl 0x10c557f76 <+509>: movq %rbx, %rsp 0x10c557f79 <+512>: movq %r15, %rdi 0x10c557f7c <+515>: callq 0x10c7754ec ; symbol stub for: free 0x10c557f81 <+520>: xorl %r15d, %r15d 0x10c557f84 <+523>: movq 0x2b4445(%rip), %rax ; (void )0x000000011012d070: __stack_chk_guard 0x10c557f8b <+530>: movq (%rax), %rax 0x10c557f8e <+533>: cmpq -0x28(%rbp), %rax 0x10c557f92 <+537>: jne 0x10c557fd4 ; <+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>: leaq 0x382385(%rip), %rdi ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce 0x10c557fab <+562>: leaq 0x2b9886(%rip), %rsi ; __block_literal_global.8 0x10c557fb2 <+569>: callq 0x10c7753d8 ; symbol stub for: dispatch_once 0x10c557fb7 <+574>: jmp 0x10c557e96 ; <+285> 0x10c557fbc <+579>: leaq 0x382365(%rip), %rdi ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken 0x10c557fc3 <+586>: leaq 0x2b982e(%rip), %rsi ; __block_literal_global 0x10c557fca <+593>: callq 0x10c7753d8 ; symbol stub for: dispatch_once 0x10c557fcf <+598>: jmp 0x10c557f25 ; <+428> 0x10c557fd4 <+603>: callq 0x10c775102 ; symbol stub for: __stack_chk_fail

翻译成伪代码如下:

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;Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) { const char clsName = class_getName(originalClass); size_t len = strlen(clsName); len += 0x10; char newClsName = malloc(len); const char prefix = "NSKVONotifying_"; __strlcpy_chk(newClsName, prefix, len); __strlcat_chk(newClsName, clsName, len, -1); Class newCls = objc_allocateClassPair(originalClass, newClsName, 0x68); if (newCls) { objc_registerClassPair(newCls); SDTestKVOClassIndexedIvars indexedIvars = object_getIndexedIvars(newCls); indexedIvars->originalClass = originalClass; indexedIvars->KVOClass = newCls; CFMutableSetRef mset = CFSetCreateMutable(nil, 0, kCFCopyStringSetCallBacks); indexedIvars->mset = mset; CFMutableDictionaryRef mdict = CFDictionaryCreateMutable(nil, 0, nil, kCFTypeDictionaryValueCallBacks); indexedIvars->mdict = mdict; pthread_mutex_init(indexedIvars->lock); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ bool flag = true; IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(willChangeValueForKey:)); IMP didChangeValueForKeyImp = 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 { // 缺点处理过程省略...... return nil } return newCls;}

通过_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 字节的空间,其详细内存布局和用场我用一个构造体描述如下:

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;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直推)。

欢迎关注字节跳动技能团队

标签:

相关文章

技能|电脑无法通电怎么解决_戴尔_电脑

如果按下电源按钮后戴尔打算机无法打开,不通电,请按照以下步骤打消故障。视频加载中...01检讨电源线、互换适配器与外设首先检讨电源...

通讯 2025-01-24 阅读1 评论0