前面已经探究了类的加载流程,类分为懒加载类和非懒加载类,他们有不同加载流程,下面来探究下分类的加载,以及分类和类搭配使用的情况
分类的本质
准备工作
在 main.m中定义 HTPerson的分类HT, 代码如下

探索分类本质的三种方法
探索分类的本质,有以下三种方式
- 【方式一】通过clang
- 【方式二】通过Xcode文档搜索Category
- 【方式三】通过objc源码搜索category_t
【方式一】:通过clang
通过clang -rewrite-objc main.m -o main.cpp命令,查看编译后的 c++文件
- 其中分类的类型是_category_t,存储了相应的实例方法、类方法、属性、协议等信息

搜索struct _category_t,如下所示
- 其中有两个_method_list_t,分别对应对象方法和类方法

- 全局搜索_CATEGORY_INSTANCE_METHODS_HTPerson_,找到其底层实现

- 查看协议和属性的结构

这里我们发现一个【问题】:分类中定义的属性没有相应的set、get方法,我们可以通过关联对象来设置(关于如何设置关联对象,我们将在下一篇中进行分析)
【方式二】:通过Xcode文档搜索 Category
通过快捷键command+shift+0,搜索Category

【方式三】:通过objc源码搜索 category_t
通过objc818源码搜索category_t类型

分类的加载的源码分析
分类的底层结构是结构体category_t,下面我们就来探究 分类是何时加载进来的,以及加载的过程
分类加载的引入
WWDC2020中关于数据结构的变化(Class data structures changes)视频地址,苹果为分类和动态添加专门分配的了一块内存rwe,因为rwe属于dirty memory,所以肯定是需要动态开辟内存。下面从class_rw_t中去查找相关rwe的源码
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    Class firstSubclass;
    Class nextSiblingClass;
    
    // ...省略代码
    class_rw_ext_t *ext() const {
        return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
    }
    class_rw_ext_t *extAllocIfNeeded() {
        auto v = get_ro_or_rwe();
        // 判断rwe是否存在
        if (fastpath(v.is<class_rw_ext_t *>())) {
            // 如果已经有rwe数据,直接返回地址指针
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
        } else {
            // 为rwe开辟内存并且返回地址指针
            return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
        }
    }
    class_rw_ext_t *deepCopy(const class_ro_t *ro) {
        return extAlloc(ro, true);
    }
    // ...省略代码
}
从代码可以看出,extAllocIfNeeded方法用来开辟rwe内存,全局搜索extAllocIfNeeded,在下列几个地方有相关调用:
- 
attachCategories方法:添加分类信息
- demangledName
- 
class_setVersion:设置类的版本
- 
addMethods_finish:动态添加方法
- 
class_addProtocol:动态添加协议
- 
_class_addProperty:动态添加属性
- objc_duplicateClass
本文主要来探究分类的加载,?我们来分析attachCategories方法做了什么
attachCategories 反推法
attachCategories方法,源码如下:
// 将分类的 方法列表、属性、协议等数据加载到 类中
// 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);
    // 获取rwe
    auto rwe = cls->data()->extAllocIfNeeded();
    // 遍历所有的分类
    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) {
                // 当mlists的个数为 64时,对方法进行排序,然后将 mlists加载到rwe中
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            // 如果 mcount = 0,mlist存放的位置在63个位置,总共是0 ~ 63, mlists最多存放64个方法列表(mlist)
            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, __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->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
attachCategories准备分类的数据,然后调用attachLists将数据添加到rwe中,那么到底哪些地方调用attachCategories方法,
- 全局搜索attachCategories,发现有两处进行了调用,分别是attachToClass方法和load_categories_nolock方法


attachToClass流程流程分析
全局搜索attachToClass,发现只有methodizeClass方法中进行了调用

- 
methodizeClass方法,我们应该不陌生,在上一篇类的加载中有分析,从源码我们发现previously的值为nil
- 
previously作为备用参数,这种设计可能是苹果内部调试用的
- 
attachToClass调用流程:_read_images-->realizeClassWithoutSwift-->methodizeClass-->attachToClass-->attachCategories-->attachLists
load_categories_nolock流程分析
- 全局搜索load_categories_nolock,在loadAllCategories方法中调用

- 接着全局搜索loadAllCategories,在load_images方法中调用

- 
didInitialAttachCategories默认值是false,当执行完loadAllCategories()后将didInitialAttachCategories的值设为true,其实就是只调用一次loadAllCategories()方法
- 
load_categories_nolock的调用流程:load_images-->loadAllCategories-->load_categories_nolock-->attachCategories
attachLists方法分析
attachLists方法得源码如下:
void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    if (hasArray()) {
        // many lists -> many lists
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount;
        array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
        newArray->count = newCount;
        array()->count = newCount;
        for (int i = oldCount - 1; i >= 0; I--)
            newArray->lists[i + addedCount] = array()->lists[I];
        for (unsigned i = 0; i < addedCount; I++)
            newArray->lists[i] = addedLists[I];
        free(array());
        setArray(newArray);
        validate();
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
        validate();
    } 
    else {
        // 1 list -> many lists
        Ptr<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;
        if (oldList) array()->lists[addedCount] = oldList;
        for (unsigned i = 0; i < addedCount; I++)
            array()->lists[i] = addedLists[I];
        validate();
    }
}
从源码可以看出,attachLists方法总共有三个流程分支:
【流程1】:0 lists -> 1 list
- 将addedLists[0]的指针赋值给list
【流程2】:1 list -> many lists
- 计算旧的list的个数
- 计算新的list个数 ,新的list个数 = 原有的list个数 + 新增的list个数
- 根据newCount开辟相应的内存,类型是array_t类型,并设置数组标识位-setArray
- 将原有的list放在数组的末尾,因为最多只有一个不需要遍历存储
- 遍历addedLists将遍历的数据从数组的开始位置存储
【流程3】:many lists -> many lists
- 判断 - hasArray()是否存在
- 计算 - 原有的数组中的- list个数,- array()->lists
- 计算 - 新的list个数 ,- 新的list个数 = 原有的list个数 + 新增的list个数
- 根据 - newCount开辟相应的内存,类型是- array_t类型
- 设置新数组的个数等于 - newCount
- 设置原有数组的个数等于 - newCount
- 遍历原有数组中list将其存放在 - newArray->lists中 且是放在- 数组的末尾
- 遍历 - addedLists将遍历的数据从数组的开始位置存储
- 释放原有的 - array()
- 设置新的 - newArray
- list_array_tt结构和方法分析

