日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

iOS開發(fā)KVO實現(xiàn)細節(jié)解密_IOS

作者:高少東 ? 更新時間: 2022-10-03 編程語言

導讀

大多數(shù) iOS 開發(fā)人員對 KVO 的認識只局限于 isa 指針交換這一層,而 KVO 的實現(xiàn)細節(jié)卻鮮為人知。

如果自己也仿照 KVO 基礎原理來實現(xiàn)一套類 KVO 操作且獨立運行時會發(fā)現(xiàn)一切正常,然而一旦你的實現(xiàn)和系統(tǒng)的 KVO 實現(xiàn)同時作用在同一個實例上那么各種各樣詭異的 bug 和 crash 就會層出不窮。

這究竟是為什么呢?此類問題到底該如何解決呢?接下來我們將嘗試從匯編層面來入手以層層揭開 KVO 的神秘面紗......

1. 緣起 Aspects

SDMagicHook 開源之后很多小伙伴在問“ SDMagicHook 和 Aspects 的區(qū)別是什么?”,我在 GitHub 上找到 Aspects 了解之后發(fā)現(xiàn) Aspects 也是以 isa 交換為基礎原理進行的 hook 操作,但是兩者在具體實現(xiàn)和 API 設計上也有一些區(qū)別,另外 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 會不會有同樣的問題呢?測試了一下發(fā)現(xiàn) SDMagicHook 果然也中招了,而且其實此類問題的實際情況要比 Aspects 作者描述的更為復雜和詭異,問題的具體表現(xiàn)會隨著系統(tǒng) KVO(以下簡稱 native-KVO)和自己實現(xiàn)的類 KVO(custom-KVO)的調用順序和次數(shù)的不同而各異,具體如下:

  • 先調用 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 已經解決了上面提到的各類問題,具體的實現(xiàn)方案我將在下文中詳細介紹。

2. 從匯編層面探索 KVO 本質

想要弄明白這個問題首先需要研究清楚系統(tǒng)的 KVO 到底是如何實現(xiàn)的,而系統(tǒng)的 KVO 實現(xiàn)又相當復雜,我們該從哪里入手呢?

想要弄清楚這個問題,我們首先需要了解下當對被 KVO 觀察的目標屬性進行賦值操作時到底發(fā)生了什么。這里我們以自建的 Test 類為例來說明,我們對 Test 類實例的 num 屬性進行 KVO 操作:

當我們給 num 賦值時,可以看到斷點命中了 KVO 類自定義的 setNum:的實現(xiàn)即_NSSetIntValueAndNotify 函數(shù)

那么_NSSetIntValueAndNotify 的內部實現(xiàn)是怎樣的呢?我們可以從匯編代碼中發(fā)現(xiàn)一些蛛絲馬跡:

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);
????//?獲取類實例關聯(lián)的信息
????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 即開發(fā)者自己重寫實現(xiàn)過 willChangeValueForKey:

或者 didChangeValueForKey:方法的話就直接以 class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number)的方式實現(xiàn)對被觀察的原方法的調用,否則就用默認實現(xiàn)為 NSSetIntValueAndNotify_block_invoke 的棧 block 并捕獲 indexedIvars、被 KVO 觀察的實例、被觀察屬性對應的 SEL、賦值參數(shù)等所有必要參數(shù)并將這個 block 作為參數(shù)傳遞給 [obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock]調用。

看到這里你或許會有個疑問:

偽代碼中通過 object_getIndexedIvars(cls)獲取到的 indexedIvars 是什么信息呢?

block.invoke = ___ NSSetIntValueAndNotify_block_invoke 又是如何實現(xiàn)的呢?

首先我們看下 NSSetIntValueAndNotify_block_invoke 的匯編實現(xiàn):

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 的內部實現(xiàn)其實就是從 KVO 類的 indexedIvars 里取到原始類,然后根據 sel 從原始類中取出原始的方法實現(xiàn)來執(zhí)行并最終完成了一次 KVO 調用。我們發(fā)現(xiàn)整個 KVO 運作過程中 KVO 類的 indexedIvars 是一個貫穿 KVO 流程始末的關鍵數(shù)據,那么這個 indexedIvars 是何時生成的呢?

indexedIvars 里又包含哪些數(shù)據呢?想要弄清楚這個問題,我們就必須從 KVO 的源頭看起,我們知道既然 KVO 要用到 isa 交換那么最終肯定要調用到 object_setClass 方法,這里我們不妨以 object_setClass 函數(shù)為線索,通過設置條件符號斷點來追蹤 object_setClass 的調用,lldb 調試截圖如下:

斷點到 object_setClass 之后,我們再驗證看下寄存器 rdi、rsi 里面的參數(shù)打印出來分別是

<Test: 0x600003df01b0>、NSKVONotifying_Test

不錯,我們現(xiàn)在已經成功定位到 KVO 的 isa 交換現(xiàn)場了,然而為了找到 KVO 類的生成的地方我們還需要沿著調用棧向前回溯,最終我們定位到 KVO 類的生成函數(shù)_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 的這段偽代碼你會發(fā)現(xiàn)我們之前頻繁提到 indexedIvars 原來就是在這里初始化生成的。

objc_allocateClassPair 在 runtime.h 中的聲明為 Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) ,蘋果對 extraBytes 參數(shù)的解釋為“The number of bytes to allocate for indexed ivars at the end of the class and metaclass objects.”

