Runtime源代码解读8(应用载入)

在 Clang 编译 Objective-C 源文件时,需要先将 Objective-C 代码转化为 C 语言代码,然后编译得到目标文件(object),最后将目标文件链接为二进制文件(binary)。二进制文件表现为应用或框架、类库等。操作系统通过dyld提供的 API 启动或加载二进制文件。大致过程是:首先通过open(...)函数打开二进制文件;然后通过mmap(...)内存映射函数,将二进制文件中的目标文件映射到内存空间,因此目标文件也可以称为镜像文件,镜像文件所映射的内存区域则为镜像(image);最后进行镜像绑定、依赖初始化等其他操作。

本文的主要内容是:探讨 runtime 如何读取镜像中定义的 Objective-C 元数据。

注意:工程编译生成的动态链接库(Overview of Dynamic Libraries深入剖析iOS动态链接库)、静态链接库、可执行程序统称为二进制文件(binary)。

一、Objective-C 体系初始化

面向对象的 Objective-C 体系的总加载入口在objc-os.mm文件中的void _objc_init(void)。操作系统执行了_objc_init()后,才能开始加载 Objective-C 编写的应用。

void _objc_init(void)用于初始化 runtime 环境,并注册三个回调 以监听dyld加载镜像的两个状态、以及卸载镜像动作。开头的三行代码说明该函数只被执行一次。操作系统调用dyld的 API 加载(dyld开源代码)二进制文件并完成内存映射后,一旦镜像切换到指定状态 或者 监听到镜像卸载动作,则会触发_objc_init中所注册的相应的回调函数:

  • 镜像切换到dyld_image_state_bound(镜像完成绑定)时,触发map_images(...)回调函数,将镜像中定义的 Objective-C 元数据(类、分类、协议等等)加载到 runtime;

  • 镜像加载切换到dyld_image_state_dependents_initialized(镜像的依赖库完成初始化)时,触发load_images (...)回调函数,执行镜像中的 Objective-C 元素初始化操作,主要是执行类和分类的load方法;

  • 监测到卸载镜像动作时,触发unmap_image (...)回调函数,将属于该镜像的元素从 runtime 系统记录的已加载 Objective-C 元素中移除。

