導讀:

大多數 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>: callq0x10e7cc882; symbol stubfor: object_getClass
0x10e5b0fe1<+31>: movq %rax, %rdi
0x10e5b0fe4<+34>: callq0x10e7cc88e; symbol stubfor: object_getIndexedIvars
0x10e5b0fe9<+39>: movq %rax, %rbx
0x10e5b0fec<+42>: leaq0x20(%rbx), %r14
0x10e5b0ff0<+46>: movq %r14, %rdi
0x10e5b0ff3<+49>: callq0x10e7cca26; symbol stubfor: pthread_mutex_lock
0x10e5b0ff8<+54>: movq0x18(%rbx), %rdi
0x10e5b0ffc<+58>: movq %r15, %rsi
0x10e5b0fff<+61>: callq0x10e7cb472; symbol stubfor: CFDictionaryGetValue
0x10e5b1004<+66>: movq0x36329d(%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>: callq0x10e7cca32; symbol stubfor: pthread_mutex_unlock
0x10e5b1021<+95>: cmpb $0x0,0x60(%rbx)
0x10e5b1025<+99>: je0x10e5b1066; <+164>
0x10e5b1027<+101>: movq0x36439a(%rip), %rsi ;"willChangeValueForKey:"
0x10e5b102e<+108>: movq0x2b2843(%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>: callq0x10e7cc2b2; symbol stubfor: class_getMethodImplementation
0x10e5b1049<+135>: movq %r13, %rdi
0x10e5b104c<+138>: movq %r15, %rsi
0x10e5b104f<+141>: movl-0x2c(%rbp), %edx
0x10e5b1052<+144>: callq *%rax
0x10e5b1054<+146>: movq0x364385(%rip), %rsi ;"didChangeValueForKey:"
0x10e5b105b<+153>: movq %r13, %rdi
0x10e5b105e<+156>: movq %r12, %rdx
0x10e5b1061<+159>: callq *%r14
0x10e5b1064<+162>: jmp0x10e5b10be; <+252>
0x10e5b1066<+164>: movq0x2b22eb(%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>: leaq0xf5d(%rip), %rax ; ___NSSetIntValueAndNotify_block_invoke
0x10e5b1084<+194>: movq %rax,0x10(%r9)
0x10e5b1088<+198>: leaq0x2b7929(%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>: movq0x364fab(%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>: movq0x362f73(%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

上面這段彙編代碼翻譯爲僞代碼大致如下:

複製代碼

typedefstruct{
Class originalClass;// offset 0x0
Class KVOClass;// offset 0x8
CFMutableSetRefmset;// offset 0x10
CFMutableDictionaryRefmdict;// 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
boolflag;// offset 0x60
} SDTestKVOClassIndexedIvars;

typedefstruct{
Class isa;// offset 0x0
intflags;// offset 0x8
intreserved;
IMP invoke;// offset 0x10
void*descriptor;// offset 0x18
void*captureVar1;// offset 0x20
void*captureVar2;// offset 0x28
void*captureVar3;// offset 0x30
intcaptureVar4;// offset 0x38

} SDTestStackBlock;

void_NSSetIntValueAndNotify(idobj, SEL sel,intnumber) {
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(*)(idobj, SEL sel,intnumber))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:nilkey:nilusingBlock:&SDTestStackBlock];
}
}

這段代碼的大致意思是說首先通過 object_getIndexedIvars(cls) 獲取到 KVO 類的 indexedIvars,如果 indexedIvars->flag 爲 true 即開發者自己重寫實現過 willChangeValueForKey: 或者 didChangeValueForKey: 方法的話就直接以 class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number) 的方式實現對被觀察的原方法的調用,否則就用默認實現爲 NSSetIntValueAndNotify_block_invoke 的棧 block 並捕獲 indexedIvars、被 KVO 觀察的實例、被觀察屬性對應的 SEL、賦值參數等所有必要參數並將這個 block 作爲參數傳遞給 [obj _changeValueForKey: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>: movq0x28(%rbx), %rax
0x10bf27fee<+13>: movq0x30(%rbx), %rsi
0x10bf27ff2<+17>: movq (%rax), %rdi
0x10bf27ff5<+20>: callq0x10c1422b2; symbol stubfor: class_getMethodImplementation
0x10bf27ffa<+25>: movq0x20(%rbx), %rdi
0x10bf27ffe<+29>: movq0x30(%rbx), %rsi
0x10bf28002<+33>: movl0x38(%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;
SELmethodSel=block->captureVar3;
IMPimp= class_getMethodImplementation(indexedIvars->originalClass);
idobj =block->captureVar1;
SELsel=block->captureVar3;
intnum =block->captureVar4;
imp(obj, sel, num);
}

這個 block 的內部實現其實就是從 KVO 類的 indexedIvars 裏取到原始類,然後根據 sel 從原始類中取出原始的方法實現來執行並最終完成了一次 KVO 調用。我們發現整個 KVO 運作過程中 KVO 類的 indexedIvars 是一個貫穿 KVO 流程始末的關鍵數據,那麼這個 indexedIvars 是何時生成的呢?indexedIvars 裏又包含哪些數據呢?想要弄清楚這個問題,我們就必須從 KVO 的源頭看起,我們知道既然 KVO 要用到 isa 交換那麼最終肯定要調用到 object_setClass 方法,這裏我們不妨以 object_setClass 函數爲線索,通過設置條件符號斷點來追蹤 object_setClass 的調用,lldb 調試截圖如下:

斷點到 object_setClass 之後,我們再驗證看下寄存器 rdi、rsi 裏面的參數打印出來分別是 <Test: 0x600003df01b0>、NSKVONotifying_Test

不錯,我們現在已經成功定位到 KVO 的 isa 交換現場了,然而爲了找到 KVO 類的生成的地方我們還需要沿着調用棧向前回溯,最終我們定位到 KVO 類的生成函數 _NSKVONotifyingCreateInfoWithOriginalClass,其彙編代碼如下:

複製代碼

Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:
->0x10c557d79<+0>: pushq %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>: movq0x2b463e(%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>: callq0x10c55b452; NSKeyValueObservingAssertRegistrationLockHeld
0x10c557da0<+39>: movq %r14, %rdi
0x10c557da3<+42>: callq0x10c7752b8; symbol stubfor: class_getName
0x10c557da8<+47>: movq %rax, %r12
0x10c557dab<+50>: movq %r12, %rdi
0x10c557dae<+53>: callq0x10c775ba0; symbol stubfor: strlen
0x10c557db3<+58>: movq %rax, %rbx
0x10c557db6<+61>: addq $0x10, %rbx
0x10c557dba<+65>: movq %rbx, %rdi
0x10c557dbd<+68>: callq0x10c775666; symbol stubfor: malloc
0x10c557dc2<+73>: movq %rax, %r15
0x10c557dc5<+76>: leaq0x29d604(%rip), %rsi ; _NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix
0x10c557dcc<+83>: movq $-0x1, %rcx
0x10c557dd3<+90>: movq %r15, %rdi
0x10c557dd6<+93>: movq %rbx, %rdx
0x10c557dd9<+96>: callq0x10c77510e; symbol stubfor: __strlcpy_chk
0x10c557dde<+101>: movq $-0x1, %rcx
0x10c557de5<+108>: movq %r15, %rdi
0x10c557de8<+111>: movq %r12, %rsi
0x10c557deb<+114>: movq %rbx, %rdx
0x10c557dee<+117>: callq0x10c775108; symbol stubfor: __strlcat_chk
0x10c557df3<+122>: movl $0x68, %edx
0x10c557df8<+127>: movq %r14, %rdi
0x10c557dfb<+130>: movq %r15, %rsi
0x10c557dfe<+133>: callq0x10c775762; symbol stubfor: objc_allocateClassPair
0x10c557e03<+138>: movq %rax, %rbx
0x10c557e06<+141>: testq %rbx, %rbx
0x10c557e09<+144>: je0x10c557f17; <+414>
0x10c557e0f<+150>: movq %rbx, %rdi
0x10c557e12<+153>: callq0x10c775816; symbol stubfor: objc_registerClassPair
0x10c557e17<+158>: movq %r15, %rdi
0x10c557e1a<+161>: callq0x10c7754ec; symbol stubfor: free
0x10c557e1f<+166>: movq %rbx, %rdi
0x10c557e22<+169>: callq0x10c77588e; symbol stubfor: object_getIndexedIvars
0x10c557e27<+174>: movq %rax, %r15
0x10c557e2a<+177>: movq %r14, (%r15)
0x10c557e2d<+180>: movq %rbx,0x8(%r15)
0x10c557e31<+184>: movq0x2b4748(%rip), %rdx ; (void*)0x000000010d7fd1f8: kCFCopyStringSetCallBacks
0x10c557e38<+191>: xorl %edi, %edi
0x10c557e3a<+193>: xorl %esi, %esi
0x10c557e3c<+195>: callq0x10c774778; symbol stubfor: CFSetCreateMutable
0x10c557e41<+200>: movq %rax,0x10(%r15)
0x10c557e45<+204>: movq0x2b49e4(%rip), %rcx ; (void*)0x000000010d7f6bb8: kCFTypeDictionaryValueCallBacks
0x10c557e4c<+211>: xorl %edi, %edi
0x10c557e4e<+213>: xorl %esi, %esi
0x10c557e50<+215>: xorl %edx, %edx
0x10c557e52<+217>: callq0x10c774454; symbol stubfor: CFDictionaryCreateMutable
0x10c557e57<+222>: movq %rax,0x18(%r15)
0x10c557e5b<+226>: leaq-0x38(%rbp), %rbx
0x10c557e5f<+230>: movq %rbx, %rdi
0x10c557e62<+233>: callq0x10c775a3e; symbol stubfor: pthread_mutexattr_init
0x10c557e67<+238>: movl $0x2, %esi
0x10c557e6c<+243>: movq %rbx, %rdi
0x10c557e6f<+246>: callq0x10c775a44; symbol stubfor: pthread_mutexattr_settype
0x10c557e74<+251>: leaq0x20(%r15), %rdi
0x10c557e78<+255>: movq %rbx, %rsi
0x10c557e7b<+258>: callq0x10c775a20; symbol stubfor: pthread_mutex_init
0x10c557e80<+263>: movq %rbx, %rdi
0x10c557e83<+266>: callq0x10c775a38; symbol stubfor: pthread_mutexattr_destroy
0x10c557e88<+271>: cmpq $-0x1,0x3824a0(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken +7
0x10c557e90<+279>: jne0x10c557fa4; <+555>
0x10c557e96<+285>: movq (%r15), %rdi
0x10c557e99<+288>: movq0x366528(%rip), %rsi ;"willChangeValueForKey:"
0x10c557ea0<+295>: callq0x10c7752b2; symbol stubfor: class_getMethodImplementation
0x10c557ea5<+300>: movb $0x1, %cl
0x10c557ea7<+302>: cmpq0x38248a(%rip), %rax ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange
0x10c557eae<+309>: jne0x10c557ec9; <+336>
0x10c557eb0<+311>: movq (%r15), %rdi
0x10c557eb3<+314>: movq0x366526(%rip), %rsi ;"didChangeValueForKey:"
0x10c557eba<+321>: callq0x10c7752b2; symbol stubfor: class_getMethodImplementation
0x10c557ebf<+326>: cmpq0x38247a(%rip), %rax ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange
0x10c557ec6<+333>: setne %cl
0x10c557ec9<+336>: movb %cl,0x60(%r15)
0x10c557ecd<+340>: movq0x36715c(%rip), %rsi ;"_isKVOA"
0x10c557ed4<+347>: leaq0x1ff(%rip), %rdx ; NSKVOIsAutonotifying
0x10c557edb<+354>: xorl %ecx, %ecx
0x10c557edd<+356>: movq %r15, %rdi
0x10c557ee0<+359>: callq0x10c558057; NSKVONotifyingSetMethodImplementation
0x10c557ee5<+364>: movq0x365154(%rip), %rsi ;"dealloc"
0x10c557eec<+371>: leaq0x1ef(%rip), %rdx ; NSKVODeallocate
0x10c557ef3<+378>: xorl %ecx, %ecx
0x10c557ef5<+380>: movq %r15, %rdi
0x10c557ef8<+383>: callq0x10c558057; NSKVONotifyingSetMethodImplementation
0x10c557efd<+388>: movq0x36519c(%rip), %rsi ;"class"
0x10c557f04<+395>: leaq0x433(%rip), %rdx ; NSKVOClass
0x10c557f0b<+402>: xorl %ecx, %ecx
0x10c557f0d<+404>: movq %r15, %rdi
0x10c557f10<+407>: callq0x10c558057; NSKVONotifyingSetMethodImplementation
0x10c557f15<+412>: jmp0x10c557f84; <+523>
0x10c557f17<+414>: cmpq $-0x1,0x382409(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog +7
0x10c557f1f<+422>: jne0x10c557fbc; <+579>
0x10c557f25<+428>: movq0x3823f4(%rip), %r14 ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog
0x10c557f2c<+435>: movl $0x10, %esi
0x10c557f31<+440>: movq %r14, %rdi
0x10c557f34<+443>: callq0x10c7758e2; symbol stubfor: os_log_type_enabled
0x10c557f39<+448>: testb %al, %al
0x10c557f3b<+450>: je0x10c557f79; <+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>: leaq0x296c1d(%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>: callq0x10c7751aa; symbol stubfor: _os_log_error_impl
0x10c557f76<+509>: movq %rbx, %rsp
0x10c557f79<+512>: movq %r15, %rdi
0x10c557f7c<+515>: callq0x10c7754ec; symbol stubfor: free
0x10c557f81<+520>: xorl %r15d, %r15d
0x10c557f84<+523>: movq0x2b4445(%rip), %rax ; (void*)0x000000011012d070: __stack_chk_guard
0x10c557f8b<+530>: movq (%rax), %rax
0x10c557f8e<+533>: cmpq-0x28(%rbp), %rax
0x10c557f92<+537>: jne0x10c557fd4; <+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>: leaq0x382385(%rip), %rdi ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce
0x10c557fab<+562>: leaq0x2b9886(%rip), %rsi ; __block_literal_global.8
0x10c557fb2<+569>: callq0x10c7753d8; symbol stubfor: dispatch_once
0x10c557fb7<+574>: jmp0x10c557e96; <+285>
0x10c557fbc<+579>: leaq0x382365(%rip), %rdi ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken
0x10c557fc3<+586>: leaq0x2b982e(%rip), %rsi ; __block_literal_global
0x10c557fca<+593>: callq0x10c7753d8; symbol stubfor: dispatch_once
0x10c557fcf<+598>: jmp0x10c557f25; <+428>
0x10c557fd4<+603>: callq0x10c775102; symbol stubfor: __stack_chk_fail

翻譯成僞代碼如下:

複製代碼

typedefstruct{
Class originalClass;// offset 0x0
Class KVOClass;// offset 0x8
CFMutableSetRefmset;// offset 0x10
CFMutableDictionaryRefmdict;// 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
boolflag;// offset 0x60
} SDTestKVOClassIndexedIvars;


Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) {
constchar*clsName = class_getName(originalClass);
size_t len = strlen(clsName);
len +=0x10;
char*newClsName = malloc(len);
constchar*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;
CFMutableSetRefmset =CFSetCreateMutable(nil,0, kCFCopyStringSetCallBacks);
indexedIvars->mset = mset;
CFMutableDictionaryRefmdict =CFDictionaryCreateMutable(nil,0,nil, kCFTypeDictionaryValueCallBacks);
indexedIvars->mdict = mdict;
pthread_mutex_init(indexedIvars->lock);
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
boolflag =true;
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{
// 錯誤處理過程省略......
returnnil
}
returnnewCls;
}

通過 _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 字節的空間,其具體內存佈局和用途我用一個結構體描述如下:

複製代碼

typedefstruct{
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
boolflag;// 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_Test setNum: 實現的即 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

相關文章