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

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

  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 代码,因为过于冗长,下篇再做分析;

推荐阅读更多精彩内容