iOS13 KVC

前提

這段時間升級了 Xcode11.0,在 iOS13.0 運(yùn)行的時候,當(dāng)運(yùn)行到 [textField setValue:color forKeyPath:@"_placeholderLabel.textColor"] 崩潰了,拋出了KVC錯誤

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug'

在 iOS13 中,不再允許通過 KVC 的方式去訪問私有屬性,需要通過其他方式修改。

目前我找到的會觸發(fā) KVC 訪問權(quán)限異常崩潰的方法有:

  • UITabBarButton -> _info
  • UITextField -> _placeholderLabel
  • _UIBarBackground -> _shadowView
  • _UIBarBackground -> _backgroundEffectView
  • UISearchBar -> _cancelButtonText
  • UISearchBar -> _cancelButton
  • UISearchBar -> _searchField

在網(wǎng)上看到有人說 私有KVC崩潰與系統(tǒng)版本無關(guān),與Xcode版本有關(guān),Xcode11編譯會崩潰

這個說法是錯的,接下來主要拿 UITextFieldUISearchBarKVC 崩潰做講解

分析

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)行會拋出 KVC 異常,通過堆棧發(fā)現(xiàn),異常是在 -[UITextField valueForKey:] 中拋出的

UITextField_error_bt.png

UITextField 屬于系統(tǒng)UI庫,而在 iOS13 中,UITextField 內(nèi)部重寫了 valueForKey: 方法,通過判斷參數(shù) key 是否為 _placeholderLabel 來決定是否訪問了私有屬性,下圖是 iOS13 的 UIKitCore 中新增的 -[UITextField valueForKey:] 的匯編實現(xiàn):

-[UITextField valueForKey].jpg

如果參數(shù) key 等于字符串 _placeholderLabel,則調(diào)用 _UIKVCAccessProhibited() C函數(shù)決定是否拋出異常,這個函數(shù)放到下面再講。而在 iOS12 的 UIKitCore 中,UITextField 是沒有重寫 valueForKey: 方法的,因此在 iOS12 上是不會拋出異常。既然 UITextField 內(nèi)部是通過判斷 key 是否等于_placeholderLabel 來拋出異常的,那么試試不加 "_":

UITextField *tf = [UITextField new];
[tf valueForKey:@"placeholderLabel"];

正常運(yùn)行沒報錯~

在 iOS13 中,UITextField 只重寫了 valueForKey:,沒有重寫 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: ?

可能有人會認(rèn)為,UISearchBar 內(nèi)部也重寫了 valueForKey: 方法,判斷 key 值。看函數(shù)調(diào)用堆棧,是進(jìn)入到 [UISearchBar _searchField] 方法才拋出異常的,而且 UISearchBar 內(nèi)部并沒有重寫 valueForKey: 方法的

UISearchBar_error_bt.jpg

看匯編:

-[UISearchBar _searchField].jpg

方法內(nèi)部直接調(diào)用了 _UIKVCAccessProhibited() 函數(shù),那為什么 iOS12 不會崩潰呢?

UISearchBar 在 iOS12 和 iOS13 上的實現(xiàn)略有不同。在 iOS13 中,UISearchBar 實現(xiàn)了 searchField,_searchField,searchTextField,_searchTextField_searchBarTextField;在 iOS12 中,UISearchBar 實現(xiàn)了 searchField,_searchBarTextField

在 iOS13 中,UISearchBar 額外實現(xiàn)了 _searchField 方法,因此通過 _searchFieldsearchField 取值,分別調(diào)的是不同的方法。蘋果為什么要這么做呢?我也不知道??♂?。不過在 iOS13 中,UISearchBar 的私有變量不再是自己內(nèi)部創(chuàng)建了,而是通過 _UISearchBarVisualProviderIOS 這個類來創(chuàng)建的,這個類是 iOS13 后才有的,估計是為了區(qū)分 iOS 系統(tǒng)和 iPadOS 系統(tǒng)吧??

如果要想繼續(xù)獲取 UISearchBar 的輸入框,可以使用 searchField,不要加 _

