iOS13 KVC

前提

這段時(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)主要拿 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)行會(huì)拋出 KVC 異常绣溜,通過(guò)堆棧發(fā)現(xiàn)慷彤,異常是在 -[UITextField valueForKey:] 中拋出的

UITextField_error_bt.png

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):

-[UITextField valueForKey].jpg

如果參數(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: 方法的

UISearchBar_error_bt.jpg

看匯編:

-[UISearchBar _searchField].jpg

方法內(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ò) _searchFieldsearchField 取值嚼松,分別調(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ō)不定在以后的版本中肄扎,連 placeholderLabelsearchField 也不給訪問(wèn)了呢

分割線

提示:

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

_UIKVCAccessProhibited

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

_UIKVCAccessProhibited.jpg

其中主要是拿全局變量 __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

直接看 UIKitCoreMachO 文件的話拴念,會(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 中的涌哲,

forEachLoadCommand.png

LC_BUILD_VERSION = 0x32 LC_VERSION_MIN_IPHONEOS = 0x25

查看MachO文件:

MachO.png

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

MachO-10.jpg

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ǔ)圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末橄登,一起剝皮案震驚了整個(gè)濱河市抓歼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拢锹,老刑警劉巖谣妻,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異卒稳,居然都是意外死亡蹋半,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門充坑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)减江,“玉大人染突,你說(shuō)我怎么就攤上這事”沧疲” “怎么了份企?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)巡莹。 經(jīng)常有香客問(wèn)我司志,道長(zhǎng),這世上最難降的妖魔是什么降宅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任俐芯,我火速辦了婚禮,結(jié)果婚禮上钉鸯,老公的妹妹穿的比我還像新娘吧史。我一直安慰自己,他們只是感情好唠雕,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布贸营。 她就那樣靜靜地躺著,像睡著了一般岩睁。 火紅的嫁衣襯著肌膚如雪钞脂。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天捕儒,我揣著相機(jī)與錄音冰啃,去河邊找鬼。 笑死刘莹,一個(gè)胖子當(dāng)著我的面吹牛阎毅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播点弯,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼扇调,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了抢肛?” 一聲冷哼從身側(cè)響起狼钮,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捡絮,沒(méi)想到半個(gè)月后熬芜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡福稳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年涎拉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡曼库,死狀恐怖区岗,靈堂內(nèi)的尸體忽然破棺而出略板,到底是詐尸還是另有隱情毁枯,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布叮称,位于F島的核電站种玛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瓤檐。R本人自食惡果不足惜赂韵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挠蛉。 院中可真熱鬧祭示,春花似錦、人聲如沸谴古。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)掰担。三九已至汇陆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間带饱,已是汗流浹背毡代。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勺疼,地道東北人教寂。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像执庐,于是被迫代替她去往敵國(guó)和親孝宗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容