// 运行时环境的启动总入口
void _objc_init(void)
{
    // 重要:_objc_init全局只调用一次
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // 运行环境初始化系列操作
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
    
    // 监听镜像加载卸载,注册回调    
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

二、载入镜像

载入镜像是由系统或应用调用dyld的 API 实现的,例如用户打开 APP 则会触发载入镜像过程。镜像载入过程需要完成目标文件映射到内存空间、依赖库初始化等工作。而本章仅介绍内存映射过程生成的镜像的内存布局。使用 Clang 编译.m源文件,所生成的.o目标文件包含以下三大块信息,依次相邻分布:

  • 镜像元数据区:包含头信息(header info)、架构相关信息fat_header(针对多架构编译的目标文件才有);
  • 加载命令区:加载命令(load commands),包含了镜像中保存的数据的元数据,加载命令用于访问或操作目标文件中的数据。例如LC_SEGMENT_64用于读取目标数据段中保存的数据;
  • 数据区:包括代码段(text segment)、数据段(data segment)、符号表(symbol table)等等。其中,代码段顾名思义用于保存代码,目标文件中保存的代码是机器码。数据段用于保存 在代码中初始化的全局静态变量及全局变量,这些数据固化在镜像文件中。

下图是一个.o目标文件的结构示例:

目标文件实例.png

注意:关于目标文件的结构的详细介绍,可以参考 认识MachO

2.1 镜像的内存结构

目标文件映射到内存生成镜像,由于操作系统的 CPU 架构通常是单一的,因此镜像基本只包含单一架构的数据。以下是目标文件映射到内存生成的镜像的内存结构示意图。图中左边表示镜像内存的布局结构,从下至上为低地址到高地址。右边的mach_header_64结构体定义了镜像头信息的数据结构;load_command结构体表示抽象的加载命令类型,通常镜像中不会直接保存load_command,而是继承了load_command的具体加载命令类型,例如:segment_command_64用于加载镜像中 segment 的数据;section_64结构体用于记录 segment 中具有特定含义的一块数据(section)的元信息。

  • 红色区域表示保存mach_header结构体;
  • 绿色区域表示保存load_command结构体,加载数据段;
  • 黄色区域表示保存section_64结构体;
  • 紫色区域表示保存了一块代码段数据(section);
  • 洋红色区域表示保存了一块常量数据段数据(section);
  • 青色区域保存保存了一块数据段数据(section);
镜像内存结构示意图.jpg

2.1 读取镜像中的数据

上图中segment_command_64中红色标记的成员,以及section_64中绿色标记的成员,它们是读取镜像中数据的关键。

segment_command_64结构体中:

  • cmdsize:具体的加载命令结构体类型 所占用字节数;
  • fileoff:数据段(segment)的数据区在镜像内存空间数据区中的相对地址;
  • filesize:数据段(segment)的数据区占用字节数;
  • nsect:表示数据段中包含的 data section 的数量;

section_64结构体中:

  • size:数据段 data section 占用字节数;
  • offset:数据段 data section 在镜像内存空间数据区中的相对地址。

读取镜像的数据段(segment)的代码如下。其中head参数,指向镜像的头信息结构体,即指向镜像文件映射的内存缓冲区,segname为数据段名称。其原理是:

  • 首先通过头信息内存地址 + sizeof(struct mach_header_64)获取加载命令区的起始地址;
  • 然后遍历加载命令区中所有的加载命令,当遍历到的加载命令sgpLC_SEGMENT_64,且加载命令的数据段名称segname匹配时,返回该数据段,反之则通过sgp = sgp->cmdsize跳过命令占用空间以遍历到下一个加载命令。
static const segment_command_64 *
getsegbynamefromheader(const mach_header_64 *head, const char *segname)
{
    const segment_command_64 *sgp;
    unsigned long I;
    
    sgp = (const segment_command_64 *) (head + 1);
    for (i = 0; i < head->ncmds; i++){
        if (sgp->cmd == LC_SEGMENT_64) {
            if (strncmp(sgp->segname, segname, sizeof(sgp->segname)) == 0) {
                return sgp;
            }
        }
        sgp = (const segment_command_64 *)((char *)sgp + sgp->cmdsize);
    }
    return NULL;
}

读取数据段中的 data section 数据的模拟实现代码如下,该函数定义在<mach-o/getsect.h>头文件中,但是没有公布实现代码,以下是根据目标文件内存布局模拟的实现逻辑。原理是:

  • 首先通过getsegbynamefromheader(...)获取目标数据段sgp
  • 然后计算数据区起始地址datastart = mhp + sizeofheader + sizeofcmds,表示数据区紧接在 头信息 和 加载命令区 后面;
  • 然后遍历sgp中所有 data section 的元数据sectptr。当sectname匹配时返回datastart + sectptr->offset,表示 data section 的数据区域的起始地址;反之则通过sectptr += sizeof(struct section_64)以遍历到下一个 data section 的元数据;
extern uint8_t *getsectiondata(
    const struct mach_header_64 *mhp,
    const char *segname,
    const char *sectname,
    unsigned long *size){

    // 1. 获取目标数据段
    const segment_command_64 *sgp = getsegbynamefromheader(mhp, segname);
    if (!sgp)
        return NULL;

    uint_32t sizeofcmds = mhp->sizeofcmds;
    uint_32t sizeofheader = sizeof(struct mach_header_64);
    uint8_t * datastart = mhp + sizeofheader + sizeofcmds; // 计算数据区起始地址

    // 2. 获取目标数据段首个 data section 元数据(section_64结构体)的起始地址
    uint8_t * sectptr = sgp + sizeof(struct segment_command_64);

    // 3. 遍历目标数据段的所有 data section
    for(int i = 0; i < sgp->nsects; i++) {
        if (strncmp(sectptr->sectname, sectname, sizeof(sectptr->sectname)) == 0) {
            if(size > sectptr->size)
                return NULL;  // 获取字节数超出 section 数据范围应该给出错误提示

            uint8_t *sectdataptr = datastart + sectptr->offset;
            return sectdataptr;
        }
            
        sectptr += sizeof(struct section_64); // 偏移到下一个
    }
    return NULL;
}

三、镜像内容映射到内存 map_images

载入镜像后进入特定的状态将触发_objc_init(...)初始化 Objective-C 运行环境时注册的三个回调。完成镜像绑定后将触发map_images回调,加载镜像中定义的 Objective-C 元素。传入回调的参数包括:

  • 待处理镜像头信息的数组const struct mach_header * const mhdrs[]
  • 镜像的路径const char * const paths[]
  • 镜像头信息数组的长度unsigned count

map_images(...)函数直接调用了map_images_nolock (...)函数,该函数包含很多底层知识点,非常晦涩,这里并没有深入分析,总结其功能为:

  • 预处理优化初始相关,该部分逻辑公开的细节很少可以忽略;
  • 通过addHeader(...)汇总镜像中的 Objective-C 元素,生成head_info的数组,作为下一步的输入;
  • 调用_read_images(...)加载镜像中的 Objective-C 元素,如类、分类、协议、消息等等。

第3点是其核心功能。以下是map_images(...)map_images_nolock(...)的源代码,代码中标注了“核心逻辑”的部分将在后文分别详细介绍。

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

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();
    }

    hCount = 0;  // 所有 包含Objective-C元素的镜像
    int totalClasses = 0;  // 类的总数
    int unoptimizedTotalClasses = 0;  // 未优化的类的总数
    {
        // 遍历镜像中所有元素信息,逐一转换成header_info,并添加到hList
        uint32_t i = mhCount;
        while (i--) {
            const headerType *mhdr = (const headerType *)mhdrs[I];

            // (核心逻辑1)addHeader内部逻辑将headerType转化为header_info类型并添加到一张全局的
            // 链表中,返回header_info类型的转化结果
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) continue;
            
            if (mhdr->filetype == MH_EXECUTE) {
                // 一些可执行文件的初始化代码
#if __OBJC2__
                size_t count;
                _getObjc2SelectorRefs(hi, &count);
                selrefCount += count;
                _getObjc2MessageRefs(hi, &count);
                selrefCount += count;
#else
                _getObjcSelectorRefs(hi, &selrefCount);
#endif
                
                // 忽略GC兼容检查代码
                ...

            }
            
            hList[hCount++] = hi;
        }
    }

    if (firstTime) {
        //初始化一些最基本的选择器,如alloc、dealloc、initialize、load等等
        sel_init(selrefCount);

        // 初始化AutoreleasePool和SideTable
        arr_init();

        // 忽略GC兼容检查代码
        ...

        // 忽略针对MAC OS X平台的initialize的fork安全检查代码(Fixme: 不懂)
        ...

    }

    if (hCount > 0) {
        // (核心逻辑2)加载二进制文件中的元素,包括类、分类、协议等等
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
}

void sel_init(size_t selrefCount)
{
    // 保存到SelrefCount静态的int中,只是为了后面初始化namedSelectors哈希表时提供初始size
    SelrefCount = selrefCount;

#if SUPPORT_PREOPT
    // 预处理优化Selector,忽略
    builtins = preoptimizedSelectors();

    if (PrintPreopt  &&  builtins) {
        uint32_t occupied = builtins->occupied;
        uint32_t capacity = builtins->capacity;
    }
#endif

// sel_registerNameNoLock用于将SEL添加到全局的namedSelectors哈希表中,方便系统
// 快速判断某个SEL是否可识别
#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
}

void arr_init(void) 
{
    AutoreleasePoolPage::init();  //自动释放池初始化,后续独立文章介绍
    SideTableInit();  //SideTable初始化,后续独立文章介绍
}

3.1 核心逻辑 addHeader

addHeader的作用不仅仅在于返回headerType对应的header_info结构体,其内部调用了appendHeader()将所有已加载的镜像的元素所对应的header_info串联成链表结构,链表首节点为FirstHeader、末尾节点为LastHeader。该链表在后面卸载镜像元素时需要用到。至此完成镜像数据收集工作。

3.1.1 addHeader 所收集数据 header_info

addHeader所收集的信息header_info结构体的功能如下:

  • 用于快速定位镜像中的头信息(mach_header_64结构体)、镜像信息。镜像信息用于标记镜像的属性,以objc_image_info结构体格式保存于 镜像文件的__DATA数据段的__objc_imageinfo data section,镜像信息在构建镜像文件时生成;
  • 包含header_info_rw rw_data[]用于记录镜像以及镜像中的 Objective-C 元素的状态,该成员的数据是可读写的。

镜像具体包含的状态见header_info_rw结构体:

  • 最低位isLoaded标记镜像是否已加载;
  • 次低位allClassesRealized标记镜像中所有的 Objective-C 类是否已完成 class realizing;
  • 其他位用于保存下一个header_info节点的地址,由于地址的最低 3 位必为0因此实际上仅需保存最低 3 位之外的位即可,又因为header_info_rw仅用了最低 2 位作为特殊标记位,因此指定当前header_info* curHeader指向下一个节点header_info* nextHeader时,仅需设置((header_info_rw*)&curHeader->rw_data[0])->next = nextHeader >> 2
typedef struct header_info {
private:
    // 保存镜像中的头信息mach_header_64相对当前header_info的内存地址偏移
    intptr_t mhdr_offset;

    // 保存镜像中的镜像信息相对当前header_info
    // 的内存地址偏移
    intptr_t info_offset;

public:

    // 公开的API
    ...

private:
    // 虽然定义为数组,但是包含的元素基本为1个,不会超过1个。
    header_info_rw rw_data[];
} header_info;

typedef struct header_info_rw {

    // 公开的API
    ...

private:
#ifdef __LP64__
    uintptr_t isLoaded              : 1;
    uintptr_t allClassesRealized    : 1;
    uintptr_t next                  : 62;
#else
    uintptr_t isLoaded              : 1;
    uintptr_t allClassesRealized    : 1;
    uintptr_t next                  : 30;
#endif
} header_info_rw;

typedef struct objc_image_info {
    uint32_t version; // currently 0
    uint32_t flags;

#if __cplusplus >= 201103L
  private:
    enum : uint32_t {
        IsReplacement       = 1<<0,  // used for Fix&Continue, now ignored
        SupportsGC          = 1<<1,  // image supports GC
        RequiresGC          = 1<<2,  // image requires GC
        OptimizedByDyld     = 1<<3,  // image is from an optimized shared cache
        CorrectedSynthesize = 1<<4,  // used for an old workaround, now ignored
        IsSimulated         = 1<<5,  // image compiled for a simulator platform
        HasCategoryClassProperties  = 1<<6,  // class properties in category_t
        // not yet used = 1<<7

        // Swift版本兼容相关,忽略
        ...

    };
  public:
    
    // Swift版本兼容相关,忽略
    ...

  public:
    // 公开的API,忽略
    ...

#endif
} objc_image_info;

3.1.2 addHeader 具体处理流程

具体处理流程及细节注释在下列源代码中。

#define SEG_OBJC    "__OBJC"

header_info *FirstHeader = 0; 
header_info *LastHeader  = 0; 
int HeaderCount = 0;

static header_info * addHeader(const headerType *mhdr, const char *path, int &totalClasses, int &unoptimizedTotalClasses)
{
    header_info *hi;
    if (bad_magic(mhdr)) return NULL;  // 校验magic,忽略

    bool inSharedCache = false;
    hi = preoptimizedHinfoForHeader(mhdr);  // 预处理优化相关,忽略
    if (hi) {
        // 预处理优化相关,忽略
        // 在 dyld shared cache 中查找到mhdr
        // 不允许重复加载
        if (hi->isLoaded()) return NULL;
        inSharedCache = true;
        hi->setLoaded(true);
    }
    else 
    {
        // 在 dyld shared cache 中未查找到mhdr

        // 1. 不允许重复添加header_info到链表。getNext()用于获取下一个header_info节点
        for (hi = FirstHeader; hi; hi = hi->getNext()) {
            if (mhdr == hi->mhdr()) return NULL;
        }

        size_t info_size = 0;
        unsigned long seg_size;
        const objc_image_info *image_info = _getObjcImageInfo(mhdr,&info_size); // 获取镜像信息
        const uint8_t *objc_segment = getsegmentdata(mhdr,SEG_OBJC,&seg_size); // 获取__OBJC数据段
        
        // 2. 若__OBJC数据段、镜像信息为空,则直接返回NULL
        if (!objc_segment  &&  !image_info) return NULL;

        // 3. 分配内存header_info结构体需要占用的内存空间,为header_info结构体占用字节数、header_info_rw
        // 结构体的占用字节数之和,因为header_info的rw_data数组仅包含1个header_info_rw元素
        hi = (header_info *)calloc(sizeof(header_info) + sizeof(header_info_rw), 1);

        // 4. 设置 header_info 中用于定位镜像头信息的 mhdr_offset
        hi->setmhdr(mhdr);

        // 5. 设置 header_info 中用于定位镜像信息的 info_offset
        static const objc_image_info emptyInfo = {0, 0};
        hi->setinfo(image_info ?: &emptyInfo); 

        // 6. 设置镜像isLoaded为YES,表示镜像已加载
        hi->setLoaded(true);
        // 7. 设置镜像allClassesRealized为NO,表示镜像中定义的类尚未开始class realizing
        hi->setAllClassesRealized(NO);
    }

    {
        // 8. 统计镜像中包含的类的总数
        size_t count = 0;
        if (_getObjc2ClassList(hi, &count)) {
            totalClasses += (int)count;
            if (!inSharedCache) unoptimizedTotalClasses += count;
        }
    }

    // 9. 将构建的header_info添加到全局的已加载镜像链表,添加到链表末尾
    appendHeader(hi);
    
    return hi;
}

void appendHeader(header_info *hi)
{
    HeaderCount++;
    hi->setNext(NULL);
    if (!FirstHeader) {
        // 链表为空
        FirstHeader = LastHeader = hi;
    } else {
        if (!LastHeader) {
            // 找到最后一个节点
            LastHeader = FirstHeader;
            while (LastHeader->getNext()) LastHeader = LastHeader->getNext();
        }
        // 添加到最后一个节点后面
        LastHeader->setNext(hi);
        LastHeader = hi;
    }
}

objc_image_info *
_getObjcImageInfo(const headerType *mhdr, size_t *outBytes)
{
    return getDataSection<objc_image_info>(mhdr, "__objc_imageinfo", 
                                           outBytes, nil);
}

3.2 核心逻辑 _read_images

_read_images根据前面生成的header_info结构体的数组,加载镜像中定义的 Objective-C 元素,如类、分类、协议。下面的代码非常长,将其分为几个部分详细分析。从代码注释中可以清晰地知道其处理流程。

// 加载镜像中的 Objective-C 元素
void _read_images(header_info **hList, uint32_t hCount)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t I;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);

    runtimeLock.assertLocked();

#define EACH_HEADER \
    hIndex = 0;         \
    crashlog_header_name(nil) && hIndex < hCount && (hi = hList[hIndex]) && crashlog_header_name(hi); \
    hIndex++

    // 1. 首次运行的初始化配置,注意该逻辑块内的代码全局只执行一次
    if (!doneOnce) {
        doneOnce = YES;

        // 1.1 配置 isa 类型支持相关,忽略
        ...

        // 1.2 配置 tagged pointer 支持相关,忽略
        ...

        // 1.3 初始化gdb_objc_realized_classes
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotal : total) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        
        // 1.4 初始化allocatedClasses哈希表用于保存已完成内存分配的类及元类,完成内存
        // 分配是指完成类的class_rw_t内存分配
        allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
    }

    // 2. 发现类,代码见<3.2.1>
    ...

    // 3. 类重映射,代码见<3.2.2>
    ...

    // 4. 获取__objc_selrefs数据section中定义的选择器,添加到namedSelectors哈希表,忽略
    ...

#if SUPPORT_FIXUP
    // 5. 获取__objc_msgrefs数据section中定义的消息,添加消息的选择器到namedSelectors
    // 哈希表,若其IMP指向预定义的IMP,则需要保证其指向新版本runtime定义的IMP,忽略
    ...

#endif

    // 6. 发现协议,忽略
    ...

    // 7. 协议排序,忽略
    ...

    // 8. 认识非懒加载类,代码见<3.2.3>
    ...

    // 9. 认识懒加载类,代码见<3.2.4>
    ...

    // 10. 发现分类,代码见<3.2.5>
    ...

#undef EACH_HEADER
}

3.2.1 发现类

_read_images(...)中以下代码用于发现类。实际上是构建 future class 的实体类(remapped future class/resolved future class)的过程,可以简称为 future class 解析(future class resolving)。具体步骤是:

  • 首先,通过_getObjc2ClassList(...)读取header_info指向的镜像中的__objc_classlist数据 section,以提取镜像中定义的所有类;
  • 然后,调用readClass(...)读取镜像中对应 future class(保存在全局的future_named_class_map哈希表中)的类的元数据,构建 future class 的实体类(remapped future class),并将实体类添加到remappedClasses全局哈希表中;
  • 最后,若readClass(...)返回的类不等于传入的类,则说明该类已被重映射,将返回的类保存到已解析 future class 局部变量resolvedFutureClasses中。
    for (EACH_HEADER) {
        classref_t *classlist = _getObjc2ClassList(hi, &count);
        
        if (! mustReadClasses(hi)) {
            // 若镜像有经过优化,则不需要调用readClass()
            continue;
        }

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->isPreoptimized();

        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[I];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

            // 若readClass返回的类非空且不等于直接从header_info读出来的类则该类为future class
            if (newCls != cls  &&  newCls) {
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }

3.2.2 类重新映射

_read_images(...)中以下代码用于类重映射。若发现类阶段构建了 future class 的实体类,则镜像中 原指向 future class 的类引用 以及父类引用 需要指向实体类,这就是类重映射过程。具体过程如下:

  • 调用_getObjc2ClassRefs(...)函数获取镜像中__objc_classrefs数据 section 中保存的所有类引用;
  • 遍历所有类引用:调用remapClassRef(Class *clsref)重映射类引用。内部逻辑是当检测到remppedClass哈希表内Key为*clsref的Value值newCls不等于*clsref时,表示该类需要重新映射,将clsref指向newCls
  • 调用_getObjc2SuperRefs(...)函数获取镜像中__objc_superrefs数据 section 中保存的所有父类引用;
  • 遍历所有类引用:调用remapClassRef(Class *clsref)重映射父类引用。
    // 若发现类阶段有处理 future class
    if (!noClassesRemapped()) {
        for (EACH_HEADER) {
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {
                // 重新映射镜像中定义的类的引用
                remapClassRef(&classrefs[I]);
            }
            
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {
                // 重新映射镜像中包含的 super
                remapClassRef(&classrefs[I]);
            }
        }
    }

3.2.3 认识非懒加载类

完成 future class 解析和类重映射后,需要加载的类的内存地址即可确定。此时需要对需要加载的类进行 class realizing 以确定其class_rw_t数据的内存地址,以及确定类的对象内存布局。非懒加载类的 class realizing 的代码如下,通过调用realizeClassWithoutSwift(...)函数实现:

    for (EACH_HEADER) {
        classref_t *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            // 模拟器环境特殊配置,忽略
            ...
            
            addClassTableEntry(cls);

            // Swift环境兼容相关,忽略
            ...

            realizeClassWithoutSwift(cls);
        }
    }

3.2.4 认识 resolved future class

继续进行已解析的 future class 的 class realizing 过程,代码如下:

    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            realizeClass(resolvedFutureClasses[I]);
            resolvedFutureClasses[i]->setRequiresRawIsa(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }    

3.2.5 分类发现

分类中扩展类的遵循协议列表、属性列表、方法列表需要在运行时添加到类的class_rw_t数据中,该过程必须在类的内存地址、class_rw_t内存地址、类的对象内存布局确定后方能进行。大致过程如下:

  • 调用_getObjc2CategoryList(...)函数获取镜像的__objc_catlist数据 section 中保存的镜像中定义的分类列表;
  • 遍历分类列表;
  • 若分类所扩展的类为空则报错,并直接处理下一个分类;
  • 若分类扩展元素包括协议列表或实例属性或实例方法,则将调用addUnattachedCategoryForClass(...)将分类添加到扩展类的待处理分类哈希表,然后调用remethodizeClass(...)将协议列表或实例属性或实例方法元素添加到扩展类的class_rw_t数据中;
  • 若分类扩展元素包括协议列表或类属性或类方法,则将调用addUnattachedCategoryForClass(...)将分类添加到扩展类的元类的待处理分类哈希表,然后调用remethodizeClass(...)将协议列表或实例属性或实例方法元素添加到扩展类的元类的class_rw_t数据中;
for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[I];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // 分类所扩展的类为空则报错
                catlist[i] = nil;
                continue;
            }

            // 协议、实例方法、实例属性添加到类的class_rw_t中
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
            }

            // 协议、类方法、类属性添加到类的元类的class_rw_t中
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
            }
        }
    }

四、初始化类库load_images

第三章介绍的map_images用于汇总镜像信息,并加载其中定义的 Objective-C 元素,load_images的逻辑基于map_images所加载的类、分类信息进行。load_images主要调用了两个函数:

  • prepare_load_methods(...):执行类、分类的load方法进行类的初始化之前的准备工作,主要是收集镜像中实现了load方法的类和分类;
  • void call_load_methods(void):调用类、分类中的load方法初始化类;

prepare_load_methodscall_load_methods是成对存在的两个操作,前者记录实现了load方法的类、分类,后者按正确的顺序调用所记录的类和分类的load方法。

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // 若镜像中的所有类、分类均未实现load方法,则直接返回
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // 收集镜像中实现了load方法的类和分类
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // 执行prepare_load_methods收集的类和分类的load方法
    call_load_methods();
}

4.1 prepare_load_methods函数

分析prepare_load_methods函数逻辑前,首先看两个数据结构loadable_classloadable_category分别表示包含load方法的类和分类。声明两个静态数组容器loadable_class的数组loadable_classes,以及loadable_category的数组loadable_categories,分别用于记录包含了load方法但尚未调用load的类和分类。prepare_load_methods的主逻辑就是操作上述两个数组容器。

loadable_classes数组支持动态扩容,占用内存存在冗余空间,因此不仅需要loadable_classes_used记录loadable_classes实际保存的loadable_class结构体数量,还需要loadable_classes_allocated记录为数组分配的用于保存loadable_class结构体的内存单元数量。loadable_categories同理。

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

// 数组容器,记录包含load方法的所有类的信息
static struct loadable_class *loadable_classes = nil;
//数组内存中存在冗余空间,用loadable_classes_used实际保存的单元数量
static int loadable_classes_used = 0;  
//数组内存中存在冗余空间,因此用loadable_classes_allocated记录分配的单元数量
static int loadable_classes_allocated = 0; 

// 数组容器,记录包含load方法的所有分类的信息
static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;

prepare_load_methods包含两大步骤:

  • 获取镜像中所有非懒加载类,依次调用schedule_class_load(...)函数将包含load方法的类添加到loadable_class数组容器。schedule_class_load(...)函数内部,在调用add_class_to_loadable_list(cls)将类loadable_class数组容器前,递归调用了schedule_class_load(cls->superclass),以保证父类的load方法先于子类load方法执行;
  • 获取镜像中所有非懒加载分类,依次调用add_category_to_loadable_list(...)函数将包含load方法的类添加到loadable_category数组容器。注意在这之前需要保证分类所扩展的类完成 class realizing;
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, I;

    runtimeLock.assertWriting();

    // 获取二进制文件中,非懒加载的所有类的信息
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    // 获取二进制文件中,非懒加载的所有分类的信息
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

// ------- 将类添加到loadable_classes数组 ------- //
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized()); 

    if (cls->data()->flags & RW_LOADED) return;

    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  

    // loadable_classes数组扩容,因此loadable_classes数组中是存在冗余空间的,这是loadable_classes_allocated存在原因
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

// ------- 将分类类添加到loadable_categories数组 ------- //
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);
    if (!method) return;
    
    // loadable_categories数组扩容
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

4.2 call_load_methods函数

分析完prepare_load_methods的逻辑,其实call_load_methods的逻辑已经比较明显了。就是从loadable_classes容器及loadable_categories容器中推出类和分类,依次调用load方法。

call_load_methods(void)代码的逻辑比较怪异,在do-while循环内部的while循环明明已经判断loadable_classes_used <= 0,为什么在do-while还要判断loadable_classes_used > 0进入下一次迭代?这是因为类、分类的load方法中,均可能存在动态加载镜像文件的逻辑,从而引入新的类、分类的load方法。do-while循环内部,执行类的load方法使用了一个while循环,而执行分类的load方法则只调用了一次,这是因为分类load方法必须等待其扩展类的load方法执行完毕才能执行,因此需要立即进入下一次迭代以执行扩展类的load方法。

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. 遍历并执行类的所有可调用的load方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 执行分类的load方法
        more_categories = call_category_loads();

        // 3. 循环直到类及分类的所有load方法均被执行
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

4.2.1 执行类的load方法

由于在prepare_load_methods已经确定了类的load方法执行顺序,因此call_class_loads(void)仅需简单迭代执行loadable_class中的load方法即可。处理过程大致如下:

  • 将局部变量classes指向loadable_classes,将loadable_classes指向nilclasses表示本次需要执行的所有类的load方法,为旧容器。loadable_classes表示本次执行的类的load方法中动态载入的所有新类的load方法,为新容器;
  • 遍历classes中所有loadable_class结构体,执行其method所指向的load方法。遍历classes时,若load方法中载入了新的类的load方法,则又会被收集于loadable_classes所指向的新容器中;
  • 释放classes局部变量所指向的旧容器内存空间;
static void call_class_loads(void)
{
    int I;
    
    struct loadable_class *classes = loadable_classes;
    // loadable_classes指向nil
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
   
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        // 执行load方法。load方法可能包含动态加载镜像的逻辑,此时loadable_classes则会指向
        // 新的容器来收集动态加载镜像中的load方法
        (*load_method)(cls, SEL_load); 
    }
    
    if (classes) free(classes);  // 释放旧容器
}

4.2.2 执行分类的load方法

执行分类的load方法的逻辑比类稍微复杂。处理过程如下:

  • 局部变量cats指向loadable_categories表示旧容器。loadable_categories指向nil表示新容器;
  • 遍历旧容器中的所有loadable_category结构体,若loadable_categorycls成员非空且可加载,则执行method成员指向的load方法,并把cat成员置nil
  • cats收集 旧容器中未执行load方法的所有分类(判断cat成员非空);
  • cats收集 执行旧容器的load方法过程中动态载入的所有分类;
  • cats保存的loadable_category结构体数量大于0,则设置loadable_categories指向cats所指向的内存空间;反之loadable_categoriesnil
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {  //cls->isLoadable()恒为YES
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // 收集上面for循环未执行load方法的所有分类,其中包含了load方法中可能存在动态加载
    // 镜像时载入的分类的load方法,这些load方法不能立刻执行,需要其扩展类的load方法
    // 执行完毕后才能执行。
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[I];
        } else {
            shift++;
        }
    }
    // loadable_categories旧容器中尚未执行load方法的loadable_category结构体数量,这些
    // loadable_category均保留在loadable_categories新容器
    used -= shift;  

    // 若loadable_categories_used大于0,说明在执行分类load方法时收集到新的分类load方法
    new_categories_added = (loadable_categories_used > 0);

    // 将新收集的分类load方法添加到loadable_categories新容器
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[I];
    }

    // 释放旧loadable_categories容器
    if (loadable_categories) free(loadable_categories);

    // 赋值新loadable_categories容器
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    return new_categories_added;
}

五、总结

  • 应用加载的本质是加载应用沙盒中的可执行文件(也是目标文件、镜像文件),镜像文件可以映射到内存空间生成镜像。镜像中包含了头信息、加载命令、数据区,镜像中定义的 Objective-C 元素的元信息主要保存在__DATA数据段、__CONST_DATA数据段中。例如__objc_classlist保存镜像中定义的所有类、__objc_catlist保存镜像中定义的所有分类;

  • 生成镜像后,触发map_images回调读取镜像中定义的 Objective-C 元素的元信息,主要包括发现类、发现协议、认识类、将分类中的方法添加到类、元类的class_rw_t的方法列表中;

  • 完成镜像绑定后,触发load_images收集需要调用的类及分类的load方法,其优先级是静态加载的父类>静态加载的子类>静态加载的分类>动态加载的父类>动态加载的子类>动态加载的分类;

  • 下一篇介绍 runtime 实现基于引用计数的内存管理机制。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容