在 iOS13 上,UIKitCore 這個系統(tǒng)共享UI庫中,新增了 _UIKVCAccessProhibited 函數(shù)去限制了 KVC 訪問權(quán)限控制,蘋果之所以要私有屬性也是不想我們?nèi)ピL問的,所以盡量不訪問吧。說不定在以后的版本中,連 placeholderLabelsearchField 也不給訪問了呢

分割線

提示:

下面的內(nèi)容主要講解的是 Xcode10 和 Xcode11 打的包,在 iOS13 上運(yùn)行結(jié)果不一樣的原因,其中涉及到了 MachODYLD 的知識了,有興趣的可以繼續(xù)看,沒興趣的就返回吧

_UIKVCAccessProhibited

這是在 iOS13 上才有的函數(shù),因此拿 iOS13 模擬器中的 UIKitCore 分析,先看看Hopper反編譯出來的偽代碼吧

_UIKVCAccessProhibited.jpg

其中主要是拿全局變量 __UIApplicationLinkedOnVersion 和 寄存器 rdx 中的值做比較,寄存器 rdx 存儲的是函數(shù)的第3個參數(shù),在 [UITextField valueForKey:][UISearchBar _searchField] 中傳入的值都為 0xd0000,值得注意的時候,匯編中用的是立即數(shù),是固定的。

Xcode10 打的包運(yùn)行在 iOS13 上不會崩潰,因此猜想 Xcode10 的包,__UIApplicationLinkedOnVersion 的值是比 0xd0000 小的,而 Xcode11 的包會拋出異常,那么 Xcode11 的包,__UIApplicationLinkedOnVersion 的值要大于等于 0xd0000

直接看 UIKitCoreMachO 文件的話,會發(fā)現(xiàn) __UIApplicationLinkedOnVersion 的值為 0x0,即使不為 0x0,在運(yùn)行時也會動態(tài)改變,否則沒法區(qū)分 Xcode10 和 Xcode11

__UIApplicationLinkedOnVersion 配套出現(xiàn)的還有的 __UIApplicationLinkedOnVersionOnce,猜想代碼中肯定會出現(xiàn)和??類似的代碼:

static dispatch_once_t __UIApplicationLinkedOnVersionOnce;
dispatch_once(&__UIApplicationLinkedOnVersionOnce, ^{
    ...
});

還真的在 -[UIApplication _runWithMainScene:transitionContext:completion:] 中找到了實現(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;
}

看來全局變量 __UIApplicationLinkedOnVersion 的值是通過 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ù),我對 dyld 也不熟,也是一步一步找進(jìn)去的,如果筆者找錯了,還望下方留言告知一下

筆者用的是iOS模擬器研究的,所以對于一些宏編譯,只保留了模擬器相關(guān)的。SDK 的版本主要是通過 APP 的可執(zhí)行文件獲取的,即 MachO文件。

APP 的 SDK 版本是存儲在 MachO 文件的加載命令 load command 中的,

forEachLoadCommand.png

LC_BUILD_VERSION = 0x32 LC_VERSION_MIN_IPHONEOS = 0x25

查看MachO文件:

MachO.png

SDK 版本取的是 Load Command 偏移12個字節(jié)后的4個字節(jié),即取 0xD0000,該 MachO文件是通過 Xcode11 編譯得到的,因此 Xcode11 編譯的包,運(yùn)行在 iOS 上,全局變量 __UIApplicationLinkedOnVersion 的值為 0xD0000,可以在代碼中加入如下代碼,測試結(jié)果:

extern NSInteger _UIApplicationLinkedOnVersion;
NSLog(@"%lx", (long)_UIApplicationLinkedOnVersion);

打印結(jié)果為

d0000

Bingo!!!!!

Xcode10 編譯運(yùn)行后的打印結(jié)果為:c0400,MachO文件中的值也確實為 0xC0400

MachO-10.jpg

END

所以同樣的代碼[textField valueForKey:@"_placeholderLabel"] 同樣運(yùn)行在 iOS13 上,用 Xcode10 編譯的包不會崩潰,用 Xcode11 編譯的包會崩潰,不僅是因為 iOS13 的系統(tǒng)內(nèi)部實現(xiàn)變了,還和編譯時所用的 SDK 版本有關(guān)

Fix:10.25 之前的圖片有點(diǎn)錯亂,重新補(bǔ)圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容