美文网首页
iOS类加载流程(四):map_images流程分析

iOS类加载流程(四):map_images流程分析

作者: 康小曹 | 来源:发表于2022-06-21 20:57 被阅读0次

    经过之前的文章,我们已经知道了:

    1. objc 调用 dyld 方法注册 3 个回调时,会通过 notifyBatchPartial 触发 map_images 回调;
    2. 有新的 image 被 map 时,也会触发该回调;

    那么,map 函数到底做了什么?现在就来看看 map 函数的流程是怎么样的。

    先上流程图:

    流程图

    1. map_images

    简化代码如下:

    void 
    map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                      const struct mach_header * const mhdrs[]) {
        static bool firstTime = YES;
        header_info *hList[mhCount];
        uint32_t hCount;
        size_t selrefCount = 0;
        
        if (firstTime) {
            // 共享缓存预优化操作初始化
            preopt_init();
        }
    
        ...省略totalClass的计算逻辑
    
        if (firstTime) {
            //sel初始化
            sel_init(selrefCount);
            // SideTable
            arr_init();
        }
    
        if (hCount > 0) {
            _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
        }
    
        firstTime = NO;
    }
    

    这个方法其实就做了这么几件事:

    1. 共享缓存相关的预优化操作;
    2. 计算当前所有 image 中总共的 class 、方法数量;
    3. 初始化方法表和 SideTable;
    4. _read_images 相关逻辑;

    接下来一步一步分析~~~

    2. 预优化

    preopt_init 函数在 objc-opt.mm 中有两个,其中一个就是单纯的打印逻辑:

    preopt_init-disable

    而另外一个函数才是真正的逻辑,而影响的关键在于 SUPPORT_PREOPT 这个宏,其定义如下:

    SUPPORT_PREOPT

    也就是说,iOS 架构下改值必定为 1,所以会进入到下面的逻辑,那么 preopt_init 函数就是这样的:

    void preopt_init(void) {
        // Get the memory region occupied by the shared cache.
        size_t length;
        const void *start = _dyld_get_shared_cache_range(&length);
        if (start) {
            shared_cache_start = (uintptr_t)start;
            shared_cache_end = shared_cache_start + length;
        } else {
            shared_cache_start = shared_cache_end = 0;
        }
        
        // `opt` not set at compile time in order to detect too-early usage
        const char *failure = nil;
        opt = &_objc_opt_data;
    
        if (DisablePreopt) {
            // OBJC_DISABLE_PREOPTIMIZATION is set
            // If opt->version != VERSION then you continue at your own risk.
            failure = "(by OBJC_DISABLE_PREOPTIMIZATION)";
        }  else if (opt->version != objc_opt::VERSION) {
            // This shouldn't happen. You probably forgot to edit objc-sel-table.s.
            // If dyld really did write the wrong optimization version, 
            // then we must halt because we don't know what bits dyld twiddled.
            _objc_fatal("bad objc preopt version (want %d, got %d)", 
                        objc_opt::VERSION, opt->version);
        } else if (!opt->selopt()  ||  !opt->headeropt_ro()) {
            // One of the tables is missing. 
            failure = "(dyld shared cache is absent or out of date)";
        }
        
        if (failure) {
            // All preoptimized selector references are invalid.
            preoptimized = NO;
            opt = nil;
            disableSharedCacheOptimizations();
        } else {
            // Valid optimization data written by dyld shared cache
            preoptimized = YES;
        }
    }
    

    上述代码逻辑如下:

    1. 获取共享缓存内存地址;
    2. 使用 &_objc_opt_data 赋值 opt
    3. 分别根据一些逻辑来判断是否初始化成功;
    4. 初始化标志位;

    上述代码中,最重要的无非就是一句代码:

    opt = &_objc_opt_data;
    

    这个 _objc_opt_data 是个啥?

    _objc_opt_data

    根据注释可以知道 _objc_opt_data 就是 mach-o 文件中的 __TEXT 这个 segment 中的 __objc_opt_ro 这个 section。

    这个 __objc_opt_ro 是个啥?相关资料很少,总结一下:

    1. libobjc.A.dylib 特有;
    2. dyld 需要对这些数据进行 rebase,也就是更新 slide;
    3. 主要是存储一些预优化的数据;

    对于第一点,可以从 mach-O 上直观看到:

    __objc_opt_ro

    对于第二点,是从 Adding ASLR to jailbroken iPhones 看到的资料:

    Adding ASLR to jailbroken iPhones

    dyld3 确实也对这些数据进行了 rebase:

    dyld3-doOptimizeObjC

    上面的代码来自 dyld-655.1,这个版本已经被全面应用在 iOS12 上了,dyld3 中优化的逻辑大概是:缓存 dyld 中的一些不会改变或者变化不频繁的数据,下次启动时不需要再重新全量解析,以此来优化启动速度。比如 rebase 属于每次都需要重新解析的,不会也不能缓存。但是一些类的结构,特别是系统相关的类的结构基本不会有变化,所以这一部分数据可以缓存下来,下次启动时直接加载,不需要重新解析了。

    对于第三点,是在 IDA 的 7.2 版本的更新公告中找到的:

    IDA-7.2

    上图来自 IDA-7.2 版本的更新公告:https://hex-rays.com/products/ida/news/7_2/

    IDA:IDA Pro(简称IDA)是DataRescue公司(home of PhotoRescue, data recovery solution for flash memory cards)出品的一款交互式反汇编工具,它功能强大、操作复杂,要完全掌握它,需要很多知识。

    至此,我们知道了 __objc_opt_ro 主要是 dyld3 中用来存储预优化数据的,那么这些数据有什么呢?

    回到声明了 _objc_opt_data 的 objc-opt.mm 文件中,可以看到相关的方法并不多:

    objc-opt.mm

    很明显,在 objc-opt.mm 中,涉及到了协议、方法、类等的优化。

    再来看看 objc_opt_t 这个结构体的定义,这个结构体通过 #include <objc-shared-cache.h> 导入:

    objc_opt_t

    所以,这里大概可以猜出来,dyld3 对 libobjc.A.dylib 中的类、方法、协议、ro、rw 数据都做了一些预优化操作,大概是加载(map、read)、实现(realize)完成之后,将这些数据保存下来,这样在以后的冷启动过程中,就不需要再执行这些操作,只需要读取完毕之后存入内存缓存即可使用。

    相关的 wwdc 可能会有帮助:Advancements in the Objective-C runtime

    比如,builtins 这个全局变量存储的就是被优化过的 objc 内置函数(猜的),对应的 search_builtins() 就在很多地方被调用:

    static SEL search_builtins(const char *name) 
    {
    #if SUPPORT_PREOPT
        if (builtins) return (SEL)builtins->get(name);
    #endif
        return nil;
    }
    

    至此,预优化的逻辑告一段落,做个总结吧:

    1. dyld3 中读取 libobjc.A.dylib 中的 __objc_opt_ro 数据并进行 rebase;
    2. objc 在冷启动时会进行预加载优化;
    3. 预加载优化大概是一些系统函数、类的、方法的优化,这样就省去了启动时的消耗;
    4. 启动优化在 dyld2 进化到 dyld3 时优化比较大,有兴趣的可以重点关注下高版本 dyld 中 dyld3 的源码;

    3. objc 相关 image 处理

    这一部分主要是处理当前所有的 image 的 header:

    // Count classes. Size various table based on the total.
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    {
        uint32_t i = mhCount;
        while (i--) {
            const headerType *mhdr = (const headerType *)mhdrs[i];
    
            // 判断image是否有objc相关内容,有则实例化并记录header
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) {
                // no objc data in this entry
                continue;
            }
            
            if (mhdr->filetype == MH_EXECUTE) {
    #if __OBJC2__
                // objc2 新增特性
                size_t count;
                _getObjc2SelectorRefs(hi, &count);
                selrefCount += count;
                // read_image中会被处理
                _getObjc2MessageRefs(hi, &count);
                selrefCount += count;
    #else
                _getObjcSelectorRefs(hi, &selrefCount);
    #endif
            }
            
            hList[hCount++] = hi;
        }
    }
    

    如上,所有的 image 都会递归调用这段代码。

    上述逻辑中,首先调用 addHeader 对 image 的 header 进行处理,该方法代码就不贴了,主要做了这么几件事:

    1. 有效性判断;
    2. 通过 _getObjcImageInfo 和获取 __OBJC 这个 segment中的内容,以此来判断该 image 是否属于 objc 应该处理的 image;
    3. 如果改 image 属于 objc 相关的 image,则通过 calloc 来实例化 header 并存储相关信息;
    4. 通过 appendHeader 存储实例化的 header 到一个链表中供后续使用;
    5. 通过 mach-o 中的 __objc_classlist 获取该 image 中的 classCount;

    总之,这一步就是实例化了 objc 相关的 header 并且以链表的形式存储到内存中以备后用,与此同时,完成了 classCount 和方法引用数量的计算。

    4. Sel的初始化操作和注册逻辑

    接下来进行了方法相关的初始化逻辑,代码如下:

    /***********************************************************************
    * sel_init
    * Initialize selector tables and register selectors used internally.
    **********************************************************************/
    void sel_init(size_t selrefCount)  {
        // save this value for later
        SelrefCount = selrefCount;
        
    #if SUPPORT_PREOPT
        builtins = preoptimizedSelectors();
    #endif
        
        // Register selectors used by libobjc
    
    #define s(x) SEL_##x = sel_registerNameNoLock(#x, NO)
    #define t(x,y) SEL_##y = sel_registerNameNoLock(#x, NO)
    
        mutex_locker_t lock(selLock);
    
        s(load);
        s(initialize);
        t(resolveInstanceMethod:, resolveInstanceMethod);
        t(resolveClassMethod:, resolveClassMethod);
        t(.cxx_construct, cxx_construct);
        t(.cxx_destruct, cxx_destruct);
        s(retain);
        s(release);
        s(autorelease);
        s(retainCount);
        s(alloc);
        t(allocWithZone:, allocWithZone);
        s(dealloc);
        s(copy);
        s(new);
        t(forwardInvocation:, forwardInvocation);
        t(_tryRetain, tryRetain);
        t(_isDeallocating, isDeallocating);
        s(retainWeakReference);
        s(allowsWeakReference);
    
    #undef s
    #undef t
    }
    

    上述方法的关键在于:

    1. builtins = preoptimizedSelectors();
    2. sel_registerNameNoLock

    对于第一点,这里的代码:

    objc_selopt_t *preoptimizedSelectors(void)  {
        return opt ? opt->selopt() : nil;
    }
    

    这个 opt 就是上文中说到的和预优化有关的逻辑,在 preopt_init() 中被赋值:

    preopt_init

    是不是很惊喜?正好和上文对上了~~~~~~

    此时,再去看看 preoptimizedSelectors 这个方法具体逻辑:

    objc_selopt_t *preoptimizedSelectors(void) 
    {
        return opt ? opt->selopt() : nil;
    }
    

    selopt() 是声明在 objc_opt_t 结构体中的:

    const objc_selopt_t* selopt() const {
        if (selopt_offset == 0) return NULL;
        return (objc_selopt_t *)((uint8_t *)this + selopt_offset);
    }
    

    也就是说,预优化过后 sel 的表会被加载到内存中,而这个内存地址在这里被赋值给了 builtins 这个全局变量(猜的),对应的 search_builtins() 就在很多地方被调用:

    static SEL search_builtins(const char *name) 
    {
    #if SUPPORT_PREOPT
        if (builtins) return (SEL)builtins->get(name);
    #endif
        return nil;
    }
    

    上述代码应该很好理解了,就是给过来一个 sel 对应的 name,在 builtins 中搜索是否存在,存在表示这个 SEL 是被预优化过的,后续可能会节省很多初始化、实现等逻辑,比如 sel_ismapped 函数中:

    sel_isMapped

    继续来看第二步,也就是 sel_registerNameNoLock 函数,这个函数最终会走到 __sel_registerName,其代码如下:

    static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
    {
        SEL result = 0;
    
        // 锁操作
        if (shouldLock) selLock.assertUnlocked();
        else selLock.assertLocked();
    
        // 错误判断
        if (!name) return (SEL)0;
    
        // 已经预优化就不需要再注册了
        result = search_builtins(name);
        if (result) return result;
        
        // namedSelectors中查找
        conditional_mutex_locker_t lock(selLock, shouldLock);
        if (namedSelectors) {
            result = (SEL)NXMapGet(namedSelectors, name);
        }
        if (result) return result;
    
        // No match. Insert.
        if (!namedSelectors) {
            namedSelectors = NXCreateMapTable(NXStrValueMapPrototype,
                                              (unsigned)SelrefCount);
        }
        
        if (!result) {
            result = sel_alloc(name, copy);
            // fixme choose a better container (hash not map for starters)
            NXMapInsert(namedSelectors, sel_getName(result), result);
        }
    
        return result;
    }
    

    上述代码逻辑非常清晰:

    1. 锁操作;
    2. 字符串有效判断;
    3. 已经预优化过的不需要再注册,直接返回预优化过后的 SEL;
    4. 在 namedSelectors 中寻找,找到则返回结果;
    5. 没有 namedSelectors 则初始化;
    6. 对 SEL 分配内存空间然后插入到 namedSelectors 中;

    总之,SEL 也被分配了内存空间,并且和 Class 一样,以 MapTable 的形式被存储。

    至此,SEL 的初始化和注册逻辑就完成了~~~

    总结一下,这一步主要做了几件事:

    1. 将预优化过后的 SEL 复制到 builtins 中,方便后续的 search 操作;
    2. 预优化后的 SEL 可以节省很多操作;
    3. 注册 SEL 并以 MapTable 的形式存储;

    5. 初始化SideTable

    方法相关初始化逻辑完成后,对 SideTable 进行了初始化。SideTable 是 OC 中相当关键的一个存储结构,关于 SideTable 详见:iOS:SideTable,本文就不再赘述了。

    6. read_images

    上述步骤过后,就到了 read_images 方法的调用。该方法会传递:

    1. hList:objc 相关 image 的 header 列表;
    2. hCount:objc 相关 image 的 header 数量(这里是 C 语言数组的常规操作,数组本质是指针。count 是必须要传的,否则无法知道数组的结束点);
    3. totalClasses:所有 objc 相关的 images 中 class 的总数;
    4. unoptimizedTotalClasses:未优化的 class 的总数;

    至于 read_images 代码,因为过于冗长,下篇再做分析;

    相关文章

      网友评论

          本文标题:iOS类加载流程(四):map_images流程分析

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