iOS13 KVC

作者: 代码移动工程师 | 来源:发表于2019-10-24 13:57 被阅读0次

    前提## 前提

    这段时间升级了 Xcode11.0,在 iOS13.0 运行的时候,当运行到 [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 的方式去访问私有属性,需要通过其他方式修改。

    目前我找到的会触发 KVC 访问权限异常崩溃的方法有:

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

    在网上看到有人说 私有KVC崩溃与系统版本无关,与Xcode版本有关,Xcode11编译会崩溃

    这个说法是错的,接下来主要拿 UITextFieldUISearchBarKVC 崩溃做讲解

    分析

    UITextField - _placeholderLabel

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

    先将上面的代码分别在 Xcode10.3,Xcode11.0,iOS12.4,iOS13上运行,看看运行结果:

    • Xcode10.3 - iOS12.4: ✅
    • Xcode10.3 - iOS13: ✅
    • Xcode11.0 - iOS12.4: ✅
    • Xcode11.0 - iOS13.0: ❌

    只有在 Xcode11.0 - iOS13.0 上运行会抛出 KVC 异常,通过堆栈发现,异常是在 -[UITextField valueForKey:] 中抛出的

    ![## 前提

    这段时间升级了 Xcode11.0,在 iOS13.0 运行的时候,当运行到 [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 的方式去访问私有属性,需要通过其他方式修改。

    目前我找到的会触发 KVC 访问权限异常崩溃的方法有:

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

    在网上看到有人说 私有KVC崩溃与系统版本无关,与Xcode版本有关,Xcode11编译会崩溃

    这个说法是错的,接下来主要拿 UITextFieldUISearchBarKVC 崩溃做讲解

    分析

    UITextField - _placeholderLabel

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

    先将上面的代码分别在 Xcode10.3,Xcode11.0,iOS12.4,iOS13上运行,看看运行结果:

    • Xcode10.3 - iOS12.4: ✅
    • Xcode10.3 - iOS13: ✅
    • Xcode11.0 - iOS12.4: ✅
    • Xcode11.0 - iOS13.0: ❌

    只有在 Xcode11.0 - iOS13.0 上运行会抛出 KVC 异常,通过堆栈发现,异常是在 -[UITextField valueForKey:] 中抛出的

    image

    UITextField 属于系统UI库,而在 iOS13 中,UITextField 内部重写了 valueForKey: 方法,通过判断参数 key 是否为 _placeholderLabel 来决定是否访问了私有属性,下图是 iOS13 的 UIKitCore 中新增的 -[UITextField valueForKey:] 的汇编实现:

    image

    如果参数 key 等于字符串 _placeholderLabel,则调用 _UIKVCAccessProhibited() C函数决定是否抛出异常,这个函数放到下面再讲。而在 iOS12 的 UIKitCore 中,UITextField 是没有重写 valueForKey: 方法的,因此在 iOS12 上是不会抛出异常。既然 UITextField 内部是通过判断 key 是否等于_placeholderLabel 来抛出异常的,那么试试不加 "_":

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

    正常运行没报错~

    在 iOS13 中,UITextField 只重写了 valueForKey:,没有重写 setValue:forKey:,因此下面的方法也是能正常运行的

    UITextField *tf = [UITextField new];
    [tf valueForKey:@"placeholderLabel"];
    [tf setValue:nil forKey:@"placeholderLabel"];
    [tf setValue:nil forKey:@"_placeholderLabel"];
    
    

    如果要想继续获取 UITextField 的占位文本框,可以使用 placeholderLabel,不要加 _

    UISearchBar - _searchField

    UISearchBar *sb = [UISearchBar new];
    [sb valueForKey:@"_searchField"];
    
    

    先将上面的代码分别在 Xcode10.3,Xcode11.0,iOS12.4,iOS13上运行,看看运行结果:

    • Xcode10.3 - iOS12.4: ✅
    • Xcode10.3 - iOS13: ✅
    • Xcode11.0 - iOS12.4: ✅
    • Xcode11.0 - iOS13.0: ❌

    可能有人会认为,UISearchBar 内部也重写了 valueForKey: 方法,判断 key 值。看函数调用堆栈,是进入到 [UISearchBar _searchField] 方法才抛出异常的,而且 UISearchBar 内部并没有重写 valueForKey: 方法的

    image

    看汇编:

    image

    方法内部直接调用了 _UIKVCAccessProhibited() 函数,那为什么 iOS12 不会崩溃呢?

    UISearchBar 在 iOS12 和 iOS13 上的实现略有不同。在 iOS13 中,UISearchBar 实现了 searchField_searchFieldsearchTextField_searchTextField_searchBarTextField;在 iOS12 中,UISearchBar 实现了 searchField_searchBarTextField

    在 iOS13 中,UISearchBar 额外实现了 _searchField 方法,因此通过 _searchFieldsearchField 取值,分别调的是不同的方法。苹果为什么要这么做呢?我也不知道🤷‍♂️。不过在 iOS13 中,UISearchBar 的私有变量不再是自己内部创建了,而是通过 _UISearchBarVisualProviderIOS 这个类来创建的,这个类是 iOS13 后才有的,估计是为了区分 iOS 系统和 iPadOS 系统吧😓

    如果要想继续获取 UISearchBar 的输入框,可以使用 searchField,不要加 _

    在 iOS13 上,UIKitCore 这个系统共享UI库中,新增了 _UIKVCAccessProhibited 函数去限制了 KVC 访问权限控制,苹果之所以要私有属性也是不想我们去访问的,所以尽量不访问吧。说不定在以后的版本中,连 placeholderLabelsearchField 也不给访问了呢

    分割线

    提示:

    下面的内容主要讲解的是 Xcode10 和 Xcode11 打的包,在 iOS13 上运行结果不一样的原因,其中涉及到了 MachODYLD 的知识了,有兴趣的可以继续看,没兴趣的就返回吧

    _UIKVCAccessProhibited

    这是在 iOS13 上才有的函数,因此拿 iOS13 模拟器中的 UIKitCore 分析,先看看Hopper反编译出来的伪代码吧

    image

    其中主要是拿全局变量 __UIApplicationLinkedOnVersion 和 寄存器 rdx 中的值做比较,寄存器 rdx 存储的是函数的第3个参数,在 [UITextField valueForKey:][UISearchBar _searchField] 中传入的值都为 0xd0000,值得注意的时候,汇编中用的是立即数,是固定的。

    Xcode10 打的包运行在 iOS13 上不会崩溃,因此猜想 Xcode10 的包,__UIApplicationLinkedOnVersion 的值是比 0xd0000 小的,而 Xcode11 的包会抛出异常,那么 Xcode11 的包,__UIApplicationLinkedOnVersion 的值要大于等于 0xd0000

    直接看 UIKitCoreMachO 文件的话,会发现 __UIApplicationLinkedOnVersion 的值为 0x0,即使不为 0x0,在运行时也会动态改变,否则没法区分 Xcode10 和 Xcode11

    __UIApplicationLinkedOnVersion 配套出现的还有的 __UIApplicationLinkedOnVersionOnce,猜想代码中肯定会出现和👇类似的代码:

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

    还真的在 -[UIApplication _runWithMainScene:transitionContext:completion:] 中找到了实现代码:

    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 */
    };
    
    

    这里只取关键的函数,我对 dyld 也不熟,也是一步一步找进去的,如果笔者找错了,还望下方留言告知一下

    笔者用的是iOS模拟器研究的,所以对于一些宏编译,只保留了模拟器相关的。SDK 的版本主要是通过 APP 的可执行文件获取的,即 MachO文件。

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

    image

    LC_BUILD_VERSION = 0x32 LC_VERSION_MIN_IPHONEOS = 0x25

    查看MachO文件:

    image

    SDK 版本取的是 Load Command 偏移12个字节后的4个字节,即取 0xD0000,该 MachO文件是通过 Xcode11 编译得到的,因此 Xcode11 编译的包,运行在 iOS 上,全局变量 __UIApplicationLinkedOnVersion 的值为 0xD0000,可以在代码中加入如下代码,测试结果:

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

    打印结果为

    d0000
    
    

    Bingo!!!!!

    Xcode10 编译运行后的打印结果为:c0400,MachO文件中的值也确实为 0xC0400

    image

    END

    所以同样的代码[textField valueForKey:@"_placeholderLabel"] 同样运行在 iOS13 上,用 Xcode10 编译的包不会崩溃,用 Xcode11 编译的包会崩溃,不仅是因为 iOS13 的系统内部实现变了,还和编译时所用的 SDK 版本有关

    Fix:10.25 之前的图片有点错乱,重新补图

    作者:kwdx
    链接:https://www.jianshu.com/p/fefef4ecd763
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    ](https://img.haomeiwen.com/i3488607/d3969202cf7c0bf9.png?imageMogr2/auto-orient/strip|imageView2/2/w/814/format/webp)

    UITextField 属于系统UI库,而在 iOS13 中,UITextField 内部重写了 valueForKey: 方法,通过判断参数 key 是否为 _placeholderLabel 来决定是否访问了私有属性,下图是 iOS13 的 UIKitCore 中新增的 -[UITextField valueForKey:] 的汇编实现:

    image

    如果参数 key 等于字符串 _placeholderLabel,则调用 _UIKVCAccessProhibited() C函数决定是否抛出异常,这个函数放到下面再讲。而在 iOS12 的 UIKitCore 中,UITextField 是没有重写 valueForKey: 方法的,因此在 iOS12 上是不会抛出异常。既然 UITextField 内部是通过判断 key 是否等于_placeholderLabel 来抛出异常的,那么试试不加 "_":

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

    正常运行没报错~

    在 iOS13 中,UITextField 只重写了 valueForKey:,没有重写 setValue:forKey:,因此下面的方法也是能正常运行的

    UITextField *tf = [UITextField new];
    [tf valueForKey:@"placeholderLabel"];
    [tf setValue:nil forKey:@"placeholderLabel"];
    [tf setValue:nil forKey:@"_placeholderLabel"];
    
    

    如果要想继续获取 UITextField 的占位文本框,可以使用 placeholderLabel,不要加 _

    UISearchBar - _searchField

    UISearchBar *sb = [UISearchBar new];
    [sb valueForKey:@"_searchField"];
    
    

    先将上面的代码分别在 Xcode10.3,Xcode11.0,iOS12.4,iOS13上运行,看看运行结果:

    • Xcode10.3 - iOS12.4: ✅
    • Xcode10.3 - iOS13: ✅
    • Xcode11.0 - iOS12.4: ✅
    • Xcode11.0 - iOS13.0: ❌

    可能有人会认为,UISearchBar 内部也重写了 valueForKey: 方法,判断 key 值。看函数调用堆栈,是进入到 [UISearchBar _searchField] 方法才抛出异常的,而且 UISearchBar 内部并没有重写 valueForKey: 方法的

    image

    看汇编:

    image

    方法内部直接调用了 _UIKVCAccessProhibited() 函数,那为什么 iOS12 不会崩溃呢?

    UISearchBar 在 iOS12 和 iOS13 上的实现略有不同。在 iOS13 中,UISearchBar 实现了 searchField_searchFieldsearchTextField_searchTextField_searchBarTextField;在 iOS12 中,UISearchBar 实现了 searchField_searchBarTextField

    在 iOS13 中,UISearchBar 额外实现了 _searchField 方法,因此通过 _searchFieldsearchField 取值,分别调的是不同的方法。苹果为什么要这么做呢?我也不知道🤷‍♂️。不过在 iOS13 中,UISearchBar 的私有变量不再是自己内部创建了,而是通过 _UISearchBarVisualProviderIOS 这个类来创建的,这个类是 iOS13 后才有的,估计是为了区分 iOS 系统和 iPadOS 系统吧😓

    如果要想继续获取 UISearchBar 的输入框,可以使用 searchField,不要加 _

    在 iOS13 上,UIKitCore 这个系统共享UI库中,新增了 _UIKVCAccessProhibited 函数去限制了 KVC 访问权限控制,苹果之所以要私有属性也是不想我们去访问的,所以尽量不访问吧。说不定在以后的版本中,连 placeholderLabelsearchField 也不给访问了呢

    分割线

    提示:

    下面的内容主要讲解的是 Xcode10 和 Xcode11 打的包,在 iOS13 上运行结果不一样的原因,其中涉及到了 MachODYLD 的知识了,有兴趣的可以继续看,没兴趣的就返回吧

    _UIKVCAccessProhibited

    这是在 iOS13 上才有的函数,因此拿 iOS13 模拟器中的 UIKitCore 分析,先看看Hopper反编译出来的伪代码吧

    image

    其中主要是拿全局变量 __UIApplicationLinkedOnVersion 和 寄存器 rdx 中的值做比较,寄存器 rdx 存储的是函数的第3个参数,在 [UITextField valueForKey:][UISearchBar _searchField] 中传入的值都为 0xd0000,值得注意的时候,汇编中用的是立即数,是固定的。

    Xcode10 打的包运行在 iOS13 上不会崩溃,因此猜想 Xcode10 的包,__UIApplicationLinkedOnVersion 的值是比 0xd0000 小的,而 Xcode11 的包会抛出异常,那么 Xcode11 的包,__UIApplicationLinkedOnVersion 的值要大于等于 0xd0000

    直接看 UIKitCoreMachO 文件的话,会发现 __UIApplicationLinkedOnVersion 的值为 0x0,即使不为 0x0,在运行时也会动态改变,否则没法区分 Xcode10 和 Xcode11

    __UIApplicationLinkedOnVersion 配套出现的还有的 __UIApplicationLinkedOnVersionOnce,猜想代码中肯定会出现和👇类似的代码:

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

    还真的在 -[UIApplication _runWithMainScene:transitionContext:completion:] 中找到了实现代码:

    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 */
    };
    
    

    这里只取关键的函数,我对 dyld 也不熟,也是一步一步找进去的,如果笔者找错了,还望下方留言告知一下

    笔者用的是iOS模拟器研究的,所以对于一些宏编译,只保留了模拟器相关的。SDK 的版本主要是通过 APP 的可执行文件获取的,即 MachO文件。

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

    image

    LC_BUILD_VERSION = 0x32 LC_VERSION_MIN_IPHONEOS = 0x25

    查看MachO文件:

    image

    SDK 版本取的是 Load Command 偏移12个字节后的4个字节,即取 0xD0000,该 MachO文件是通过 Xcode11 编译得到的,因此 Xcode11 编译的包,运行在 iOS 上,全局变量 __UIApplicationLinkedOnVersion 的值为 0xD0000,可以在代码中加入如下代码,测试结果:

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

    打印结果为

    d0000
    
    

    Bingo!!!!!

    Xcode10 编译运行后的打印结果为:c0400,MachO文件中的值也确实为 0xC0400

    image

    END

    所以同样的代码[textField valueForKey:@"_placeholderLabel"] 同样运行在 iOS13 上,用 Xcode10 编译的包不会崩溃,用 Xcode11 编译的包会崩溃,不仅是因为 iOS13 的系统内部实现变了,还和编译时所用的 SDK 版本有关

    Fix:10.25 之前的图片有点错乱,重新补图

    作者:kwdx
    链接:https://www.jianshu.com/p/fefef4ecd763
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    这段时间升级了 Xcode11.0,在 iOS13.0 运行的时候,当运行到 [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 的方式去访问私有属性,需要通过其他方式修改。

    目前我找到的会触发 KVC 访问权限异常崩溃的方法有:

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

    在网上看到有人说 私有KVC崩溃与系统版本无关,与Xcode版本有关,Xcode11编译会崩溃

    这个说法是错的,接下来主要拿 UITextFieldUISearchBarKVC 崩溃做讲解

    分析

    UITextField - _placeholderLabel

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

    先将上面的代码分别在 Xcode10.3,Xcode11.0,iOS12.4,iOS13上运行,看看运行结果:

    • Xcode10.3 - iOS12.4: ✅
    • Xcode10.3 - iOS13: ✅
    • Xcode11.0 - iOS12.4: ✅
    • Xcode11.0 - iOS13.0: ❌

    只有在 Xcode11.0 - iOS13.0 上运行会抛出 KVC 异常,通过堆栈发现,异常是在 -[UITextField valueForKey:] 中抛出的

    image

    UITextField 属于系统UI库,而在 iOS13 中,UITextField 内部重写了 valueForKey: 方法,通过判断参数 key 是否为 _placeholderLabel 来决定是否访问了私有属性,下图是 iOS13 的 UIKitCore 中新增的 -[UITextField valueForKey:] 的汇编实现:

    image

    如果参数 key 等于字符串 _placeholderLabel,则调用 _UIKVCAccessProhibited() C函数决定是否抛出异常,这个函数放到下面再讲。而在 iOS12 的 UIKitCore 中,UITextField 是没有重写 valueForKey: 方法的,因此在 iOS12 上是不会抛出异常。既然 UITextField 内部是通过判断 key 是否等于_placeholderLabel 来抛出异常的,那么试试不加 "_":

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

    正常运行没报错~

    在 iOS13 中,UITextField 只重写了 valueForKey:,没有重写 setValue:forKey:,因此下面的方法也是能正常运行的

    UITextField *tf = [UITextField new];
    [tf valueForKey:@"placeholderLabel"];
    [tf setValue:nil forKey:@"placeholderLabel"];
    [tf setValue:nil forKey:@"_placeholderLabel"];
    
    

    如果要想继续获取 UITextField 的占位文本框,可以使用 placeholderLabel,不要加 _

    UISearchBar - _searchField

    UISearchBar *sb = [UISearchBar new];
    [sb valueForKey:@"_searchField"];
    
    

    先将上面的代码分别在 Xcode10.3,Xcode11.0,iOS12.4,iOS13上运行,看看运行结果:

    • Xcode10.3 - iOS12.4: ✅
    • Xcode10.3 - iOS13: ✅
    • Xcode11.0 - iOS12.4: ✅
    • Xcode11.0 - iOS13.0: ❌

    可能有人会认为,UISearchBar 内部也重写了 valueForKey: 方法,判断 key 值。看函数调用堆栈,是进入到 [UISearchBar _searchField] 方法才抛出异常的,而且 UISearchBar 内部并没有重写 valueForKey: 方法的

    image

    看汇编:

    image

    方法内部直接调用了 _UIKVCAccessProhibited() 函数,那为什么 iOS12 不会崩溃呢?

    UISearchBar 在 iOS12 和 iOS13 上的实现略有不同。在 iOS13 中,UISearchBar 实现了 searchField_searchFieldsearchTextField_searchTextField_searchBarTextField;在 iOS12 中,UISearchBar 实现了 searchField_searchBarTextField

    在 iOS13 中,UISearchBar 额外实现了 _searchField 方法,因此通过 _searchFieldsearchField 取值,分别调的是不同的方法。苹果为什么要这么做呢?我也不知道🤷‍♂️。不过在 iOS13 中,UISearchBar 的私有变量不再是自己内部创建了,而是通过 _UISearchBarVisualProviderIOS 这个类来创建的,这个类是 iOS13 后才有的,估计是为了区分 iOS 系统和 iPadOS 系统吧😓

    如果要想继续获取 UISearchBar 的输入框,可以使用 searchField,不要加 _

    在 iOS13 上,UIKitCore 这个系统共享UI库中,新增了 _UIKVCAccessProhibited 函数去限制了 KVC 访问权限控制,苹果之所以要私有属性也是不想我们去访问的,所以尽量不访问吧。说不定在以后的版本中,连 placeholderLabelsearchField 也不给访问了呢

    分割线

    提示:

    下面的内容主要讲解的是 Xcode10 和 Xcode11 打的包,在 iOS13 上运行结果不一样的原因,其中涉及到了 MachODYLD 的知识了,有兴趣的可以继续看,没兴趣的就返回吧

    _UIKVCAccessProhibited

    这是在 iOS13 上才有的函数,因此拿 iOS13 模拟器中的 UIKitCore 分析,先看看Hopper反编译出来的伪代码吧

    image

    其中主要是拿全局变量 __UIApplicationLinkedOnVersion 和 寄存器 rdx 中的值做比较,寄存器 rdx 存储的是函数的第3个参数,在 [UITextField valueForKey:][UISearchBar _searchField] 中传入的值都为 0xd0000,值得注意的时候,汇编中用的是立即数,是固定的。

    Xcode10 打的包运行在 iOS13 上不会崩溃,因此猜想 Xcode10 的包,__UIApplicationLinkedOnVersion 的值是比 0xd0000 小的,而 Xcode11 的包会抛出异常,那么 Xcode11 的包,__UIApplicationLinkedOnVersion 的值要大于等于 0xd0000

    直接看 UIKitCoreMachO 文件的话,会发现 __UIApplicationLinkedOnVersion 的值为 0x0,即使不为 0x0,在运行时也会动态改变,否则没法区分 Xcode10 和 Xcode11

    __UIApplicationLinkedOnVersion 配套出现的还有的 __UIApplicationLinkedOnVersionOnce,猜想代码中肯定会出现和👇类似的代码:

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

    还真的在 -[UIApplication _runWithMainScene:transitionContext:completion:] 中找到了实现代码:

    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 */
    };
    
    

    这里只取关键的函数,我对 dyld 也不熟,也是一步一步找进去的,如果笔者找错了,还望下方留言告知一下

    笔者用的是iOS模拟器研究的,所以对于一些宏编译,只保留了模拟器相关的。SDK 的版本主要是通过 APP 的可执行文件获取的,即 MachO文件。

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

    image

    LC_BUILD_VERSION = 0x32 LC_VERSION_MIN_IPHONEOS = 0x25

    查看MachO文件:

    image

    SDK 版本取的是 Load Command 偏移12个字节后的4个字节,即取 0xD0000,该 MachO文件是通过 Xcode11 编译得到的,因此 Xcode11 编译的包,运行在 iOS 上,全局变量 __UIApplicationLinkedOnVersion 的值为 0xD0000,可以在代码中加入如下代码,测试结果:

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

    打印结果为

    d0000
    
    

    Bingo!!!!!

    Xcode10 编译运行后的打印结果为:c0400,MachO文件中的值也确实为 0xC0400

    image

    END

    所以同样的代码[textField valueForKey:@"_placeholderLabel"] 同样运行在 iOS13 上,用 Xcode10 编译的包不会崩溃,用 Xcode11 编译的包会崩溃,不仅是因为 iOS13 的系统内部实现变了,还和编译时所用的 SDK 版本有关

    Fix:10.25 之前的图片有点错乱,重新补图

    链接:https://www.jianshu.com/p/fefef4ecd763

    相关文章

      网友评论

          本文标题:iOS13 KVC

          本文链接:https://www.haomeiwen.com/subject/wxnrvctx.html