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

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

推荐阅读更多精彩内容