- 
rwe结构中的方法、属性、协议的类型都是继承自list_array_tt,在底层是二维数组的形式存储

实例验证分类的加载
通过上面的两个例子,我们可以大致将类 和 分类 是否实现+load方法的情况分为4种
| 类和分类 | 分类实现+load | 分类未实现+load | 
|---|---|---|
| 类实现+load | 非懒加载类+非懒加载分类<span class="Apple-tab-span" style="white-space:pre"></span> | 非懒加载类+懒加载分类<span class="Apple-tab-span" style="white-space:pre"></span> | 
| 类未实现+load | 懒加载类+非懒加载分类<span class="Apple-tab-span" style="white-space:pre"></span> | 懒加载类+懒加载分类 | 
准备工作
- 创建HTPerson类以及分类HTPerson (HTA)
非懒加载类和非懒加载分类的加载
即主类实现了+load方法,分类同样实现了+load方法,在前文分类的加载时机时,我们已经分析过这种情况,所以可以直接得出结论,这种情况下
- 程序启动,会直接加载非懒加载类,加载主类的方法
- 
分类的数据加载是通过load_images加载到类中的
运行代码,发现会调用attachCategories方法,来加载分类信息,通过bt查看函数调用栈

在相应函数出设置断点,打印结果如下

- 通过MachOView查看可执行文件

非懒加载类与懒加载分类
即主类实现了+load方法,分类未实现+load方法
- 运行程序,发现并没有调用attachCategories方法,那么分类是如何加载的呢?

- 在realizeClassWithoutSwift方法处设置断点,我们来看一下ro是否有分类方法

- 获取ro的方法列表:p ro->baseMethods()
- 打印第i个方法信息: p $2.get(i).big()

从上面的打印输出可以看出,分类的方法和类的方法已经合并到一起了,方法的顺序是 HTA分类-HTPerson类,此时分类已经 加载进来了,但是还没有排序,说明这种情况下分类数据在编译时就与类数据合并到一起了,不需要运行时添加进去
- 通过MachOView查看可执行文件

