前提
這段時(shí)間升級(jí)了 Xcode11.0田藐,在 iOS13.0 運(yùn)行的時(shí)候怠苔,當(dāng)運(yùn)行到 [textField setValue:color forKeyPath:@"_placeholderLabel.textColor"]
崩潰了,拋出了KVC錯(cuò)誤
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug'
在 iOS13 中埋心,不再允許通過(guò) KVC 的方式去訪問(wèn)私有屬性指郁,需要通過(guò)其他方式修改。
目前我找到的會(huì)觸發(fā) KVC 訪問(wèn)權(quán)限異常崩潰的方法有:
-
UITabBarButton ->
_info
-
UITextField ->
_placeholderLabel
-
_UIBarBackground ->
_shadowView
-
_UIBarBackground ->
_backgroundEffectView
-
UISearchBar ->
_cancelButtonText
-
UISearchBar ->
_cancelButton
-
UISearchBar ->
_searchField
在網(wǎng)上看到有人說(shuō) 私有KVC崩潰與系統(tǒng)版本無(wú)關(guān)拷呆,與Xcode版本有關(guān)闲坎,Xcode11編譯會(huì)崩潰
這個(gè)說(shuō)法是錯(cuò)的疫粥,接下來(lái)主要拿 UITextField 和 UISearchBar 的 KVC 崩潰做講解
分析
UITextField - _placeholderLabel
UITextField *tf = [UITextField new];
[tf valueForKey:@"_placeholderLabel"];
先將上面的代碼分別在 Xcode10.3,Xcode11.0腰懂,iOS12.4梗逮,iOS13上運(yùn)行,看看運(yùn)行結(jié)果:
- Xcode10.3 - iOS12.4: ?
- Xcode10.3 - iOS13: ?
- Xcode11.0 - iOS12.4: ?
- Xcode11.0 - iOS13.0: ?
只有在 Xcode11.0 - iOS13.0 上運(yùn)行會(huì)拋出 KVC 異常绣溜,通過(guò)堆棧發(fā)現(xiàn)慷彤,異常是在 -[UITextField valueForKey:]
中拋出的
UITextField 屬于系統(tǒng)UI庫(kù),而在 iOS13 中怖喻,UITextField 內(nèi)部重寫了 valueForKey:
方法底哗,通過(guò)判斷參數(shù) key
是否為 _placeholderLabel
來(lái)決定是否訪問(wèn)了私有屬性,下圖是 iOS13 的 UIKitCore 中新增的 -[UITextField valueForKey:]
的匯編實(shí)現(xiàn):
如果參數(shù) key
等于字符串 _placeholderLabel锚沸,則調(diào)用 _UIKVCAccessProhibited()
C函數(shù)決定是否拋出異常跋选,這個(gè)函數(shù)放到下面再講。而在 iOS12 的 UIKitCore 中哗蜈,UITextField 是沒(méi)有重寫 valueForKey:
方法的野建,因此在 iOS12 上是不會(huì)拋出異常。既然 UITextField 內(nèi)部是通過(guò)判斷 key 是否等于_placeholderLabel 來(lái)拋出異常的恬叹,那么試試不加 "_":
UITextField *tf = [UITextField new];
[tf valueForKey:@"placeholderLabel"];
正常運(yùn)行沒(méi)報(bào)錯(cuò)~
在 iOS13 中候生,UITextField 只重寫了 valueForKey:
,沒(méi)有重寫 setValue:forKey:
绽昼,因此下面的方法也是能正常運(yùn)行的
UITextField *tf = [UITextField new];
[tf valueForKey:@"placeholderLabel"];
[tf setValue:nil forKey:@"placeholderLabel"];
[tf setValue:nil forKey:@"_placeholderLabel"];
如果要想繼續(xù)獲取 UITextField 的占位文本框唯鸭,可以使用
placeholderLabel
,不要加_
UISearchBar - _searchField
UISearchBar *sb = [UISearchBar new];
[sb valueForKey:@"_searchField"];
先將上面的代碼分別在 Xcode10.3硅确,Xcode11.0目溉,iOS12.4,iOS13上運(yùn)行菱农,看看運(yùn)行結(jié)果:
- Xcode10.3 - iOS12.4: ?
- Xcode10.3 - iOS13: ?
- Xcode11.0 - iOS12.4: ?
- Xcode11.0 - iOS13.0: ?
可能有人會(huì)認(rèn)為缭付,UISearchBar 內(nèi)部也重寫了 valueForKey:
方法,判斷 key 值循未∠菝ǎ看函數(shù)調(diào)用堆棧,是進(jìn)入到 [UISearchBar _searchField]
方法才拋出異常的的妖,而且 UISearchBar 內(nèi)部并沒(méi)有重寫 valueForKey:
方法的
看匯編:
方法內(nèi)部直接調(diào)用了 _UIKVCAccessProhibited()
函數(shù)绣檬,那為什么 iOS12 不會(huì)崩潰呢?
UISearchBar 在 iOS12 和 iOS13 上的實(shí)現(xiàn)略有不同嫂粟。在 iOS13 中娇未,UISearchBar 實(shí)現(xiàn)了 searchField
,_searchField
星虹,searchTextField
零抬,_searchTextField
镊讼,_searchBarTextField
;在 iOS12 中平夜,UISearchBar 實(shí)現(xiàn)了 searchField
狠毯,_searchBarTextField
在 iOS13 中,UISearchBar 額外實(shí)現(xiàn)了 _searchField
方法褥芒,因此通過(guò) _searchField
和 searchField
取值嚼松,分別調(diào)的是不同的方法。蘋果為什么要這么做呢锰扶?我也不知道??♂?献酗。不過(guò)在 iOS13 中,UISearchBar 的私有變量不再是自己內(nèi)部創(chuàng)建了坷牛,而是通過(guò) _UISearchBarVisualProviderIOS 這個(gè)類來(lái)創(chuàng)建的罕偎,這個(gè)類是 iOS13 后才有的,估計(jì)是為了區(qū)分 iOS 系統(tǒng)和 iPadOS 系統(tǒng)吧??
如果要想繼續(xù)獲取 UISearchBar 的輸入框京闰,可以使用
searchField
颜及,不要加_
在 iOS13 上,UIKitCore 這個(gè)系統(tǒng)共享UI庫(kù)中蹂楣,新增了 _UIKVCAccessProhibited 函數(shù)去限制了 KVC 訪問(wèn)權(quán)限控制俏站,蘋果之所以要私有屬性也是不想我們?nèi)ピL問(wèn)的,所以盡量不訪問(wèn)吧痊土。說(shuō)不定在以后的版本中肄扎,連 placeholderLabel
和 searchField
也不給訪問(wèn)了呢
分割線
提示:
下面的內(nèi)容主要講解的是 Xcode10 和 Xcode11 打的包,在 iOS13 上運(yùn)行結(jié)果不一樣的原因赁酝,其中涉及到了 MachO 和 DYLD 的知識(shí)了犯祠,有興趣的可以繼續(xù)看,沒(méi)興趣的就返回吧
_UIKVCAccessProhibited
這是在 iOS13 上才有的函數(shù)酌呆,因此拿 iOS13 模擬器中的 UIKitCore 分析衡载,先看看Hopper反編譯出來(lái)的偽代碼吧
其中主要是拿全局變量 __UIApplicationLinkedOnVersion
和 寄存器 rdx
中的值做比較,寄存器 rdx
存儲(chǔ)的是函數(shù)的第3個(gè)參數(shù)隙袁,在 [UITextField valueForKey:]
和 [UISearchBar _searchField]
中傳入的值都為 0xd0000痰娱,值得注意的時(shí)候,匯編中用的是立即數(shù)藤乙,是固定的猜揪。
Xcode10 打的包運(yùn)行在 iOS13 上不會(huì)崩潰惭墓,因此猜想 Xcode10 的包坛梁,__UIApplicationLinkedOnVersion
的值是比 0xd0000 小的,而 Xcode11 的包會(huì)拋出異常腊凶,那么 Xcode11 的包划咐,__UIApplicationLinkedOnVersion
的值要大于等于 0xd0000
直接看 UIKitCore 的 MachO 文件的話拴念,會(huì)發(fā)現(xiàn) __UIApplicationLinkedOnVersion
的值為 0x0,即使不為 0x0褐缠,在運(yùn)行時(shí)也會(huì)動(dòng)態(tài)改變政鼠,否則沒(méi)法區(qū)分 Xcode10 和 Xcode11
與 __UIApplicationLinkedOnVersion
配套出現(xiàn)的還有的 __UIApplicationLinkedOnVersionOnce
,猜想代碼中肯定會(huì)出現(xiàn)和??類似的代碼:
static dispatch_once_t __UIApplicationLinkedOnVersionOnce;
dispatch_once(&__UIApplicationLinkedOnVersionOnce, ^{
...
});
還真的在 -[UIApplication _runWithMainScene:transitionContext:completion:]
中找到了實(shí)現(xiàn)代碼:
if (*(int32_t *)__UIApplicationLinkedOnVersion == 0x0) {
if (*__UIApplicationLinkedOnVersionOnce != 0xffffffffffffffff) {
dispatch_once(__UIApplicationLinkedOnVersionOnce, ^ {/* block implemented at _____UIApplicationLinkedOnOrAfter_block_invoke */ } });
}
}
void _____UIApplicationLinkedOnOrAfter_block_invoke(void * _block) {
*(int32_t *)__UIApplicationLinkedOnVersion = dyld_get_program_sdk_version(_block);
return;
}
看來(lái)全局變量 __UIApplicationLinkedOnVersion
的值是通過(guò) dyld_get_program_sdk_version()
獲取的队魏。下載 DYLD 源碼看看吧
// APIs.cpp
uint32_t dyld_get_program_sdk_version()
{
static uint32_t sProgramSDKVersion = 0;
if (sProgramSDKVersion == 0) {
sProgramSDKVersion = dyld3::dyld_get_sdk_version(gAllImages.mainExecutable());
}
return sProgramSDKVersion;
}
uint32_t dyld_get_sdk_version(const mach_header* mh)
{
__block bool versionFound = false;
__block uint32_t retval = 0;
dyld3::dyld_get_image_versions(mh, ^(dyld_platform_t platform, uint32_t sdk_version, uint32_t min_version) {
if (versionFound) return;
if (platform == ::dyld_get_active_platform()) {
versionFound = true;
switch (dyld3::dyld_get_base_platform(platform)) {
case PLATFORM_BRIDGEOS: retval = sdk_version + 0x00090000; return;
case PLATFORM_WATCHOS: retval = sdk_version + 0x00070000; return;
default: retval = sdk_version; return;
}
} else if (platform == PLATFORM_IOSSIMULATOR && ::dyld_get_active_platform() == PLATFORM_IOSMAC) {
//FIXME bringup hack
versionFound = true;
retval = 0x000C0000;
}
});
return retval;
}
void dyld_get_image_versions(const struct mach_header* mh, void (^callback)(dyld_platform_t platform, uint32_t sdk_version, uint32_t min_version))
{
Diagnostics diag;
const MachOFile* mf = (MachOFile*)mh;
if ( mf->isMachO(diag, mh->sizeofcmds + sizeof(mach_header_64)) )
dyld_get_image_versions_internal(mh, callback);
}
static void dyld_get_image_versions_internal(const struct mach_header* mh, void (^callback)(dyld_platform_t platform, uint32_t sdk_version, uint32_t min_version))
{
const MachOFile* mf = (MachOFile*)mh;
__block bool lcFound = false;
mf->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
lcFound = true;
// If SDK field is empty then derive the value from library linkages
if (sdk == 0) {
sdk = deriveVersionFromDylibs(mh);
}
callback((const dyld_platform_t)platform, sdk, minOS);
});
// No load command was found, so again, fallback to deriving it from library linkages
if (!lcFound) {
dyld_platform_t platform = PLATFORM_IOSSIMULATOR;
uint32_t derivedVersion = deriveVersionFromDylibs(mh);
if ( platform != 0 && derivedVersion != 0 ) {
callback(platform, derivedVersion, 0);
}
}
}
void MachOFile::forEachSupportedPlatform(void (^handler)(Platform platform, uint32_t minOS, uint32_t sdk)) const
{
Diagnostics diag;
forEachLoadCommand(diag, ^(const load_command* cmd, bool& stop) {
const build_version_command* buildCmd = (build_version_command *)cmd;
const version_min_command* versCmd = (version_min_command*)cmd;
switch ( cmd->cmd ) {
case LC_BUILD_VERSION:
handler((Platform)(buildCmd->platform), buildCmd->minos, buildCmd->sdk);
break;
...
case LC_VERSION_MIN_IPHONEOS:
if ( (this->cputype == CPU_TYPE_X86_64) || (this->cputype == CPU_TYPE_I386) )
handler(Platform::iOS_simulator, versCmd->version, versCmd->sdk); // old sim binary
else
handler(Platform::iOS, versCmd->version, versCmd->sdk);
break;
...
)
handler(Platform::watchOS_simulator, versCmd->version, versCmd->sdk); // old sim binary
else
handler(Platform::watchOS, versCmd->version, versCmd->sdk);
break;
}
});
diag.assertNoError(); // any malformations in the file should have been caught by earlier validate() call
}
struct version_min_command {
uint32_t cmd; /* LC_VERSION_MIN_MACOSX or
LC_VERSION_MIN_IPHONEOS */
uint32_t cmdsize; /* sizeof(struct min_version_command) */
uint32_t version; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */
uint32_t sdk; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */
};
// Xcode: usr/include/mach-o/loader.h
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
這里只取關(guān)鍵的函數(shù)公般,我對(duì) dyld 也不熟,也是一步一步找進(jìn)去的胡桨,如果筆者找錯(cuò)了官帘,還望下方留言告知一下
筆者用的是iOS模擬器研究的,所以對(duì)于一些宏編譯昧谊,只保留了模擬器相關(guān)的刽虹。SDK 的版本主要是通過(guò) APP 的可執(zhí)行文件獲取的,即 MachO文件呢诬。
APP 的 SDK 版本是存儲(chǔ)在 MachO 文件的加載命令 load command 中的涌哲,
LC_BUILD_VERSION = 0x32 LC_VERSION_MIN_IPHONEOS = 0x25
查看MachO文件:
SDK 版本取的是 Load Command 偏移12個(gè)字節(jié)后的4個(gè)字節(jié),即取 0xD0000尚镰,該 MachO文件是通過(guò) Xcode11 編譯得到的阀圾,因此 Xcode11 編譯的包,運(yùn)行在 iOS 上狗唉,全局變量 __UIApplicationLinkedOnVersion 的值為 0xD0000稍刀,可以在代碼中加入如下代碼,測(cè)試結(jié)果:
extern NSInteger _UIApplicationLinkedOnVersion;
NSLog(@"%lx", (long)_UIApplicationLinkedOnVersion);
打印結(jié)果為
d0000
Bingo!!!!!
Xcode10 編譯運(yùn)行后的打印結(jié)果為:c0400敞曹,MachO文件中的值也確實(shí)為 0xC0400
END
所以同樣的代碼[textField valueForKey:@"_placeholderLabel"]
同樣運(yùn)行在 iOS13 上账月,用 Xcode10 編譯的包不會(huì)崩潰,用 Xcode11 編譯的包會(huì)崩潰澳迫,不僅是因?yàn)?iOS13 的系統(tǒng)內(nèi)部實(shí)現(xiàn)變了局齿,還和編譯時(shí)所用的 SDK 版本有關(guān)
Fix:10.25 之前的圖片有點(diǎn)錯(cuò)亂,重新補(bǔ)圖