iOS開發(fā)KVO實(shí)現(xiàn)細(xì)節(jié)解密
導(dǎo)讀
大多數(shù) iOS 開發(fā)人員對(duì) KVO 的認(rèn)識(shí)只局限于 isa 指針交換這一層,而 KVO 的實(shí)現(xiàn)細(xì)節(jié)卻鮮為人知。
如果自己也仿照 KVO 基礎(chǔ)原理來(lái)實(shí)現(xiàn)一套類 KVO 操作且獨(dú)立運(yùn)行時(shí)會(huì)發(fā)現(xiàn)一切正常,然而一旦你的實(shí)現(xiàn)和系統(tǒng)的 KVO 實(shí)現(xiàn)同時(shí)作用在同一個(gè)實(shí)例上那么各種各樣詭異的 bug 和 crash 就會(huì)層出不窮。
這究竟是為什么呢?此類問題到底該如何解決呢?接下來(lái)我們將嘗試從匯編層面來(lái)入手以層層揭開 KVO 的神秘面紗......
1. 緣起 Aspects
SDMagicHook 開源之后很多小伙伴在問“ SDMagicHook 和 Aspects 的區(qū)別是什么?”,我在 GitHub 上找到 Aspects 了解之后發(fā)現(xiàn) Aspects 也是以 isa 交換為基礎(chǔ)原理進(jìn)行的 hook 操作,但是兩者在具體實(shí)現(xiàn)和 API 設(shè)計(jì)上也有一些區(qū)別,另外 SDMagicHook 還解決了 Aspects 未能解決的 KVO 沖突難題。
1.1 SDMagicHook 的 API 設(shè)計(jì)更加友好靈活
SDMagicHook 和 Aspects 的具體異同分析見:
https://github.com/larksuite/SDMagicHook/issues/3
1.2 SDMagicHook 解決了 Aspects 未能解決的 KVO 沖突難題
在 Aspects 的 readme 中我還注意到了這樣一條關(guān)于 KVO 兼容問題的描述:
SDMagicHook 會(huì)不會(huì)有同樣的問題呢?測(cè)試了一下發(fā)現(xiàn) SDMagicHook 果然也中招了,而且其實(shí)此類問題的實(shí)際情況要比 Aspects 作者描述的更為復(fù)雜和詭異,問題的具體表現(xiàn)會(huì)隨著系統(tǒng) KVO(以下簡(jiǎn)稱 native-KVO)和自己實(shí)現(xiàn)的類 KVO(custom-KVO)的調(diào)用順序和次數(shù)的不同而各異,具體如下:
- 先調(diào)用 custom-KVO 再調(diào)用 native-KVO,native-KVO 和 custom-KVO 都運(yùn)行正常
- 先調(diào)用 native-KVO 再調(diào)用 custom-KVO,custom-KVO 運(yùn)行正常,native-KVO 會(huì) crash
- 先調(diào)用 native-KVO 再調(diào)用 custom-KVO 再調(diào)用 native-KVO,native-KVO 運(yùn)行正常,custom-KVO 失效,無(wú) crash
目前,SDMagicHook 已經(jīng)解決了上面提到的各類問題,具體的實(shí)現(xiàn)方案我將在下文中詳細(xì)介紹。
2. 從匯編層面探索 KVO 本質(zhì)
想要弄明白這個(gè)問題首先需要研究清楚系統(tǒng)的 KVO 到底是如何實(shí)現(xiàn)的,而系統(tǒng)的 KVO 實(shí)現(xiàn)又相當(dāng)復(fù)雜,我們?cè)搹哪睦锶胧帜兀?/p>
想要弄清楚這個(gè)問題,我們首先需要了解下當(dāng)對(duì)被 KVO 觀察的目標(biāo)屬性進(jìn)行賦值操作時(shí)到底發(fā)生了什么。這里我們以自建的 Test 類為例來(lái)說明,我們對(duì) Test 類實(shí)例的 num 屬性進(jìn)行 KVO 操作:
當(dāng)我們給 num 賦值時(shí),可以看到斷點(diǎn)命中了 KVO 類自定義的 setNum:的實(shí)現(xiàn)即_NSSetIntValueAndNotify 函數(shù)
那么_NSSetIntValueAndNotify 的內(nèi)部實(shí)現(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); ????//?獲取類實(shí)例關(guān)聯(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ā)者自己重寫實(shí)現(xiàn)過 willChangeValueForKey:
或者 didChangeValueForKey:方法的話就直接以 class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number)的方式實(shí)現(xiàn)對(duì)被觀察的原方法的調(diào)用,否則就用默認(rèn)實(shí)現(xiàn)為 NSSetIntValueAndNotify_block_invoke 的棧 block 并捕獲 indexedIvars、被 KVO 觀察的實(shí)例、被觀察屬性對(duì)應(yīng)的 SEL、賦值參數(shù)等所有必要參數(shù)并將這個(gè) block 作為參數(shù)傳遞給 [obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock]調(diào)用。
看到這里你或許會(huì)有個(gè)疑問:
偽代碼中通過 object_getIndexedIvars(cls)獲取到的 indexedIvars 是什么信息呢?
block.invoke = ___ NSSetIntValueAndNotify_block_invoke 又是如何實(shí)現(xiàn)的呢?
首先我們看下 NSSetIntValueAndNotify_block_invoke 的匯編實(shí)現(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); }
這個(gè) block 的內(nèi)部實(shí)現(xiàn)其實(shí)就是從 KVO 類的 indexedIvars 里取到原始類,然后根據(jù) sel 從原始類中取出原始的方法實(shí)現(xiàn)來(lái)執(zhí)行并最終完成了一次 KVO 調(diào)用。我們發(fā)現(xiàn)整個(gè) KVO 運(yùn)作過程中 KVO 類的 indexedIvars 是一個(gè)貫穿 KVO 流程始末的關(guān)鍵數(shù)據(jù),那么這個(gè) indexedIvars 是何時(shí)生成的呢?
indexedIvars 里又包含哪些數(shù)據(jù)呢?想要弄清楚這個(gè)問題,我們就必須從 KVO 的源頭看起,我們知道既然 KVO 要用到 isa 交換那么最終肯定要調(diào)用到 object_setClass 方法,這里我們不妨以 object_setClass 函數(shù)為線索,通過設(shè)置條件符號(hào)斷點(diǎn)來(lái)追蹤 object_setClass 的調(diào)用,lldb 調(diào)試截圖如下:
斷點(diǎn)到 object_setClass 之后,我們?cè)衮?yàn)證看下寄存器 rdi、rsi 里面的參數(shù)打印出來(lái)分別是
<Test: 0x600003df01b0>、NSKVONotifying_Test
不錯(cuò),我們現(xiàn)在已經(jīng)成功定位到 KVO 的 isa 交換現(xiàn)場(chǎng)了,然而為了找到 KVO 類的生成的地方我們還需要沿著調(diào)用棧向前回溯,最終我們定位到 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?{ ????????//?錯(cuò)誤處理過程省略...... ????????return?nil ????} ????return?newCls; }
通過_NSKVONotifyingCreateInfoWithOriginalClass 的這段偽代碼你會(huì)發(fā)現(xiàn)我們之前頻繁提到 indexedIvars 原來(lái)就是在這里初始化生成的。
objc_allocateClassPair 在 runtime.h 中的聲明為 Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) ,蘋果對(duì) extraBytes 參數(shù)的解釋為“The number of bytes to allocate for indexed ivars at the end of the class and metaclass objects.”
這就是說當(dāng)我們?cè)谕ㄟ^ objc_allocateClassPair 來(lái)生成一個(gè)新的類時(shí)可以通過指定 extraBytes 來(lái)為此類開辟額外的空間用于存儲(chǔ)一些數(shù)據(jù)。系統(tǒng)在生成 KVO 類時(shí)會(huì)額外分配 0x68 字節(jié)的空間,其具體內(nèi)存布局和用途我用一個(gè)結(jié)構(gòu)體描述如下:
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 導(dǎo)致的 native-KVO Crash
讀到這里相信你對(duì) KVO 實(shí)現(xiàn)細(xì)節(jié)有了大致的了解,然后我們?cè)倩氐阶畛醯膯栴},為什么“先調(diào)用 native-KVO 再調(diào)用 custom-KVO,custom-KVO 運(yùn)行正常,native-KVO 會(huì) crash”呢?我們還以上面提到過的 Test 類為例說明一下:
首先用 Test 類實(shí)例化了一個(gè)實(shí)例 test,然后對(duì) test 的 num 屬性進(jìn)行 native-KVO 操作,這時(shí) test 的 isa 指向了 NSKVONotifying_Test 類。
然后我們?cè)賹?duì) test 進(jìn)行 custom-KVO 操作,這時(shí)我們的 custom-KVO 會(huì)基于 NSKVONotifying_Test 類再生成一個(gè)新的子類 SD_NSKVONotifying_Test_abcd,此時(shí)問題就來(lái)了,如果我們沒有仿照 native-KVO 的做法額外分配 0x68 字節(jié)的空間用于存儲(chǔ) KVO 關(guān)鍵信息,那么當(dāng)我們向 test 發(fā)送 setNum:消息然后 setNum:方法調(diào)用 super 實(shí)現(xiàn)走到了 KVO 的_NSSetIntValueAndNotify 方法時(shí)還按照 SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(cls)方式來(lái)獲取 KVO 信息并嘗試獲取從中獲取數(shù)據(jù)時(shí)發(fā)生異常導(dǎo)致 crash。
找到問題的根源之后我們就可以見招拆招,我們可以仿照 native-KVO 的做法在生成 SD_NSKVONotifying_Test_abcd 也額外分配 0x68 自己的空間,然后當(dāng)要進(jìn)行 custom-KVO 操作時(shí)將 NSKVONotifying_Test 的 indexedIvars 拷貝一份到 SD_NSKVONotifying_Test_abcd 即可,代碼實(shí)現(xiàn)如下:
一般情況下在 native-KVO 的基礎(chǔ)上再做 custom-KVO 的話拷貝完 native-KVO 類的 indexedIvars 到 custom-KVO 類上就可以了,而我們的 SDMagicHook 只做到這些還不夠,因?yàn)?SDMagicHook 在生成的新類上以消息轉(zhuǎn)發(fā)的形式來(lái)調(diào)度方法,這樣一來(lái)問題瞬間就變得更為復(fù)雜。舉例說明如下:
- 由于用到消息轉(zhuǎn)發(fā),我們會(huì)將 SD_NSKVONotifying_Test_abcd 的
setNum:
對(duì)應(yīng)的實(shí)現(xiàn)指向_objc_msgForward,然后生成一個(gè)新的 SEL__sd_B_abcd_setNum:
來(lái)指向其子類的原生實(shí)現(xiàn),在我們這個(gè)例子中就是 NSKVONotifying_TestsetNum:
實(shí)現(xiàn)的即void _NSSetIntValueAndNotify(id obj, SEL sel, int number)
函數(shù)。 - 當(dāng) test 實(shí)例收到
setNum:
消息時(shí)會(huì)先觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制,然后 SDMagicHook 的消息調(diào)度系統(tǒng)會(huì)最終通過向 test 實(shí)例發(fā)送一個(gè)__sd_B_abcd_setNum:
消息來(lái)實(shí)現(xiàn)對(duì)被 Hook 的原生方法的回調(diào),而現(xiàn)在__sd_B_abcd_setNum:
對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)正是void _NSSetIntValueAndNotify(id obj, SEL sel, int number)
,所以__sd_B_abcd_setNum:
就會(huì)被作為 sel 參數(shù)傳遞到_NSSetIntValueAndNotify
函數(shù)。 - 然后當(dāng)
_NSSetIntValueAndNotify
函數(shù)內(nèi)部嘗試從 indexedIvars 拿到原始類 Test 然后從 Test 上查找__sd_B_abcd_setNum:
對(duì)應(yīng)的方法并調(diào)用時(shí)由于找不到對(duì)應(yīng)函數(shù)實(shí)現(xiàn)而發(fā)生 crash。為解決這個(gè)問題,我們還需要為 Test 類新增一個(gè)__sd_B_abcd_setNum:
方法并將其實(shí)現(xiàn)指向setNum:
的實(shí)現(xiàn),代碼如下:
至此,“先調(diào)用 native-KVO 再調(diào)用 custom-KVO,custom-KVO 運(yùn)行正常,native-KVO 會(huì) crash”這個(gè)問題就可以順利解決了。
4. 如何解決 native-KVO 導(dǎo)致 custom-KVO 失效的問題
目前還剩下一個(gè)問題“先調(diào)用 native-KVO 再調(diào)用 custom-KVO 再調(diào)用 native-KVO,native-KVO 運(yùn)行正常,custom-KVO 失效,無(wú) crash”。
為什么會(huì)出現(xiàn)這個(gè)問題呢?這次我們依然以 Test 類為例,首先用 Test 類實(shí)例化了一個(gè)實(shí)例 test,然后對(duì) test 的 num 屬性進(jìn)行 native-KVO 操作,這時(shí) test 的 isa 指向了 NSKVONotifying_Test 類。
然后我們?cè)賹?duì) test 進(jìn)行 custom-KVO 操作,這時(shí)我們的 custom-KVO 會(huì)基于 NSKVONotifying_Test 類再生成一個(gè)新的子類 SD_NSKVONotifying_Test_abcd,這時(shí)如果再對(duì) test 的 num 屬性進(jìn)行 native-KVO 操作就會(huì)驚奇地發(fā)現(xiàn) test 的 isa 又重新指向了 NSKVONotifying_Test 類然后 custom-KVO 就全部失效了。
WHY?!!原來(lái) native-KVO 會(huì)持有一個(gè)全局的字典:
_NSKeyValueContainerClassForIsa.NSKeyValueContainerClassPerOriginalClass 以 KVO 操作的原類為 key 和 NSKeyValueContainerClass 實(shí)例為 value 存儲(chǔ) KVO 類信息。
這樣一來(lái),當(dāng)我們?cè)俅螌?duì) test 實(shí)例進(jìn)行 KVO 操作時(shí),native-KVO 就會(huì)以 Test 類為 key 從 NSKeyValueContainerClassPerOriginalClass 中查找到之前存儲(chǔ)的 NSKeyValueContainerClass 并從中直接獲取 KVO 類 NSKVONotifying_Test 然后調(diào)用 object_setclass 方法設(shè)置到 test 實(shí)例上然后 custom-KVO 就直接失效了。
想要解決這個(gè)問題,我想到了兩種思路:
1.修改 NSKVONotifying_Test 相關(guān) KVO 數(shù)據(jù)
2.hook 攔截系統(tǒng)的 setclass 操作。然后仔細(xì)一想方案 1 是不可取的,因?yàn)?NSKVONotifying_Test 的相關(guān)數(shù)據(jù)是被所有 Test 類的實(shí)例在進(jìn)行 KVO 操作時(shí)共享的,任何改動(dòng)都有可能對(duì) Test 類實(shí)例的 KVO 產(chǎn)生全局影響。
所以,我們就需要借助 FishHook 來(lái) hook 系統(tǒng)的 object_setclass 函數(shù),當(dāng)系統(tǒng)以 NSKVONotifying_Test 為參數(shù)對(duì)一個(gè)實(shí)例進(jìn)行 setclass 操作時(shí),我們檢查如果當(dāng)前的 isa 指針是 SD_NSKVONotifying_Test_abcd 且 SD_NSKVONotifying_Test_abcd 繼承自系統(tǒng)的 NSKVONotifying_Test 時(shí)就跳過此次 setclass 操作。
但是這樣做還不夠,因?yàn)?custom-KVO 采用了特殊的消息轉(zhuǎn)發(fā)機(jī)制來(lái)調(diào)度被 hook 的方法,如果先進(jìn)行 custom-KVO 然后在進(jìn)行 native-KVO 就會(huì)導(dǎo)致被觀察屬性被重復(fù)調(diào)用。
所以,我們?cè)趯?duì)一個(gè)實(shí)例進(jìn)行首次 custom-KVO 操作之前先進(jìn)行 native-KVO,這樣一來(lái)就可以保證我們的 custom-KVO 的方法調(diào)度正常工作了。
代碼如下:
總結(jié)
KVO 的本質(zhì)其實(shí)就是基于被觀察的實(shí)例的 isa 生成一個(gè)新的類并在這個(gè)類的 extra 空間中存放各種和 KVO 操作相關(guān)的關(guān)鍵數(shù)據(jù),然后這個(gè)新的類以一個(gè)中間人的角色借助 extra 空間中存放各種數(shù)據(jù)完成復(fù)雜的方法調(diào)度。
系統(tǒng)的 KVO 實(shí)現(xiàn)比較復(fù)雜,很多函數(shù)的調(diào)用層次也比較深,我們一開始不妨從整個(gè)函數(shù)調(diào)用棧的末端層層向前梳理出主要的操作路徑,在對(duì) KVO 操作有個(gè)大致的了解之后再?gòu)娜值慕嵌日蛉娣治龈鱾€(gè)流程和細(xì)節(jié)。我們正是借助這種方式實(shí)現(xiàn)了對(duì) KVO 的快速了解和認(rèn)識(shí)。
至此,一個(gè)良好兼容 native-KVO 的 custom-KVO 就全部完成了。回頭來(lái)看,這個(gè)解決方案其實(shí)還是過于 tricky 了,不過這也只能是在 iOS 系統(tǒng)的各種限制下的無(wú)奈的選擇了。我們不提倡隨意使用類似的 tricky 操作,更多是想要通過這個(gè)例子向大家介紹一下 KVO 的本質(zhì)以及我們分析和解決問題的思路。
如果各位讀者可以從中汲取一些靈感,那么這篇文章“倒也算是不負(fù)恩澤”,倘若大家可以將這篇文章介紹到的思路和方法用于處理自己開發(fā)中的遇到的各種疑難雜癥“那便真真是極好的了”!更多關(guān)于iOS開發(fā)KVO細(xì)節(jié)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS版微信朋友圈識(shí)別圖片位置信息 如何實(shí)現(xiàn)?
這篇文章主要為大家詳細(xì)介紹了iOS版微信朋友圈識(shí)別圖片位置信息的實(shí)現(xiàn)方法2016-10-10iOS微信瀏覽器回退不刷新實(shí)例(監(jiān)聽瀏覽器回退事件)
下面小編就為大家?guī)?lái)一篇iOS微信瀏覽器回退不刷新實(shí)例(監(jiān)聽瀏覽器回退事件)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-05-05iOS基于UIScrollView實(shí)現(xiàn)滑動(dòng)引導(dǎo)頁(yè)
這篇文章主要為大家詳細(xì)介紹了iOS基于UIScrollView實(shí)現(xiàn)滑動(dòng)引導(dǎo)頁(yè)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01IOS 屏幕適配方案實(shí)現(xiàn)縮放window的示例代碼
這篇文章主要介紹了IOS 屏幕適配方案實(shí)現(xiàn)縮放window的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04iOS開發(fā)中CAlayer層的屬性以及自定義層的方法
這篇文章主要介紹了iOS開發(fā)中CAlayer層的屬性以及自定義層的方法,代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-11-11iOS中無(wú)限循環(huán)滾動(dòng)簡(jiǎn)單處理實(shí)現(xiàn)原理分析
這篇文章主要介紹了iOS中無(wú)限循環(huán)滾動(dòng)簡(jiǎn)單處理實(shí)現(xiàn)原理分析,需要的朋友可以參考下2017-12-12簡(jiǎn)單好用可任意定制的iOS Popover氣泡效果
Popover(氣泡彈出框/彈出式氣泡/氣泡)是由一個(gè)矩形和三角箭頭組成的彈出窗口,箭頭指向的地方通常是導(dǎo)致Popover彈出的控件或區(qū)域。本文通過實(shí)例代碼給大家介紹了iOS Popover氣泡效果,需要的朋友參考下吧2017-12-12