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)的調用順序和次數的不同而各異,具體如下:

  • 先調用 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細節的資料請關註WalkonNet其它相關文章!

推薦閱讀: