OC底层原理13之分类加载

559 阅读12分钟

本章内容

  1. 分类的本质
  2. rwe开辟赋值
  3. 分类加载流程
  4. 五种情况分析类与分类实现load方法

本章目的

知道分类的底层结构,以及知道分类加载流程。

分类的本质

分类在OC底层的原型是什么?其实我们能很容易理解的,想想看如果说分类在底层没有实现的话又是靠什么进行存储呢?所以研究底层也是很有必要的,分类在底层实现就是结构体。我们可以通过还原CXX文件进行查看,甚至也可以看objc源码进行探究。但是为了更加直观的查看我推荐第一种方式,clang还原方式,当然我们也会展示objc源码

先看源文件,然后看下面两种方式对比 image.png

clang还原

  1. 查看分类本质结构体 category_t

image.png

  1. 查看CXX 文件分类的实现。看到下面或许会有疑惑为什么是name被clang为Person。其实你大可不必有次疑惑,因为我们是clang还原,而clang是编译时LLVM的东西它在此时只是给个匿名并不是真实名字。

image.png

分类objc源码

可以看到与clang还原的基本一致,但是我所标出的类属性肯定会有人疑惑,什么是类属性,OC还有类属性?其实是有的,但是对于类属性的意义是什么?你可以理解为 调用为Person.name,但是类属性就与其他语言一样虽说调用权限是该类的,但是实际内存是全局的(实例属性的实现方式:ivar+setter+getter,而类属性其实是调用的setter+getter方法为全局变量赋值)。如果说想了解可以看我的补充。

image.png

补充类属性

image.png

rwe的开辟

其实这块并没有什么看的,但是为了解决上一章遗留的问题所以单独拎出来。先看下rwe的结构是怎样的。很简单只有一些成员变量,我们既然要找rwe的开辟,首先肯定需要知道它的初始化方法 image.png

rwe初始化

我们知道rw包含了rwe,而我们可以在其rw中找找看看,果然有一个方法class_rw_ext_t *extAllocIfNeeded()是去初始化rwe的。

image.png

rwe初始化的条件

既然我们已经找到其初始化方法,就可以全局搜索objc在那些函数中调用,就可以明白了。然后我们发现有7个函数调用其初始化,但是我们所关心的是什么?也就是说有分类,添加方法,协议,属性等时候rwe就会自行开辟,这也与WWDC2020(推荐看)所符合

  1. attachCategories分类相关
  2. demangledName看其源码不重要
  3. class_setVersion类设置版本号api,不重要,里面只是对rwe进行version赋值
  4. addMethods_finish添加方法
  5. class_addProtocol添加协议
  6. _class_addProperty添加属性
  7. objc_duplicateClass这个api是苹果自行调用的,用户不能自行调用,KVO相关。或许探究KVO底层的时候你会有所发现?这里并不多说,提一句KVO系统自行创建一个子类

分类加载流程

从上面我们已经得知了一个重要因素,rwe的开辟条件之一是由于分类的缘故,而我在类的加载中说过,类的加载与分类加载也有一定的渊源。不知道你看过这篇文章是否注意到attachToClass这个函数。但是我们先不这么去探索,我们从attachCategories逆向去探索,就是谁调用了它。发现其中之一有attachToClass,还有一个load_categories_nolock函数不知道你是否注意到过在load_images流程中。也就是说我们已经确定了两个

  1. load_categories_nolock就是load_images流程中的loadAllCategories函数调用。
  2. attachToClass就是在map_images流程中的methodizeClass函数调用。

提示:如果说你对我上面的分析感觉迷惑,那么先记得load_images和map_images流程都有对分类的处理。也就是说我们加载类(map_images非懒加载类)的时候可能会有某种情况对于分类有所处理。而调用load方法的时候也会有此处理。(该描述不准确,希望你继续往下看)

attachToClass 分析

在map_images流程中该函数是必进的。但是我后来运行时测试几次发现从这个函数根本没有进入过attachCategories函数,但真的是这样吗?其实我又进行测试发现它在某一种情况会进入。总体来说这个函数不重要,跟函数名一样,附加到类中,中间方法而已

void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);
        
        /** 是为了做测试用的,证明来的是我们创建的类
        bool isMeta = cls->isMetaClass();
        const char * clsName = cls -> nonlazyMangledName();
        const char *person = "person";
        if (strcmp(clsName, person) == 0)
        {
            if (!isMeta)
            {
                printf("-----%s-----%s\n", __func__, clsName);
            }
        }
        */
        // 其实测试的时候会发现,没有从这里面直接进入过attachCategories
        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);
        }
    }

load_categories_nolock 分析

且先不说这个方法的作用,发现调用这个方法有两个入口。我们只用研究loadAllCategories(这个函数就是调用load_categories_nolock方法,就不展示源码了)

  1. loadAllCategories(load_images)流程。但是对于执行这个函数有一个重要条件didInitialAttachCategories 启动时出现的类别的初始附件是否已经完成。对于启动时候map_images流程是不执行的。

  2. _read_images(map_images)流程。而对于load_images它的执行条件是!didInitialAttachCategories && didCallDyldNotifyRegister,didCallDyldNotifyRegister就是是否调用dyld的一个函数。对于启动时候是执行的

static void load_categories_nolock(header_info *hi) {

    // hi是mach-o的头,表头
    // 是否有类属性
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    
    // 这个count的数量是下面hi->catlist(&count)
    size_t count;
    // 相当于一个闭包,在结尾才是调用
    auto processCatlist = [&](category_t * const *catlist) {
        // 例如我们类Person,分类有A,B,C三个,且都有方法实现。则数量就是3。
        // 当然只是举个例子,实际情况需要根据load方法等,不一样
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            // 获取class,也就是Person
            Class cls = remapClass(cat->cls);
            // 结构体locstamped_category_t 赋值
            locstamped_category_t lc{cat, hi};

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }
            // 这个条件,不会触发,具体我也不太清楚是什么,根类?
            // Process this category.
            if (cls->isStubClass()) {
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                // 备注:首先,将类别注册到它的目标类中。然后,如果实现了类,重新构建类的方法列表(等等)。
                // 如果分类有下面的实现
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                // 将那些东西放到类里面
                    if (cls->isRealized()) {
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }
                // 可以看出来现在 分类还是可以实现类属性的。但是我们不用
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                // 将那些东西放到元类里面
                    if (cls->ISA()->isRealized()) {
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };
    // 就是从mach-o中获取分类,但是为什么分两个?不必关心,没意义,是编译时的东西
    // _getObjc2CategoryList
    processCatlist(hi->catlist(&count));
    // _getObjc2CategoryList2
    processCatlist(hi->catlist2(&count));
}

attachCategories 分析

这个函数就是分类加载的处理了,对于分类的处理就在这里面,我们从上面已经得知了这个结果。可以看出,确实是在这里将分类的东西处理给类的,但是改变类的结构指的是rwe。可不是ro,希望你能知道。

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.
     */
    //其实是限制分类的各种数量 64个。
    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);
    // 开辟rwe,验证我们的猜想
    auto rwe = cls->data()->extAllocIfNeeded();
    // 从load_categories_nolock 过来的cats_count是1
    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) {
                // 方法排个序,注意看什么条件下进来ATTACH_BUFSIZ
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                // 对rwe的方法列表(这个列表其实是数组指针,该数组是方法列表)进行处理
                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) {
        // 对分类的方法进行排序,然后添加到rwe里面
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    // 对分类的属性添加到rwe里面
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    // 对分类的协议添加到rwe里面
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

总结:

我知道,上面的源码分析不足以让我们清楚分类加载的具体流程,但是我们通过源码会知道分类加载的时候系统都做了哪些事。而对于类和分类的加载具体流程需要你继续往下看。

类与分类实现load方法,几种情况分析

提示:为什么说实现load方法启动耗时,就是因为在load_images流程中,你类实现load方法已经导致类成为非懒加载类了,而分类如果同时实现load方法,就打破了mach-o已经分配好的方法列表需要走load_images流程重新加载

要探索分类,与类具体加载流程,其实两者是相互影响的。举例子说明:

类实现load,分类也实现load。

会打破原有机制,分类自己去加载

1. 分类只有一个的情况实现load方法。(其实可以这样说,至少一个分类实现了load方法)

可以看到先走map_images流程去加载类,再走load_images流程加载分类。在load_categories_nolock函数中就直接读取了类的全部分类

image.png image.png

2. 分类有两个及以上实现load方法。

可以看到先走map_images流程去加载类,再走load_images流程加载分类。在load_categories_nolock函数中就直接读取了类的全部分类。与1一致

image.png image.png

类实现load,分类没有实现load

并没有走 分类额外去加载流程。而且经测试,我第一次测试rw值,发现分类的方法已经包含在内。我知道以为有分类的可能会伴随rwe的开辟,然后我去看rwe,发现为nil。然后又去看ro的值,发现了ro里面的baseMethods竟然包含了分类方法。那么是不是可以推断出,在编译阶段,系统就已经完成了分类的处理。是不是人家说分类运行时加载有问题了?

image.png

类没有实现,分类实现load

该情况比较复杂一些,类有可能被当做非懒加载类,有可能没有

1. 只有一个分类实现,其他分类没实现

并没有走 分类额外去加载流程。而且经测试,发现了ro里面的baseMethods包含了分类方法。与类实现load,分类没实现一致

image.png

2. 两个及以上分类实现

该情况很复杂,对于类的实现并没有走map_images流程而是走的,load_images流程进行了被动加载。也就是说在之前系统并没有将该类加到非懒加载类表中。而因为分类的load方法导致了类的加载。对于这种情况的ro并没有包含分类的方法,可以这样说分类在运行时加载

提示1:类没有实现所以需要将分类注册到目标类中,看load_categories_nolock源码

image.png

提示2:去准备load方法,但是发现类没有实现去实现,看源码请看prepare_load_methods。而且还记得上面我所提的attachToClass方法在某一种情况去执行了附加分类的方法就是这一种情况。

image.png

类与分类均没有实现

我们都知道这种情况类的实现是在第一次消息发送的时候,所以我去查看后发现此时ro已经包含了分类的方法。也就是分类在编译时就已经将方法注册到主类中了

image.png

总结

对于分类的加载我们可以做如下总结

对于运行时加载的分类情况:1.类与分类都实现load方法;2.分类有两个以上实现load方法

对于编译时加载的分类情况:1.类实现load方法,分类没有;2.类没有实现,分类仅有一个实现;3.类与分类均没实现

所以希望不要动不动就实现load方法,很耗时,除了一些必要的操作。我们可以去执行initialize方法。

补充

对于类的ro,肯定会有疑惑。这个值是怎么来的,clean Memory,为什么有那么几种情况分类在应用启动前就会被加载到类中。其实我们能够想象到LLVM就可以解答我们的疑惑。在LLVM层也就是编译时你就会发现有个跟objc一样的class_ro_t结构体,只是成员不一样。然后去看结构体的一个方法Read,就是对ro的集中处理。这也就是我在类加载篇章说类的加载其实是在编译时,而类的实现是在运行时。