懒加载类与懒加载分类
即主类和分类均未实现+load方法
- 程序启动时,类数据不会加载,只有在首次接收消息时才加载

其中realizeClassMaybeSwiftMaybeRelock是消息流程中慢速查找中的函数,即在第一次调用消息时才会去加载懒加载类
- 在realizeClassWithoutSwift方法处设置断点,我们来看一下ro是否有分类方法

- 通过MachOView查看可执行文件

【结论】:
- 懒加载类与懒加载分类的数据加载是在消息第一次调用时加载
- 分类数据与类数据,在编译时已合并到一起,MachO文件中的分类列表__objc_catlist中无分类
懒加载类与非懒加载分类
即主类未实现+load方法,分类实现了+load方法
- 运行程序,会调用realizeClassWithoutSwift方法,即程序一启动,就会记载类数据,产看函数调用栈如下图:

- 在realizeClassWithoutSwift方法处设置断点,我们来看一下ro是否有分类方法

- 从上面的打印输出可以看出,分类的方法和类的方法已经合并到一起了,方法的顺序是 - HTA分类-HTPerson类,此时分类已经 加载进来了,但是还没有排序,说明这种情况下分类数据在- 编译时就与类数据合并到一起了,不需要运行时添加进去
- 通过 - MachOView查看可执行文件

结论:
- 懒加载类变成非懒加载类,分类的数据在编译期间合并到类数据中
多分类的情况
新增两个分类,HTPerson (HTB)和HTPerson (HTC)

通过不同组合来,验证类和分类的加载,总结如下
| 实现+load方法的分类个数 | 非懒加载类 | 懒加载类 | 
|---|---|---|
| 0 | 编译时类数据与分类数据已合并 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 | 首次接收消息时,才加载类数据,分类数据与类数据,在 编译时已合并到一起 </br> MachO文件中类和分类数据如下:</br>  __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:0  </br> __objc_nlcatlist:0 | 
| 1 | 程序启动加载类数据,load_images时加载分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:1 | 程序启动加载类数据( 编译器将类标记为非懒加载类),分类数据与类数据,在编译时已合并到一起 </br> MachO文件中类和分类数据如下:</br>  __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:0  </br> __objc_nlcatlist:0 | 
| 2 | 程序启动加载类数据,load_images时加载分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:2 | 编译后, 类仍是懒加载类,程序启动(load_images方法中)会加载类和分类数据 </br> MachO文件中类和分类数据如下:</br>  __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:3  </br> __objc_nlcatlist:2 | 
| 3 | 程序启动加载类数据,load_images时加载分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:3 | 编译后, 类仍是懒加载类,程序启动(load_images方法中)会加载类和分类数据 </br> MachO文件中类和分类数据如下:</br>  __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:3  </br> __objc_nlcatlist:3 | 
- 1、非懒加载类 + 3个懒加载分类

- 2、非懒加载类 + 2个懒加载分类 + 1个非懒加载分类
 程序启动加载类数据,load_images时按照MachO中 __objc_catlist中的顺序挨个加载分类数据

- 3、非懒加载类 + 1个懒加载分类 + 2个非懒加载分类
程序启动加载类数据,load_images时按照MachO中 __objc_catlist中的顺序挨个加载分类数据
- 4、非懒加载类 + 3个非懒加载分类
程序启动加载类数据,load_images时按照MachO中 __objc_catlist中的顺序挨个加载分类数据
- 5、懒加载类 + 3个懒加载分类
首次接收消息时,才加载类数据,分类数据与类数据,在编译时已合并到一起

- 6、懒加载类 + 2个懒加载分类 + 1个非懒加载分类
程序启动加载类数据(编译器将类标记为非懒加载类),分类数据与类数据,在编译时已合并到一起

- 7、懒加载类 + 1个懒加载分类 + 2个非懒加载分类
编译后,类仍是懒加载类,程序启动(load_images方法中)会加载类和分类数据,类和分类的加载流程:load_images --> prepare_load_methods --> realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories

- 8、懒加载类 + 3个非懒加载类
编译后,类仍是懒加载类,程序启动(load_images方法中)会加载类和分类数据,类和分类的加载流程:load_images --> prepare_load_methods --> realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories









