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

iOS类加载流程(五):read_images流程分析

作者: 康小曹 | 来源:发表于2022-06-27 15:39 被阅读0次

    read_image 方法内部大概做了这么几件事:

    1. 一些初始化操作;
    2. 映射 Class、SEL、Protocol;
    3. Class 的映射方式是保存编译器静态数据的指针,SEL 映射的主要形式是保存 name,Protocol??
    4. 映射完毕之后,对类进行实现,动态链接阶段只实现非懒加载类;
    5. 类实现之后,处理分类逻辑,对原来的类进行补充;

    read_image 这个方法的逻辑遵循一个原则,这个原则是围绕着 objc 的核心思想:消息转发

    OC 中,大部分代码的访问都是通过 objcMsgSend 或者 objcSuperMsgSend 来进行寻找和分发;

    分发需要三个关键点:

    1. 信号(方法名);
    2. 转发器(objcMsgSend);
    3. 方法实际拥有者(类/元类)

    所以,这个方法首先对所有的方法进行了映射,然后对所有的类进行了映射,还映射了协议相关内容。这样就完成了三个步骤中的前两步,即:消息最终可以转发到对应的类上了。紧接着这个方法对类进行了实现,最后完成分类的解析,在原来的类的基础上进行了完整的补充。

    1. 一些初始化操作

    read_image 中会通过传递过来的 header list 来递归处理所有的 image ,在这之前会首先进行一些初始化逻辑:

    if (!doneOnce) {
        doneOnce = YES;
        
        // 高版本iOS中一定是开启
        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }
        
        // 初始化TaggedPointer混淆器
        initializeTaggedPointerObfuscator();
    
        // namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        // totalClasses:遍历image并通过_getObjc2ClassList获取
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        
        // GDB:GNU symbolic debugger,Linux调试器
        // exported for debuggers in objc-gdb.h
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        
        allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
        
        ts.log("IMAGE TIMES: first time tasks");
    }
    

    这里的代码其实有点云里雾里,首先 TaggedPointer 在高版本 iOS 中必定是开启的,不会 disable,所以不需要关注 disableTaggedPointers

    然后, initializeTaggedPointerObfuscator 是初始化 TaggedPointer 混淆器,本质上是生成了一个随机数,代码比较简单如下:

    static void
    initializeTaggedPointerObfuscator(void) {
        if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
            // Set the obfuscator to zero for apps linked against older SDKs,
            // in case they're relying on the tagged pointer representation.
            DisableTaggedPointerObfuscation) {
            // 比较老的SDK要关闭混淆器,以防老的SDK依赖TaggedPointer?
            objc_debug_taggedpointer_obfuscator = 0;
        } else {
            // Pull random data into the variable, then shift away all non-payload bits.
            arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                           sizeof(objc_debug_taggedpointer_obfuscator));
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        }
    }
    

    这里不是很明白具体的应用场景,只能大致猜一下。

    因为使用到 Tagged Pointer 的类一般是 NSNumber、NSString 之类的。内容上,有一些标志位来让 objc 知道这个指针是 Tagged Pointer,其他的比特位则存储具体的值,这样就不需要再堆内存中开辟空间,然后再使用一个指针指向这个堆内存了。使用上直接利用这个 Tagged Pointer 就可以知道对应的值,比如一个 Number 的具体数值,或者一个字符串的具体数值。

    那么 TaggedPointer 必定会有一些规则:

    1. 判断是否属于 TaggedPointer;
    2. 判断这个 TaggedPointer 是什么类型,NSNmuber,还是 NSString 等;
    3. 知道了类型后,值是怎么通过其他的比特位来计算的?

    上述这些规则如果过于简单,攻击者可能可以直接通过这些规则来反推出变量的数值,进而获取一些信息。所以,这个对 TaggedPointer 做了混淆,每次打开 App 时都不一样,有点类似于 Slide 的作用;

    紧接着,又是创建了一个 NXCreateMapTable,名称为 gdb_objc_realized_classes

    而 dgb 的全称是 GNU symbolic debugger,也就是 Linux 平台下的调试器,难道这个表和调试有关系?

    这里注释上给予了纠正:

    // This is a misnomer: gdb_objc_realized_classes is actually a list of 
    // named classes not in the dyld shared cache, whether realized or not.
    NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h
    

    misnomer 是用词不当的意思,注释说的很清楚了, gdb_objc_realized_classes 这个全局变量实际上是共享缓存之外的一个类数组,无论这个类是否被实现,都会存在于这个数组中。

    最后,创建了一个哈希表 allocatedClasses,这个后续很多地方会用到,正好和上面的全量数组对应,这里存储已经被分配内存空间的类。

    总结:

    1. 初始化 TaggedPointer 混淆器;
    2. 创建了两个 MapTable,一个用于全量存储 Class,另外一个存储已经 realized 的类;

    2. 类映射 - 概览

    这个阶段的代码如下:

    for (EACH_HEADER) {
        classref_t *classlist = _getObjc2ClassList(hi, &count);
        
        if (! mustReadClasses(hi)) {
            // Image is sufficiently optimized that we need not call 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);
    
            if (newCls != cls  &&  newCls) {
                // Class was moved but not deleted. Currently this occurs
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses,
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }
    

    上述代码有三个重点:

    1. mustReadClasses:条件与判断;
    2. readClass:类的映射;
    3. future class :如果是 future class,则进行 append 和 realloc 操作;

    future classes 主要用于 CF 和 NS 类的桥接。因为 future classes 和 普通类的 realizeClassWithoutSwift 逻辑是相反的,暂不关注,后面会有专门的一期(也可能并不会有);

    这段代码首先获取了 classlist 列表。该列表是从 mach-O 中的 __objc_classlist 读取,这个 section 存储的就是该 image 中声明的类,是不包括引用的外部类的。而__objc_classrefs__objc_superrefs 中存储的是引用到的所有的类名,也包括外部的,如果是内部的,会指向内部地址:

    mach-o

    上图中,三个 ViewController 都继承自 UIViewController

    MachORuntimeArchitecture 中没有写 objc 相关的 section,也没有找到 __objc 相关的官方文档。所以 objc 相关的 section 只能靠自己根据实际情况去猜,可能会不准确。

    即:该方法首先读取了 image 内部声明的所有类;

    上述代码重点比较多,需要分开来看~~~

    3. 类映射 - 条件预判断(Preflight)

    先来看看 mustReadClasses

    很明显,注释中写了:Image is sufficiently optimized that we need not call readClass()。也就是说,如果这个函数返回 NO,那么证明这个 image 中的类已经被充分优化过了,不需要再读取。

    那么 mustReadClasses 内部逻辑是怎样的呢?先看看代码:

    /***********************************************************************
    * mustReadClasses
    * Preflight check in advance of readClass() from an image.
    **********************************************************************/
    bool mustReadClasses(header_info *hi)
    {
        const char *reason;
    
        // If the image is not preoptimized then we must read classes.
        if (!hi->isPreoptimized()) {
            reason = nil; // Don't log this one because it is noisy.
            goto readthem;
        }
    
        assert(!hi->isBundle());  // no MH_BUNDLE in shared cache
    
        // If the image may have missing weak superclasses then we must read classes
        if (!noMissingWeakSuperclasses()) {
            reason = "the image may contain classes with missing weak superclasses";
            goto readthem;
        }
    
        // If there are unresolved future classes then we must read classes.
        if (haveFutureNamedClasses()) {
            reason = "there are unresolved future classes pending";
            goto readthem;
        }
    
        // readClass() does not need to do anything.
        return NO;
    
     readthem:
        if (PrintPreopt  &&  reason) {
            _objc_inform("PREOPTIMIZATION: reading classes manually from %s "
                         "because %s", hi->fname(), reason);
        }
        return YES;
    }
    

    这个函数是用来做预检验,和后文的 readClass 内部的逻辑对应。该函数主要是做一些逻辑判断,最终返回 YES 或者 NO,其判断逻辑有这么几层:

    1. 是否被优化过,如果没有则返回 YES,即需要进行类的 read 操作;
    2. 有些类缺失了弱引用的父类,此时返回 YES,需要进行类的 read 操作;
    3. 如果有 FutureClass,则返回 YES,需要进行类的 read 操作;
    4. 上述情况都没有时,返回 YES;

    预校验通过之后才会进入到真正的类映射,readClass的注释如下:

    该方法注释如下:

    readClass

    从注释可以看到,该方法的目的是读取编译器生成的类的静态数据,会出现三种情况:

    1. 正常的类返回 cls,也就是静态数据在内存中的指针;
    2. 弱链接的类返回 nil;
    3. future class 返回预留的内存地址;

    4. readClass-弱连接

    我们先看第一部分,弱链接:

    Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
    {
        const char *mangledName = cls->mangledName();
        
        if (missingWeakSuperclass(cls)) {
            // No superclass (probably weak-linked). 
            // Disavow any knowledge of this subclass.
            addRemappedClass(cls, nil);
            cls->superclass = nil;
            return nil;
        }
        ......
    }
    

    这部分代码是弱链接父类的判断。

    missingWeakSuperclass 函数内部会递归判断类的 superclass 是否为空:

    missingWeakSuperclass

    因为在 OC 中的跟类的 superClass 为空,也就是 NSObject 类对象。所以如果类的 superClass 为空且该类不是根类,那么这个类就属于缺失了 SuperClass,有可能是弱引用缺失。

    所以,这里的逻辑总结起来:

    1. 非根类且缺失了 superClass 则表示可能为弱链接缺失;
    2. 弱链接缺失时,将 cls 的映射置为 nil;
    3. 弱链接缺失映射的 Map 和 FutureClass 是同一个 Map,而不是保存在映射普通类的两个 Map 中;

    这里感觉弱链接缺失应该是在处理系统库的版本兼容问题。因为弱链接在我们业务代码中基本不会使用到,而 Future Class 又是和 CF 相关的一些类,或者是被预先添加了一些数据的系统类。而他们量又都映射在同一个 Map 中,所以,感觉这个行为应该也是处理系统类的行为、

    弱引用一般用于版本兼容,这里如果是缺失了,可能会有将这个类置为 nil 之类的逻辑吧,不深究了, weak symbol 相关内容详见:dyld:启动流程解析

    根据注释,此时这个类可能就是 future class。这里大部分情况下都为 YES,不会进入到这个逻辑。

    弱引用看来还是在系统库之间的引用比较多,这些逻辑已经被封装过了,所以我们的实际主工程基本用不到这些。

    5. 类映射 - future class 的映射

    swift 的逻辑也先不看,接下来就是对 future class 的一些处理:

    if (Class newCls = popFutureNamedClass(mangledName)) {
        // This name was previously allocated as a future class.
        // Copy objc_class to future class's struct.
        // Preserve future's rw data block.
        
        // 为什么是rw?如果是从静态数据来的,那么应该是 ro
        // 根据注释,这里rw应该有一些数据的,newCls不是我们熟悉的编译器生成的静态ro,而是系统预留出来的内存,内部提前写入了一些数据
        class_rw_t *rw = newCls->data();
        // 原本的ro可能为空
        const class_ro_t *old_ro = rw->ro;
        // 拷贝静态数据cls到newCls,newCls这里应该就是一个指针
        memcpy(newCls, cls, sizeof(objc_class));
        // 替换静态数据,仍然以编译器生成的为准
        rw->ro = (class_ro_t *)newCls->data();
        newCls->setData(rw);
        
        freeIfMutable((char *)old_ro->name);
        free((void *)old_ro);
        
        addRemappedClass(cls, newCls);
        
        replacing = cls;
        cls = newCls;
    }
    

    这里大概看下,根据之前的文章,我们知道编译器生成的类相关的静态数据会在 realizeClassWithoutSwift 中被赋值给 rw,进而赋值给 class,完成类的初始化,而且 rw 也是在这个函数中被 alloc 的:

    rw初始化

    而 future class 这里的处理首先是:

    1. newCls 是系统提前预留的一段内存,内部写入了一些数据,但是 ro 可能是空的,仍然要以编译器数据为准;
    2. 获取到 newCls 的指针,并且获取了 rw 和 ro 的地址;
    3. 使用编译器的静态数据覆盖 newCls;,这里本质上仍然是 ro 的赋值;
    4. 修复 rw 中 ro 的指向;
    5. 将 rw 赋值给 newCls,这里最关键的是保留 rw 中除了 ro 之外的数据;

    从注释中也可以看到,这么做是为了保留 future class 中 rw 的数据。而 rw 中的 ro 是从 cls 来的,所以保留的数据应该是 rw 中除了 ro 的其他部分:

    rw数据

    所以,这里做个猜测:future class 是系统提前做好了一些初始化操作的类。future class 数据的完整性包括两部分:系统提前植入的数据 + 静态编译器生成的数据。

    其实在后面的过程中,对 future class 还做了两个操作:

    1. resolvedFutureClasses 元素加 1 并且重新生成;
    2. 和普通的类一样,进行了 realizeClassWithoutSwift 操作;

    总结:future class 最关键的是 rw 中除了 ro 之外的数据,可能就是 objc 为系统的类添加了一些特殊的方法、属性或者标志位(如 CF 的桥接相关方法?)。这些类的实现依赖两部分数据,包括系统提前植入的数据和静态时期编译器生成的静态数据。

    注意,future class 的映射使用的是 addRemappedClass,和一般的类存入的地方不一样。一般的类,无论是否 realize,都会先存入这 gdb_objc_realized_classes 中;

    6. 类映射 - 普通类的映射

    至此,readClass 的代码就剩最后一部分了,省略部分代码之后,普通类的处理逻辑如下:

    addNamedClass(cls, mangledName, replacing);
    addClassTableEntry(cls);
    
    // for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) {
        cls->data()->flags |= RO_FROM_BUNDLE;
        cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
    }
    
    return cls;
    

    上述代码中,一般 image 都不会是 Bundle(MH_BUNDLE),所以关键就在于:

    addNamedClass(cls, mangledName, replacing);
    addClassTableEntry(cls);
    

    addNamedClass 代码如下:

    addNamedClass

    addNonMetaClass 的方法是对 secondary metaclass 进行处理,这里搞了半天没明白这是个啥,但是可以通过断点的形式来确定不会走到这里,最终会进入到 NXMapInsert 的逻辑:

    NXMapInsert

    gdb_objc_realized_classes 就是上文提到过的 MapTable,无论 class 是否被 realize,都会先存储到这里。allocated 之后存入 allocatedClasses

    再来看看 addClassTableEntry

    image.png

    allocatedClasses 表只会存储 objc_allocateClassPair 分配内存的 class ,所以这里自然都不会进入到 INsert 操作。可以通过汇编代码验证:

    tbz/tbnz 未进行insert操作

    所以,这一步就是将类添加到了上文提到的 gdb_objc_realized_classes 表中,本质上仍然是映射操作;也就是说,对于普通类,就是将静态数据的 cls 在内存中的地址映射到了 gdb_objc_realized_classes 表中。

    7. 类映射 - 总结

    至此,readClass 流程分析完毕,做下总结:

    1. 通过 __objc_classlist 获取到所有的类;
    2. 预检验是否需要进行 readClass,一般的类都是未进行预优化的,所以都需要 read;
    3. 系统为 future class 在 rw 中预先添加了一些数据,比如属性、方法等;
    4. future class 在 read 阶段获取到了静态数据 ro ,进而在后面的步骤中通过 realize 操作补充静态时期编译器生成属性、方法、协议等数据;
    5. future class 会被添加到 remapped_class_map 表和 resolvedFutureClasses 数组中;
    6. 普通类会被添加到 gdb_objc_realized_classes 表中;
    7. 这一步完成所有类的映射,主要信息为 className 和 class 的指针。class 指针指向的就是编译器生成的静态数据被加载到内存后的地址。后续完成 realize 操作之后会替换这个指针为新的 class 地址,即 runtime 中的 class。
    8. 这一步就是为下一步 realize 打下铺垫;

    总而言之,readClass 这一步完成了 image 中所有类的静态数据的映射。

    8. 修复类映射

    这部分代码如下:

    if (!noClassesRemapped()) {
        for (EACH_HEADER) {
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[i]);
            }
            // fixme why doesn't test future1 catch the absence of this?
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[i]);
            }
        }
    }
    

    这部分代码就是获取 mach-o 中的 __objc_classrefs__objc_superrefs 表中的数据。__objc_classrefs 中的数据表示所有内部和外部 class 的引用,而和 __objc_superrefs 中的存储这有继承关系的类;

    这里的 ref 是引用,静态时期是 0 或者指向内部:

    ref

    上述代码有个重点:使用的是 remap 相关的方法。所以,需要特别注意,这里的逻辑是处理 Map 相关的类。

    那为什么需要处理呢?

    对于普通类,只是单纯地使用 Insert 操作记录到 `` 表中。而对于 remap 的表,其对应关系是 cls(key) ---> newCls(Value)。

    如果是 future class,或者说 remap 表中有值时,原本的指针(cls)是从 Mach-O 中获取并映射到内存中的。但是如果 remap 表中有值,比如弱引用置空了这个指针的指向,再比如 Future Class 中的 newCls 是系统提前预留的内存空间。这些指针的指向(Value)发生了变化,而动态链接 bind 完成之后,mach-o 中的引用指向的仍然是 cls,也就是静态编译器生成的数据。

    所以,这里 objc 需要对这些 ref 进行重置(可以理解成 rebind),重置为 remap 的新的 newCls 的地址,这样不仅能使用到 objc 精心为这些类添加的数据,还能通过 ro 访问静态编译器生成的数据。

    其关键代码在于 remapClassRef 函数:

    remapClassRef

    上述代码就是 cls -> newCls 的关键代码;

    总结下来,这一步做了这些事:

    1. 通过 noClassesRemapped 来判断是否有需要重新映射的类;
    2. 如果有,则将 这两个表中的类作为 key,去 remapped_class_map 中查找;
    3. 找到了新的指针,就将这个指针进行替换;

    至此,所有的 class 都完成了映射~~~

    从这里可以看出来,类相关的 Map 有三个:gdb_objc_realized_classes 全量保存普通类,allocatedClasses 保存已经被 realized 的类、remap 表以 key-value 的形式映射到 newCls;

    9. 方法映射(方法注册)

    方法映射相对简单,代码如下:

    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->isPreoptimized()) continue;
            
            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                sels[i] = sel_registerNameNoLock(name, isBundle);
            }
        }
    }
    

    上面代码先获取到了 __objc_selrefs 中的 SEL,然后进入到了 sel_registerNameNoLock 函数。这个函数之前提到过,初始化系统函数时就是用到了这个函数,期待吗如下:

    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. search_builtins 中查找已经被预优化处理过的函数,如果存在则不再处理;
    2. 已经被映射到 namedSelectors 中的函数不再处理;
    3. namedSelectors 未创建时创建;
    4. 为 SEL 分配内存并存储到 namedSelectors 中;

    sel_alloc 如果不需要 copy,返回的就是 __objc_selrefs 中的字符串指针:

    __objc_selrefs

    如果这个 image 是 Bundle 类型,则 copy 为 YES,逻辑也是重新分配内存并拷贝这个方法名字符串。

    总之,这里就是将方法名进行映射保存,没什么需要特别关注的。

    关于 search_builtins 详见之前的文章:iOS类加载流程(四):map_images流程分析

    9. 协议 - 概览

    协议的处理代码如下:

    // Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        // 引入一个结构体
        extern objc_class OBJC_CLASS_$_Protocol;
        // 强制类型转换
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        
        assert(cls);
        
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->isPreoptimized();
        bool isBundle = hi->isBundle();
    
        // 指向指针的指针,数组内存存储的是协议结构体的指针
        protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
        
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map,
                         isPreoptimized, isBundle);
        }
    }
    

    这段代码做了几件事:

    1. 引入了一个结构体,并被传入了在后面的 readProtocol 函数;
    2. 除了常规的是否预优化、是否为 Bundle 之外,还获取到了存储 protocol 的 map;
    3. 通过 __objc_protolist 获取到了协议列表,列表内存存储这指向结协议结构体的指针;

    10. 协议 - 静动态数据流

    这里首先来看下 __objc_protolist

    __objc_protolist

    很明显,我们看到了项目中的一些协议,但是这里有个疑问,如果是自己定义的协议,那存储在本 Mach-O 中,列表中有指针是很正常的,但是为什么系统(动态库)的结构体也有指针指向?比如 NSObject的,我们来找一下这个 data 的位置:

    __data

    我们直接来找 _protocol_t 的第二个属性,协议的名称。因为 iOS 是小端模式,即:低内存存储低位地址,所以这里存储的地址是:0x1000003524,这个位置的数据如下:

    __classname

    如上图,这个地方存储的就是 NSObject ,也就是协议名。其实在 MachOView 中直接就可以看得到,还有其他数据都可以直接看到,很直观:

    __data

    _protocol_t 的结构体如下:

    _protocol_t

    而在源码中,协议是使用 protocol_t 这个结构体来表示的:

    protocol_t

    再来看看 objc 转化成 C/C++ 的静态代码:

    _OBJC_PROTOCOL_NSObject

    转译代码时,只声明 Protocol 并不会产生转译代码,需要真正使用 Protocol 才会有对应的代码。

    而我们知道,objc_object 内部就一个 isa 成员属性。至此,静态数据、runtime 结构体、mach-O 文件都对应上了。

    也就是说,这个 NSObject 的协议真的是有数据的,而不是指向外部,等待动态链接时被 rebind。

    先来梳理一下这个数据流:

    1. protocol 列表中存储着 _OBJC_PROTOCOL_NSObject 结构体的地址;
    2. _OBJC_PROTOCOL_NSObject 中的属性 protocol_name 就是协议的名称,存储在 __objc_classname 中;

    上面一定要用 iPhone 真机进行测试,如果是模拟器,因为没有指针优化,所以会有很大差别。

    因此可以得出结论:

    • 静态编译器在识别到 @protocol 之后就会生成协议对应的静态结构体,而没有动态链接时 rebind 操作;

    这个结论可以通过两个方面来验证:

    1. 观察 objc 转译后的代码,确实存在协议的一些实现:
    静态数据

    NSObject 协议的部分如下:

    NSObject
    1. 可以自己写一个系统的 Protocol,再观察转译后的代码:
    自定义协议 转译代码

    如上图,属于 <UIKit> 的 UIApplicationDelegate 协议在静态时期直接被覆盖了。

    那么这里就有个疑问了,既然上层业务代码可以直接覆盖系统库声明的协议,那这样不会有什么问题吗?这里先留个疑问,后文会讲到。

    总结:协议处理的第一步和 Class 是完全一样的,协议和类的静态数据都是来源于编译器。

    11. 协议 - 协议重复的处理方式

    现在我们回到源码,直接进入到 readProtocol 函数。

    这个函数的代码不少,就不直接贴了。代码主要是几个 if else 的判断,先来看第一分支:

    static void
    readProtocol(protocol_t *newproto, Class protocol_class,
                 NXMapTable *protocol_map, 
                 bool headerIsPreoptimized, bool headerIsBundle)
    {
        // This is not enough to make protocols in unloaded bundles safe, 
        // but it does prevent crashes when looking up unrelated protocols.
        auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
    
        protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
    
        if (oldproto) {
            // Some other definition already won.
            if (PrintProtocols) {
                _objc_inform("PROTOCOLS: protocol at %p is %s  "
                             "(duplicate of %p)",
                             newproto, oldproto->nameForLogging(), oldproto);
            }
        } 
    ......else if(xxx)......
    ......省略.......
    

    可以看到,该方法首先通过 getProtocol 来获取已经被映射协议。而保存协议的 Map ,也就是方法的入参都是通过 protocols() 获取的。

    这里一般情况下获取到的都为 nil,但是如果协议已经被映射,那么就进入到了第一个分支。

    通过代码和注释可以很清楚的看到,如果这个协议已经被定义过,那么直接输入一些日志,不作任何处理;

    这里就需要在此剔除刚刚的疑问:编译器识别到协议,会直接生成协议对应的结构体,重复定义时,后者覆盖前者。

    编译器编译文件的先后顺序由 Xcode 中的文件顺序决定。但是如果是系统的动态库和业务主工程存在歇息重复的情况时,因为给到 objc 的 image 也是有顺序的。案例来说,主 image 排在第 0 位,libobjc.A.dylib 拍在后面,那么按照代码的逻辑,主工程中协议岂不是优先级更高?

    其实不是的,给到 objc 的 images 已经被重新排过序了,相反的,主 image 拍在最后,也就是到最后才会执行 objc 中的整个流程。

    如何验证,很简单,符号断点看看 images 的入参就好了。这里把断点打到 map_images_nolock() 这个方法上:

    image.png

    这个断点这么打有几个关键点:

    1. map_images_nolock 的入参有 mhPaths,可以看到所有的 image 对应的路径和名称;
    2. map_images_nolock 中的数组是 dyld 最后调用 objc 回调时的方法,给到的数据比较原始;
    3. map_images_nolock 使用的是 while(i--),所以是倒叙添加 header 的;

    断点之后,因为入参是存储在 x0-x8 这写寄存器中的,所以首先读取 x1 寄存器的值:

    寄存器

    然后,直接打印这个数组中的元素:

    image.png

    这里需要注意的是,因为 C 语言中,字符的类型是 char,而字符串是由多个 char + \0 组成的,所以字符串本身就是数组。而数组使用指针来指向的,所以字符串对应的类型是 char *

    而 paths 是由多个字符串组成的数组,字符串数组就是存储这 char * 元素的集合。所以 paths 的类型是 char **,即:字符串数组需要使用 char ** 来标识。

    上图可以看到,主 image 拍在第一,但是被 while(i--) 之后,最终在 addHeader 方法中最后被添加。那么 libobjc.A.dylib 在哪呢?

    libobjc.A.dylib

    所以,这里就可以释疑了,即:libobjc.A.dylib 等系统库在 addHeader 之后拍在最前面,所以协议优先级最高。

    其实到这里,虽然我们知道协议在静态编译时期有代码提示、规范代码、多继承的作用,但是到这里,还是看不出来,协议在 runtime 中的作用是什么?用来方法分发,也用不上协议啊......这个基本在静态时期通过 respondsToSelector 做了容错了。

    12. 协议 - 预优化

    接着,来看 readProtocl 中第二个分支:

    else if (headerIsPreoptimized) {
        // Shared cache initialized the protocol object itself, 
        // but in order to allow out-of-cache replacement we need 
        // to add it to the protocol table now.
    
        protocol_t *cacheproto = (protocol_t *)
            getPreoptimizedProtocol(newproto->mangledName);
        protocol_t *installedproto;
        
        if (cacheproto  &&  cacheproto != newproto) {
            // Another definition in the shared cache wins (because 
            // everything in the cache was fixed up to point to it).
            installedproto = cacheproto;
        }
        else {
            // This definition wins.
            installedproto = newproto;
        }
        
        assert(installedproto->getIsa() == protocol_class);
        assert(installedproto->size >= sizeof(protocol_t));
        
        insertFn(protocol_map, installedproto->mangledName,
                 installedproto);
    }
    

    预优化的逻辑仍然是大同小异:

    1. 在预优化的协议表中查询是否存在该协议的映射;
    2. 如果存在,且不同,则以预优化过的为准,这样就不需要后续的协议解析流程了。
    3. 如果不存在,或者相同,那么就不是预优化过的协议,需要重走后续协议解析流程;

    这里也可以看到 insertFn 中传入的 Map 依然是 protocol_map 。与类的 map 一样,使用 name 作为 key,protocol/cls 作为 Value;

    这里可以做下总结:

    1. 普通类映射:name : cls;
    2. future class 重映射:cls : newCls;
    3. future class 重绑定/重映射:cls ---> newCls(替换 mach-O 中被 bebind 过的地址);
    4. 协议映射:name : protocol;

    13. 协议 - 普通协议处理

    最后两个逻辑是没有被预优化过的 protocol 的处理逻辑:

    else if (newproto->size >= sizeof(protocol_t)) {
        // New protocol from an un-preoptimized image
        // with sufficient storage. Fix it up in place.
        newproto->initIsa(protocol_class);  // fixme pinned
        insertFn(protocol_map, newproto->mangledName, newproto);
    }
    else {
        // New protocol from an un-preoptimized image 
        // with insufficient storage. Reallocate it.
        size_t size = max(sizeof(protocol_t), (size_t)newproto->size);
        protocol_t *installedproto = (protocol_t *)calloc(size, 1);
        memcpy(installedproto, newproto, newproto->size);
        installedproto->size = (typeof(installedproto->size))size;
        
        installedproto->initIsa(protocol_class);  // fixme pinned
        insertFn(protocol_map, installedproto->mangledName, installedproto);
    }
    

    对于第一个条件 newproto->size >= sizeof(protocol_t),猜测大部分下应当是不成立的,因为静态时期这个 size 是这么设置的:

    image.png

    而 runtime 时期的协议结构体会多出几个属性:

    protocol_t

    因为这段代码在 objc-818 版本已经没有了,而且可能是这段函数的代码直接被赋值到了外部函数中,也打不到断点了,暂时不纠结了。

    总之,这个分支主要做两件事:

    1. 协议在 oc 中也近似于一个类,这里对其 isa 进行了初始化,即协议的元类都是 OBJC_CLASS_$_Protocol
    2. 读取了协议的静态数据;
    3. 完成了协议的映射,协议单独存放在 protocol_map 表中,以 name 作为 key,以 protocl 地址作为 value;

    至此,readProtocol 中的代码分析完毕~~~~~~说白了,这段代码逻辑和 readClass 基本一致,目的只有一个:映射。

    14. 修复协议引用

    代码逻辑如下:

    for (EACH_HEADER) {
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[i]);
        }
    }
    

    和类引用、方法引用一样,如果存在被 remap 的协议,那么这个协议的地址也需要被重新 rebind;

    这里还是不知道协议在 runtime 中的具体作用,但是从这里可以看出来,协议和类是关联的,如果协议只在静态时期有作用,那么这里完全没必要做这么多映射、修复工作?

    猜测协议在 runtime 中的作用:

    1. 参与方法分发的流程
    image.png

    这个需要后续研究 objcMsgSend 流程时,具体看看有没有 protocol 相关的一些判断或者限制代码。

    感觉方法查找本质上仍然是去查找方法的实现,protocol 只是声明,并没有实现,所以 protocol 的作用可能仍然偏向于静态时期???

    1. 对外提供一些结构

    比如 class_conformsToProtocol 接口,比如 protocol_getMethodDescription

    15. 非懒加载类的加载(realize)

    上述所有步骤都是在映射,也就是为 objcMsgSend 的信号做准备。信号本身就是一个方法相关的字符串,而方法的实现都是在类中,所以,接下来就真正进入了方法的装配阶段:类实现。

    详见:xxx;

    16. future class 的实现

    这部分代码如下:

    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            Class cls = resolvedFutureClasses[i];
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class is not allowed to be future");
            }
            realizeClassWithoutSwift(cls);
            cls->setInstancesRequireRawIsa(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }
    

    这部分代码很好理解,上文中,如果存在 future class,最终都会被 remap,进而被解析。而解析的本质就是保留预留的 rw 数据,并读取静态 ro 数据。 resolvedFutureClasses 这个数组则是保存这些 future class 的指针,所以这里通过 resolvedFutureClasses 对指针指向的类进行实现,目的就是将 ro 数据添加到 rw 中,和普通类的实现逻辑一直。

    所以,这里在此重申:future class 就是系统提前预留了空间和 一些 rw 数据,只有这部分逻辑和普通类有区别,其他逻辑和普通类基本没有差异。

    17. 分类

    详见xxx - 6

    相关文章

      网友评论

          本文标题:iOS类加载流程(五):read_images流程分析

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