iOS底层系列16 -- 类的加载机制

  • 前面几篇文章了解了App的编译与运行时,其底层所做一系列操作,本篇主要是来探讨类的信息是如何加载进入内存的,其中重点关注objc中的map_imagesload_images两个函数方法;
    • map_images:主要是管理App编译生成的Mach-O文件中的主程序和动态库文件的所有符号,例如class、protocol、selector、category等,将其映射入内存;
    • load_images:加载主程序和动态库所有文件的load类方法

map_images 加载镜像文件进入内存

  • 首先Dyld读取Mach-O文件,会将主程序文件和动态库文件都创建成独立的镜像文件,然后再将这些镜像文件存放在一个集合中,最后遍历这个集合,将镜像文件一一加载进入内存中;
  • map_images源码如下所示:
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
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);
}
  • 内部调用map_images_nolock函数,其简化后的实现如下:
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;

    ...
    
    //记录image镜像的数量
    hCount = 0;

    //记录所有类的数量(主程序+动态库)
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    //加载镜像文件进入内存
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    ...
}
  • 内部核心函数调用_read_images(),其用来加载镜像,本质是加载类信息,即类、分类、协议等等,主要分为以下几个步骤:
    • 条件控制保证只进行一次加载;
    • 修复预编译阶段的@selector的混乱问题;
    • 错误混乱的类处理;
    • 修复重映射一些没有被镜像文件加载进来的类;
    • 修复一些消息;
    • 当类里面有协议时:readProtocol 读取协议;
    • 修复重映射没有被加载的协议;
    • 分类处理;
    • 类的加载处理;
    • 没有被处理的类,优化那些被侵犯的类;
  • 下面分别来详细阐述每一步的执行逻辑:
  • 【第一步:条件控制保证只进行一次加载
  • 通过变量doneOnce控制只进行一次加载,简化后的源码实现如下:
if (!doneOnce) {
        doneOnce = YES;
        launchTime = YES;

        // namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
    }
  • 调用NXCreateMapTable函数,创建一张存储类Class的哈希表gdb_objc_realized_classes,此哈希表用于存储不在共享缓存且已命名类,无论类是否实现,其容量是类总数量的4/3;
  • 【第二步:修复预编译阶段的@selector的混乱问题】源码如下:
Snip20210305_18.png
  • 通过_getObjc2SelectorRefs函数,获取Mach-O文件中的静态段__objc_selrefs即objc方法的selector列表,然后遍历selector集合sels,调用sel_registerNameNoLock函数将selector注册进入namedSelectors中并返回selector实例,然后将selector与sels[i]比较,当两者不一致时,需要调整为一致;
  • 其中_getObjc2SelectorRefs函数源码如下:
    Snip20210305_20.png
  • 可以看到传入不同的section name,可以从Mach-O文件中获取不同部分的内容;
  • sel_registerNameNoLock函数源码如下:
Snip20210305_21.png
  • 【第三步:错误混乱的类处理
  • 主要是从Mach-O文件中通过_getObjc2ClassList取出所有的类,再遍历进行处理,源码如下:
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        classref_t const *classlist = _getObjc2ClassList(hi, &count);
        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

        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.
                //经过调试,并未执行if里面的流程
                //初始化所有懒加载的类需要的内存空间,但是懒加载类的数据现在是没有加载到的,连类都没有初始化
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }
    ts.log("IMAGE TIMES: discover classes");
  • _getObjc2ClassList函数,从Mach-O文件中获取所有类的集合;
  • Class cls = (Class)classlist[I],此时的class还只是一个内存地址;
  • Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized)这段代码实现了地址与类Class的绑定,证明如下:
Snip20210305_22.png
  • 到这里类信息读取到内存的仅仅只有地址 + 名称,类的data数据还并没有加载进来;
  • 【第四步:修复重映射一些没有被镜像文件加载进来的类
// Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.
    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]);
            }
        }
    }
    ts.log("IMAGE TIMES: remap classes");
  • 主要是将未映射的class与superClass进行重新映射;
  • _getObjc2ClassRefs获取Mach-O中的静态段__objc_classrefs,即类的引用;
  • _getObjc2SuperRefs获取Mach-O中的静态段__objc_superrefs,即父类的引用;
  • 由上面的注释Class list and nonlazy class list remain unremapped即类与非懒加载的类不会被重映射,言外之意懒加载的类会被重映射
  • remapClassRef都是懒加载类,需要重新映射的类;
  • 【第五步:修复一些消息
  • 通过_getObjc2MessageRefs函数从Mach-O文件中获取消息的引用集合,然后遍历将函数指针进行注册,并fix为新的函数指针;
//Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;
        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        //经过调试,并未执行for里面的流程
        //遍历将函数指针进行注册,并fix为新的函数指针
        for (i = 0; i < count; i++) {
            fixupMessageRef(refs+i);
        }
    }
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
  • 【第六步:当类里面有协议时:readProtocol 读取协议
bool cacheSupportsProtocolRoots = sharedCacheSupportsProtocolRoots();
    // 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->hasPreoptimizedProtocols();

        // Skip reading protocols if this is an image from the shared cache
        // and we support roots
        // Note, after launch we do need to walk the protocol as the protocol
        // in the shared cache is marked with isCanonical() and that may not
        // be true if some non-shared cache binary was chosen as the canonical
        // definition
        if (launchTime && isPreoptimized && cacheSupportsProtocolRoots) {
            if (PrintProtocols) {
                _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
                             hi->fname());
            }
            continue;
        }
        bool isBundle = hi->isBundle();
        protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }
    ts.log("IMAGE TIMES: discover protocols");
  • Class cls = (Class)&OBJC_CLASS_$_Protocol,cls=Protocol即Protocol类;
  • protocols()函数创建了一个存放protocol的哈希表;
  • _getObjc2ProtocolList()从Mach-O文件中获取protocol列表;
  • readProtocol()添加protocol到protocol_map哈希表中;
  • 【第七步:修复重映射没有被加载的协议
    // Fix up @protocol references
    // Preoptimized images may have the right 
    // answer already but we don't know for sure.
    for (EACH_HEADER) {
        // At launch time, we know preoptimized image refs are pointing at the
        // shared cache definition of a protocol.  We can skip the check on
        // launch, but have to visit @protocol refs for shared cache images
        // loaded later.
        if (launchTime && cacheSupportsProtocolRoots && hi->isPreoptimized())
            continue;
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        //比较当前协议和协议列表中的同一个内存地址的协议是否相同,如果不同则替换
        //经过代码调试,并未执行
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[I]);
        }
    }
    ts.log("IMAGE TIMES: fix up @protocol references");
  • _getObjc2ProtocolRefs()获取Mach-O文件中protocol引用类表,然后遍历比较当前协议和协议列表中的同一个内存地址的协议是否相同,如果不同则替换;
  • remapProtocolRef()做比较替换的;
/***********************************************************************
* remapProtocolRef
* Fix up a protocol ref, in case the protocol referenced has been reallocated.
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static size_t UnfixedProtocolReferences;
static void remapProtocolRef(protocol_t **protoref)
{
    runtimeLock.assertLocked();
    //获取协议列表中统一内存地址的协议
    protocol_t *newproto = remapProtocol((protocol_ref_t)*protoref);
    if (*protoref != newproto) {//如果当前协议 与 同一内存地址协议不同,则替换
        *protoref = newproto;
        UnfixedProtocolReferences++;
    }
}
  • 【第八步:分类处理
    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
    ts.log("IMAGE TIMES: discover categories");
  • 主要是处理分类,需要在分类初始化并将数据加载到类后才执行,对于运行时出现的分类,将分类的发现推迟到_dyld_objc_notify_register()调用完成后的第一个load_images调用为止;

  • 【第九步:非懒加载类的加载处理

  • 实现类的加载处理,主要是实现非懒加载类

// Realize non-lazy classes (for +load methods and static instances)
    //实现非懒加载的类,对于load方法和静态实例变量
    for (EACH_HEADER) {
        //获取Mach-O的静态段__objc_nlclslist非懒加载类表
        classref_t const *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            //
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;
            //测试代码
            const char *mangledName  = cls->mangledName();
            const char *YYPersonName = "YYPerson";
            
            if (strcmp(mangledName, YYPersonName) == 0) {
                auto kc_ro = (const class_ro_t *)cls->data();
                printf("_getObjc2NonlazyClassList: 这个是我要研究的 %s \n",YYPersonName);
            }
            //插入表,但是前面已经插入过了,所以不会重新插入
            addClassTableEntry(cls);

            if (cls->isSwiftStable()) {
                if (cls->swiftMetadataInitializer()) {
                    _objc_fatal("Swift class %s with a metadata initializer "
                                "is not allowed to be non-lazy",
                                cls->nameForLogging());
                }
            }
            //实现当前的类,因为前面readClass读取到内存的仅仅只有地址+名称,类的data数据并没有加载出来
            //实现所有非懒加载的类(实例化类对象的一些信息,例如rw)
            realizeClassWithoutSwift(cls, nil);
        }
    }
    ts.log("IMAGE TIMES: realize non-lazy classes");
  • 通过_getObjc2NonlazyClassList获取Mach-O的静态段__objc_nlclslist非懒加载类表;
  • 通过addClassTableEntry将非懒加载类插入类表,存储到内存,如果已经添加就不会再添加,需要确保整个结构都被添加;
  • 通过realizeClassWithoutSwift实现当前的类,因为前面第三步中的readClass读取到内存的仅仅只有地址+名称,类的data数据并没有加载出来;
  • 加入测试代码如下所示:
Snip20210308_34.png
  • 发现这里没有打印自定义的YYCat类,说明YYCat是懒加载的类,如果我们手动实现了YYCat的load类方法,发现这里才会有打印;说明是否实现类的load方法是判断此类是否是懒加载类的条件
  • 【第十步:实现没有被处理的类,优化那些被侵犯的类
// 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, nil);
            cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }
    ts.log("IMAGE TIMES: realize future classes");
  • 上面所阐述的十个步骤需重点关注第三步中的readClass函数与第十步中的realizeClassWithoutSwift函数;

readClass函数--加载类进入内存

  • readClass主要是读取类,在未调用该方法前,cls只是一个地址,执行该方法后,cls是类的名称,其关键代码是addNamedClassaddClassTableEntry,源码实现如下所示:
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.
        if (PrintConnecting) {
            _objc_inform("CLASS: IGNORING class '%s' with "
                         "missing weak-linked superclass", 
                         cls->nameForLogging());
        }
        addRemappedClass(cls, nil);
        cls->superclass = nil;
        return nil;
    }
    
    cls->fixupBackwardDeployingStableSwift();

    Class replacing = nil;
    //判断是不是后期要处理的类
    //正常情况下,不会走到popFutureNamedClass,因为这是专门针对未来待处理的类的操作
    //通过断点调试,不会走到if流程里面,因此也不会对ro、rw进行操作
    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.
        if (newCls->isAnySwift()) {
            _objc_fatal("Can't complete future class request for '%s' "
                        "because the real class is too big.", 
                        cls->nameForLogging());
        }
        //读取class的data,设置ro、rw
        //经过调试,并不会走到这里
        class_rw_t *rw = newCls->data();
        const class_ro_t *old_ro = rw->ro();
        memcpy(newCls, cls, sizeof(objc_class));
        rw->set_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;
    }
    
    //判断类是否已经加载到内存
    if (headerIsPreoptimized  &&  !replacing) {
        // class list built in shared cache
        // fixme strict assert doesn't work because of duplicates
        // ASSERT(cls == getClass(name));
        ASSERT(getClassExceptSomeSwift(mangledName));
    } else {
        //加载共享缓存中的类
        addNamedClass(cls, mangledName, replacing);
        //插入表,即相当于从mach-O文件 读取到 内存 中
        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;
}
  • 向其中加入测试代码:
Snip20210308_30.png
  • 看到控制台的打印,说明底层确实在遍历所有的类文件数据,依次加载进入内存,这里只是加载类的地址与名称信息;
  • 调用mangledName()获取类的名字,源码如下所示:
    Snip20210308_27.png
  • 当前类的父类中若有丢失的weak-linked类,则返回nil;
  • 判断是不是后期需要处理的类,在正常情况下,不会走到popFutureNamedClass,因为这是专门针对未来待处理的类的操作,也可以通过断点调试,可知不会走到if流程里面,因此也不会对ro、rw进行操作;
    • data是mach-O的数据,并不在class的内存中;
    • ro的赋值是从mach-O中的data强转赋值的;
    • rw里的ro是从ro复制过去的;
  • addNamedClass()将当前类添加到已经创建好的gdb_objc_realized_classes哈希表,该表用于存放所有类,此类必须满足是经过命名的且非元类;
static void addNamedClass(Class cls, const char *name, Class replacing = nil){
    runtimeLock.assertLocked();
    Class old;
    if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);
        // getMaybeUnrealizedNonMetaClass uses name lookups.
        // Classes not found by name lookup must be in the
        // secondary meta->nonmeta table.
        addNonMetaClass(cls);
    } else {
        //将当前类插入gdb_objc_realized_classes哈希表中
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
    ASSERT(!(cls->data()->flags & RO_META));
}
  • addClassTableEntry()将初始化的类添加到allocatedClasses表,此表在iOS底层系列15 -- dyld与objc之间的互动关联文章中提及过,是在_objc_init中的runtime_init创建的;
static void addClassTableEntry(Class cls, bool addMeta = true){
    runtimeLock.assertLocked();
    //开辟的类的表,在objc_init中的runtime_init就已经创建了此表
    auto &set = objc::allocatedClasses.get();

    ASSERT(set.find(cls) == set.end());

    if (!isKnownClass(cls))
        //将当前类添加到allocatedClasses哈希表
        set.insert(cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}
  • 由此可见readClass,实现了将Mach-O中的类读取到内存,然后插入对应的哈希表中,便于以后的查找,但是插入哈希表中的类仅有两个信息:地址 + 名称,而Mach-O文件中的关于类信息的data数据段还未读取出来;

realizeClassWithoutSwift -- 加载类的data信息(即实现类)

  • realizeClassWithoutSwift主要实现了将类data数据加载到内存中,具体逻辑分为以下几个步骤:
  • 【第一步:读取data数据,并设置ro、rw】
Snip20210305_23.png
  • 可以看到首先读取class的data信息,然后将其强转成class_ro_t类型,用一个临时变量ro保存;
  • 创建初始化rw(class_rw_t) ,为其分配内存,然后将临时变量ro赋值给rw;
  • 将创建初始化的rw赋值给class的data成员;
  • ro表示 readOnly,即只读,其在编译时就已经确定了内存,包含类名称、方法、协议和实例变量的信息,由于是只读的,所以属于Clean Memory,而Clean Memory是指加载后不会发生更改的内存
  • rw表示 readWrite,即可读可写,由于其动态性,可以往类中添加属性、方法、协议,提到rw,其实在rw中只有10%的类真正的更改了它们的方法,最终的数据都是存储到一个名为rwe的结构体中,即类的额外信息,其中rw就属于dirty memory,而 dirty memory是指在进程运行时会发生更改的内存,类结构一经使用就会变成 ditry memory,因为运行时会向它写入新数据,例如 创建一个新的方法缓存,并从类中指向它;
  • 加入测试代码进行LLDB调试:
Snip20210308_2.png
  • 可以看到从Mach-O文件中获取的class_ro_t的详细信息;
  • 然后执行下面的逻辑:
Snip20210308_3.png
  • 【第二步:递归调用realizeClassWithoutSwift,完善继承链
Snip20210305_25.png
  • 【第三步:methodizeClass加载主类的data与分类的data】
    通过methodizeClass方法,从ro中读取方法列表(包括分类中的方法)、属性列表、协议列表赋值给rwe,并返回cls;
  • 核心源码如下所示:
Snip20210310_56.png
  • 1.将主类的方法列表、属性列表、协议列表等赋值到rwe中;
  • 2.将分类中的方法赋值到rwe中;
  • 下面着重探索方法列表是如何赋值到rwe中;
Snip20210308_4.png
  • 从ro调用baseMethods函数,获取方法列表,注意如果class是元类则获取的类方法列表,如果是类则获取的是实例方法列表;
  • 调用prepareMethodLists()函数,此方法完成方法的排序;
  • 最后将完成排序的方法列表赋值给rwe;
  • YYCat类的头文件如下所示:
Snip20210310_58.png
  • 在methodizeClass函数中加入测试代码如下:
Snip20210308_7.png
  • LLDB调试结果如下:
Snip20210308_8.png
Snip20210308_9.png
  • 上面是未排序之前的方法列表;
  • 方法列表的排序逻辑如下:
Snip20210308_10.png
Snip20210308_11.png
  • 排序之后的方法列表:
Snip20210308_12.png
  • 接着探讨分类是如何赋值到rwe中,逻辑如下:
objc::unattachedCategories.attachToClass(cls, cls,isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
  • 将尚未添加的分类,添加到Class中;

  • attachToClass源码实现如下:

void attachToClass(Class cls, Class previously, int flags){
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));
        
        //测试代码
        const char *mangledName  = cls->mangledName();
        const char *YYCatName = "YYCat";
        if (strcmp(mangledName, YYCatName) == 0) {
            bool kc_isMeta = cls->isMetaClass();
            auto kc_rw = cls->data();
            auto kc_ro = kc_rw->ro();
            if (!kc_isMeta) {
                printf("%s: 定位到 %s \n",__func__,YYCatName);
            }
        }
        
        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }
  • if (it != map.end())要进入if逻辑,需要手动实现分类的load方法(不知道什么原因)
  • 重点关注attachCategories函数,它才是真正实现将分类data添加到主类class的rwe中其实现如下:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();
    
    //测试代码
    const char *mangledName  = cls->mangledName();
    const char *YYCatName = "YYCat";
    if (strcmp(mangledName, YYCatName) == 0) {
        bool kc_isMeta = cls->isMetaClass();
        auto kc_rw = cls->data();
        auto kc_ro = kc_rw->ro();
        if (!kc_isMeta) {
            printf("%s: 定位到 %s \n",__func__,YYCatName);
        }
    }

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[I];
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
  • 首先auto rwe = cls->data()->extAllocIfNeeded() 用来开辟rew内存空间的,也就是初始化class的rwe,拿到rwe之后再往rwe中写入方法,属性与协议;
  • 其次method_list_t *mlist = entry.cat->methodsForMeta(isMeta),获取到分类的方法列表;
  • 最后调用attachLists实现分类方法的加载,如下所示:
Snip20210310_53.png

attachLists的函数实现

  • 源码如下所示:
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        if (hasArray()) {//many lists -> many lists
            //获取原来二维数组的长度,即一维数组的个数
            uint32_t oldCount = array()->count;
            //注意传进来的addedLists也是一个二维数组,addedCount是二维数组的长度
            //新二维数组的长度 = 旧二维数组的长度+新二维数组的长度
            uint32_t newCount = oldCount + addedCount;
            //根据新的长度,创建一个新的二维数组数组,类型是 array_t,通过array()获取
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            //设置新二维数组的长度
            array()->count = newCount;
            //所有旧的一维数组 从 addedCount 下标开始存放
            memmove(array()->lists+addedCount,array()->lists,oldCount * sizeof(array()->lists[0]));
            //所有新的一维数组从二维数组的首位置开始存放
            memcpy(array()->lists,addedLists,addedCount * sizeof(array()->lists[0]));
        }else if (!list  &&  addedCount == 1) {//0 lists -> 1 list
            //addedLists直接赋值给二维数组,注意list就是rwe存储方法的二维数组,其名称可看成二维数组的首地址
            list = addedLists[0];
        }else {//1 list -> many lists
            //获取原来的二维数组中的一维数组list即主类的方法列表
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            //新二维数组的长度 = 旧二维数组的长度+新二维数组的长度
            uint32_t newCount = oldCount + addedCount;
            //根据新的长度,创建一个新的二维数组
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            //设置新的二维数组的长度
            array()->count = newCount;
            //旧的一维数组 从 addedCount 下标开始存放
            if (oldList) array()->lists[addedCount] = oldList;
            //所有新的一维数组从二维数组的首位置开始存放
            //其中array()->lists 表示首位元素位置
            memcpy(array()->lists, addedLists,addedCount * sizeof(array()->lists[0]));
        }
    }
  • 从源码可以看出,将分类方法赋值给主类的class_rw_ext结构体,总体逻辑分为三种情况:
【情况1:0对1】
  • 当主类的class_rw_ext结构体存储方法的二维数组method_array_t为空时,那么此时肯定是在准备加载主类方法集合;
  • 直接将主类方法的一维数组,添加到二维数组method_array_t中;
【情况2:1对多】
  • 当主类的class_rw_ext结构体存储方法的二维数组method_array_t只有一个一维数组时,此一维数组里面存放的肯定是是主类的方法集合,可以看成第一次加载分类方法集合的流程,逻辑详情见注释;
  • 将获取旧的list长度和新的list(新的分类方法集合)长度相加,然后根据相加后的二维数组长度,创建一个新的二维数组,来存放旧的list与新的list;
  • 旧的一维数组list放在二维数组的末尾,新的一维数组list放在二维数组的首位,这就为当主类与分类方法名相同时优先调用分类category中的方法提供了理论依据;
  • 下面通过实例来验证上面的三个步骤:
  • extAlloc函数中加入测试代码,并打下断点如下所示:
【情况3:多对多】
  • 当将分类数据,加入主类class_rw_ext结构体的方法的二维数组method_array_t中时,二维数组中已经存在至少两个一维数组;
  • method_array_t中已存在多个一维数组,说明出来主类方法已经加载进来之外,还有其他分类也加载进来了,现在是在加载另一个分类;
  • 将获取旧的list长度和新的list(新的分类方法集合)长度相加,然后根据相加后的二维数组长度,创建一个新的二维数组,来存放旧的list与新的list;
  • 将所有旧的一维数组list 从 addedCount下标开始依次存放,即整段平移,可以简单理解为旧的一维数组全部移动到二维数组的后面,二维数组前面空出的位置,存储新加载进来的一维数组;
  • 新加载进来的一维数组从二维数组首位置开始存储,可以简单理解为越晚加进来,越在前面,后来居上,则优先调用
Snip20210310_60.png
  • 当代码停在1270行,LLDB调试如下:
Snip20210310_61.png
  • 说明此时class的rwe中的list_array_tt是空的;
  • 过掉当前断点,停在1274行时,LLDB调试如下:
Snip20210310_62.png
  • 获取到当前主类的方法集合list;
  • 过掉当前断点,会进入attachLists函数,执行0对1的逻辑
Snip20210310_63.png
  • list赋值成功后,LLDB调试如下:
Snip20210310_64.png
  • 可以看到主类的方法信息已经加载进去了;
  • 0对1的函数调用链为:map_images -> _read_images -> readClass -> realizeClassWithoutSwift -> methodizeClass -> prepareMethodLists -> fixupMethodList -> attachToClass -> load_categories_nolock -> attachCategories -> extAllocIfNeeded -> extAlloc -> attachLists
  • 上面主类的方法集合已经加载完成了,现在进入分类方法加载的流程了;
  • 第一次进入attachCategories,LLDB调试如下:
Snip20210310_65.png
  • 然后进入attachLists函数,执行1对多逻辑
Snip20210310_66.png
  • 第二次进入attachCategories,LLDB调试如下:
Snip20210310_68.png
  • 然后进入attachLists函数,执行多对多逻辑
Snip20210310_69.png
总结
  • 综上所述,attachLists方法主要是将 类 和 分类 的数据赋值到class_rw_ext结构体中,逻辑如下:
    • 首先加载本类的data数据,此时的class_rw_ext是空的,没有数据,走0对1流程;
    • 当加入一个分类时,此时的class_rw_ext仅有一个list,即主类的list,走1对多流程;
    • 当再加入一个分类时,此时的class_rw_ext中有两个list,即主类+分类的list,走多对多流程;
cls_方法合并.png

你可能感兴趣的:(iOS底层系列16 -- 类的加载机制)