iOS开发KVO实现细节解密

目录

导读

1. 缘起 Aspects

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

1.2 SDMagicHook 解决了 Aspects 未能解决的 KVO 冲突难题

2. 从汇编层面探索 KVO 本质

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

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

总结

导读

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

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

这究竟是为什么呢?此类问题到底该如何解决呢?接下来我们将尝试从汇编层面来入手以层层揭开 KVO 的神秘面纱......

1. 缘起 Aspects

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

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

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

https://github.com/larksuite/SDMagicHook/issues/3

1.2 SDMagicHook 解决了 Aspects 未能解决的 KVO 冲突难题

在 Aspects 的 readme 中我还注意到了这样一条关于 KVO 兼容问题的描述:

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 的本质以及我们分析和解决问题的思路。

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

推荐阅读

    计算机主板BIOS设置详细-BIOS知识

    计算机主板BIOS设置详细-BIOS知识,,什么是电脑BIOS,一般电脑主板已经设置完毕后,电脑就开始按del键进入BIOS。系统启动BIOS,即微机的基本输入

    6s 32G能升级到ios14吗

    6s 32G能升级到ios14吗,手机,系统,6s 32G能升级到ios14吗可以,但是ios14更新以后会占用10左右储存,还有系统没有完全汉化,如果没接受,也就可以

    三常见BIOS故障排除解决方案

    三常见BIOS故障排除解决方案,,笔记本电脑如何长时间出现黑屏为什么为什么如何删除和修改旧IBM笔记本电脑BIOS设置中的密码我想你会与这些

    联想bios设置图解|联想bios设置方法

    联想bios设置图解|联想bios设置方法,,联想bios设置方法1.首先我们打开电脑,当开机标识出现或者电脑开机时,连续使用键盘“DEL”进入BIOS设置

    coc进度转电脑ios|coc快速升级

    coc进度转电脑ios|coc快速升级,,1. coc快速升级1、 首先,你要知道并学会“刷墙”。它的意思是尽量给城墙多升升级,免得你防御很强,但

    dellu盘启动设置|dellu盘启动bios设置

    dellu盘启动设置|dellu盘启动bios设置,,1. dellu盘启动bios设置1、插入U盘,开机按F2进BIOS,也可以先按F12进这个界面,然后选择BIOS Setup回车

    串口硬盘bios设置|BIOS设置硬盘

    串口硬盘bios设置|BIOS设置硬盘,,1. BIOS设置硬盘接好SATA硬盘后,开机,按Del键进入CMOS设置界面;按键盘上的TAB键和方向键,进入integrated

    bios设置电源管理|Bios电源设置

    bios设置电源管理|Bios电源设置,,1. Bios电源设置电脑开机显示没有检测到开机设备,这是因为计算机的硬盘损坏导致的,因为计算机在开机自检的