這就是說當我們在通過 objc_allocateClassPair 來生成一個新的類時可以通過指定 extraBytes 來為此類開辟額外的空間用于存儲一些數(shù)據。系統(tǒng)在生成 KVO 類時會額外分配 0x68 字節(jié)的空間,其具體內存布局和用途我用一個結構體描述如下:

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 實現(xiàn)細節(jié)有了大致的了解,然后我們再回到最初的問題,為什么“先調用 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 字節(jié)的空間用于存儲 KVO 關鍵信息,那么當我們向 test 發(fā)送 setNum:消息然后 setNum:方法調用 super 實現(xiàn)走到了 KVO 的_NSSetIntValueAndNotify 方法時還按照 SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(cls)方式來獲取 KVO 信息并嘗試獲取從中獲取數(shù)據時發(fā)生異常導致 crash。

找到問題的根源之后我們就可以見招拆招,我們可以仿照 native-KVO 的做法在生成 SD_NSKVONotifying_Test_abcd 也額外分配 0x68 自己的空間,然后當要進行 custom-KVO 操作時將 NSKVONotifying_Test 的 indexedIvars 拷貝一份到 SD_NSKVONotifying_Test_abcd 即可,代碼實現(xiàn)如下:

一般情況下在 native-KVO 的基礎上再做 custom-KVO 的話拷貝完 native-KVO 類的 indexedIvars 到 custom-KVO 類上就可以了,而我們的 SDMagicHook 只做到這些還不夠,因為 SDMagicHook 在生成的新類上以消息轉發(fā)的形式來調度方法,這樣一來問題瞬間就變得更為復雜。舉例說明如下:

  • 由于用到消息轉發(fā),我們會將 SD_NSKVONotifying_Test_abcd 的setNum:對應的實現(xiàn)指向_objc_msgForward,然后生成一個新的 SEL__sd_B_abcd_setNum:來指向其子類的原生實現(xiàn),在我們這個例子中就是 NSKVONotifying_TestsetNum:實現(xiàn)的即void _NSSetIntValueAndNotify(id obj, SEL sel, int number)函數(shù)。
  • 當 test 實例收到setNum:消息時會先觸發(fā)消息轉發(fā)機制,然后 SDMagicHook 的消息調度系統(tǒng)會最終通過向 test 實例發(fā)送一個__sd_B_abcd_setNum:消息來實現(xiàn)對被 Hook 的原生方法的回調,而現(xiàn)在__sd_B_abcd_setNum:對應的實現(xiàn)函數(shù)正是void _NSSetIntValueAndNotify(id obj, SEL sel, int number),所以__sd_B_abcd_setNum:就會被作為 sel 參數(shù)傳遞到_NSSetIntValueAndNotify函數(shù)。
  • 然后當_NSSetIntValueAndNotify函數(shù)內部嘗試從 indexedIvars 拿到原始類 Test 然后從 Test 上查找__sd_B_abcd_setNum:對應的方法并調用時由于找不到對應函數(shù)實現(xiàn)而發(fā)生 crash。為解決這個問題,我們還需要為 Test 類新增一個__sd_B_abcd_setNum:方法并將其實現(xiàn)指向setNum:的實現(xiàn),代碼如下:

至此,“先調用 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”。

為什么會出現(xiàn)這個問題呢?這次我們依然以 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 操作就會驚奇地發(fā)現(xiàn) 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 數(shù)據

2.hook 攔截系統(tǒng)的 setclass 操作。然后仔細一想方案 1 是不可取的,因為 NSKVONotifying_Test 的相關數(shù)據是被所有 Test 類的實例在進行 KVO 操作時共享的,任何改動都有可能對 Test 類實例的 KVO 產生全局影響。

所以,我們就需要借助 FishHook 來 hook 系統(tǒng)的 object_setclass 函數(shù),當系統(tǒng)以 NSKVONotifying_Test 為參數(shù)對一個實例進行 setclass 操作時,我們檢查如果當前的 isa 指針是 SD_NSKVONotifying_Test_abcd 且 SD_NSKVONotifying_Test_abcd 繼承自系統(tǒng)的 NSKVONotifying_Test 時就跳過此次 setclass 操作。

但是這樣做還不夠,因為 custom-KVO 采用了特殊的消息轉發(fā)機制來調度被 hook 的方法,如果先進行 custom-KVO 然后在進行 native-KVO 就會導致被觀察屬性被重復調用。

所以,我們在對一個實例進行首次 custom-KVO 操作之前先進行 native-KVO,這樣一來就可以保證我們的 custom-KVO 的方法調度正常工作了。

代碼如下:

總結

KVO 的本質其實就是基于被觀察的實例的 isa 生成一個新的類并在這個類的 extra 空間中存放各種和 KVO 操作相關的關鍵數(shù)據,然后這個新的類以一個中間人的角色借助 extra 空間中存放各種數(shù)據完成復雜的方法調度。

系統(tǒng)的 KVO 實現(xiàn)比較復雜,很多函數(shù)的調用層次也比較深,我們一開始不妨從整個函數(shù)調用棧的末端層層向前梳理出主要的操作路徑,在對 KVO 操作有個大致的了解之后再從全局的角度正向全面分析各個流程和細節(jié)。我們正是借助這種方式實現(xiàn)了對 KVO 的快速了解和認識。

至此,一個良好兼容 native-KVO 的 custom-KVO 就全部完成了。回頭來看,這個解決方案其實還是過于 tricky 了,不過這也只能是在 iOS 系統(tǒng)的各種限制下的無奈的選擇了。我們不提倡隨意使用類似的 tricky 操作,更多是想要通過這個例子向大家介紹一下 KVO 的本質以及我們分析和解決問題的思路。

原文鏈接:https://mp.weixin.qq.com/s/0Yfb-FYorH5GZ3ZB6bMCUQ

欄目分類
最近更新