写点什么

iOS 大解密:玄之又玄的 KVO

  • 2020-08-05
  • 本文字数:15055 字

    阅读完需:约 49 分钟

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)的调用顺序和次数的不同而各异,具体如下:


  1. 先调用 custom-KVO 再调用 native-KVO,native-KVO 和 custom-KVO 都运行正常

  2. 先调用 native-KVO 再调用 custom-KVO,custom-KVO 运行正常,native-KVO 会 crash

  3. 先调用 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 的本质以及我们分析和解决问题的思路。如果各位读者可以从中汲取一些灵感,那么这篇文章“倒也算是不负恩泽”,倘若大家可以将这篇文章介绍到的思路和方法用于处理自己开发中的遇到的各种疑难杂症“那便真真是极好的了”!


本文转载自公众号字节跳动技术团队(ID:toutiaotechblog)。


原文链接


https://mp.weixin.qq.com/s/0Yfb-FYorH5GZ3ZB6bMCUQ


2020-08-05 10:001975

评论

发布
暂无评论
发现更多内容

2023-05-28:为什么Redis单线程模型效率也能那么高?

福大大架构师每日一题

redis 福大大

经典智能合约之智能拍卖

timerring

区块链

CSS小技巧使用 font-variation 让文字起飞

南城FE

CSS 设计 前端开发 动画 字体

七年老程序员的三四月总结:三十岁、准备婚礼、三次分享

拭心

程序人生 总结思考

软件测试/测试开发丨学习笔记之Web自动化测试

测试人

程序员 软件测试 自动化测试 测试开发 web自动化

智能工厂 | 联合汽车电子有限公司汽车驱动科技上海智能工厂

工赋开发者社区

希尔伯特旅馆里,住着AI的某种真相

脑极体

AI 智能涌现

MySQL 启动apollo-adminservice 报错 Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'serverconf0_.Cluster' in 'field list

Andy

QUIC 协议:特性、应用场景及其对物联网/车联网的影响

EMQ映云科技

车联网 物联网 mqtt QUIC

GPT用于复杂代码生产所需要满足的必要条件

canonical

低代码 GPT GPT-4 可逆计算

ps神经滤镜是干什么的,神经滤镜的功能和作用

理理

ps神经滤镜 PS2023破解 Neural Filters下载 Photoshop2023 Mac

StarUML教程:CLI(命令行界面)功能的使用

理理

StarUML教程 CLI(命令行界面) UML软件建模器 StarUML for Mac StarUML Mac破解下载

大模型全情投入,低代码也越来越清晰

引迈信息

低代码 大模型 JNPF

深度学习进阶篇-国内预训练模型[5]:ERINE、ERNIE 3.0、ERNIE-的设计思路、模型结构、应用场景等详解

汀丶人工智能

人工智能 自然语言处理 深度学习 文心 ERNIE Transformer

利用springboot初始化机制三种实现策略模式的应用

Java你猿哥

Java spring Spring Boot 设计模式 ssm

经典智能合约案例之发红包

timerring

区块链

C语言编程—枚举

芯动大师

Git安装和配置教程:Windows/Mac/Linux三平台详细图文教程,带你一次性搞定Git环境

小万哥

git Linux 程序员 后端 C/C++

Redis和MySQL的爱恨情仇!

Java你猿哥

Java MySQL redis ssm 缓存雪崩

Typora for Mac:一款极简风格Markdown写作软件

理理

Typora破解 Mac软件 Markdown文本编辑器 Typora Mac下载

MySQL SQL脚本语句加上数据库存在判断

Andy

软件测试/测试开发丨学习笔记之用户端Web自动化测试

测试人

程序员 软件测试 自动化测试 测试开发 web自动化

iOS MachineLearning 系列(20)—— 训练生成CoreML模型

珲少

写给程序员的可逆计算理论辨析

canonical

开源 低代码 Docker 镜像 可逆计算 Nop平台

GitHub星标126K的京东「微服务进阶笔记」首次开源!好评如潮

Java你猿哥

Java 架构 微服务 Spring Cloud ssm

MySQL Idea 启动主程序 无法识别时区

Andy

文心一言 VS 讯飞星火 VS chatgpt (24)-- 算法导论4.2 6题

福大大架构师每日一题

福大大 ChatGPT 文心一言 讯飞星火

用Python做一个翻译器 | Python小知识

AIWeker

Python 人工智能 python小知识

Parallels Desktop如何退出账号?PD18虚拟机退出账号方法

理理

Parallels Desktop下载 PD18虚拟机破解 Parallels如何退出账号

mac分屏功能怎么用?mac分屏软件推荐 magnet

理理

Magnet中文版 Magnet破解版 mac分屏功能 Magnet Mac下载 苹果窗口管理软件

太牛了!在GitHub上“千金难求”的SpringBoot趣味实战课免费分享

Java你猿哥

Java spring Spring Boot ssm SpringBoot实战

iOS大解密:玄之又玄的KVO_开源_字节跳动技术团队_InfoQ精选文章