iOS底层原理19:类和分类的加载

前面已经探究了类的加载流程,类分为懒加载类非懒加载类,他们有不同加载流程,下面来探究下分类的加载,以及分类和类搭配使用的情况

分类的本质

准备工作

main.m中定义 HTPerson的分类HT, 代码如下

image

探索分类本质的三种方法

探索分类的本质,有以下三种方式

  • 【方式一】通过clang
  • 【方式二】通过Xcode文档搜索Category
  • 【方式三】通过objc源码搜索 category_t

【方式一】:通过clang

通过clang -rewrite-objc main.m -o main.cpp命令,查看编译后的 c++文件

  • 其中分类的类型是_category_t,存储了相应的实例方法类方法属性协议等信息
image

搜索struct _category_t,如下所示

  • 其中有两个_method_list_t,分别对应对象方法类方法
image
  • 全局搜索_CATEGORY_INSTANCE_METHODS_HTPerson_,找到其底层实现
image
  • 查看协议属性的结构
image

这里我们发现一个【问题】:分类中定义的属性没有相应的set、get方法,我们可以通过关联对象来设置(关于如何设置关联对象,我们将在下一篇中进行分析)

【方式二】:通过Xcode文档搜索 Category

通过快捷键command+shift+0,搜索Category

image

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

通过objc818源码搜索category_t类型

image

分类的加载的源码分析

分类的底层结构是结构体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方法
image

image

attachToClass流程流程分析

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

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

load_categories_nolock流程分析

  • 全局搜索load_categories_nolock,在loadAllCategories方法中调用
image
  • 接着全局搜索loadAllCategories,在load_images方法中调用
image
  • 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结构和方法分析

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

实例验证分类的加载

通过上面的两个例子,我们可以大致将分类 是否实现+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查看函数调用栈

image

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


image
  • 通过MachOView查看可执行文件
image

非懒加载类与懒加载分类

主类实现了+load方法,分类未实现+load方法

  • 运行程序,发现并没有调用attachCategories方法,那么分类是如何加载的呢?
image
  • realizeClassWithoutSwift方法处设置断点,我们来看一下ro是否有分类方法
image
  • 获取ro的方法列表:p ro->baseMethods()
  • 打印第i个方法信息: p $2.get(i).big()
image

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

  • 通过MachOView查看可执行文件
image

懒加载类与懒加载分类

主类和分类均未实现+load方法

  • 程序启动时,类数据不会加载,只有在首次接收消息时才加载
image

其中realizeClassMaybeSwiftMaybeRelock是消息流程中慢速查找中的函数,即在第一次调用消息时才会去加载懒加载类

  • realizeClassWithoutSwift方法处设置断点,我们来看一下ro是否有分类方法
image
  • 通过MachOView查看可执行文件
image

【结论】:

  • 懒加载类与懒加载分类的数据加载是在消息第一次调用时加载
  • 分类数据与类数据,在编译时已合并到一起,MachO文件中的分类列表__objc_catlist中无分类

懒加载类与非懒加载分类

主类未实现+load方法,分类实现了+load方法

  • 运行程序,会调用realizeClassWithoutSwift方法,即程序一启动,就会记载类数据,产看函数调用栈如下图:
image
  • realizeClassWithoutSwift方法处设置断点,我们来看一下ro是否有分类方法
image
  • 从上面的打印输出可以看出,分类的方法和类的方法已经合并到一起了,方法的顺序是 HTA分类-HTPerson类,此时分类已经 加载进来了,但是还没有排序,说明这种情况下分类数据在编译时就与类数据合并到一起了,不需要运行时添加进去

  • 通过MachOView查看可执行文件

image

结论:

  • 懒加载类变成非懒加载类,分类的数据在编译期间合并到类数据中

多分类的情况

新增两个分类,HTPerson (HTB)HTPerson (HTC)

image

通过不同组合来,验证类和分类的加载,总结如下

实现+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个懒加载分类
image
  • 2、非懒加载类 + 2个懒加载分类 + 1个非懒加载分类
    程序启动加载类数据,load_images时按照MachO中 __objc_catlist中的顺序挨个加载分类数据
image
  • 3、非懒加载类 + 1个懒加载分类 + 2个非懒加载分类

程序启动加载类数据,load_images时按照MachO中 __objc_catlist中的顺序挨个加载分类数据

  • 4、非懒加载类 + 3个非懒加载分类

程序启动加载类数据,load_images时按照MachO中 __objc_catlist中的顺序挨个加载分类数据

  • 5、懒加载类 + 3个懒加载分类

首次接收消息时,才加载类数据,分类数据与类数据,在编译时已合并到一起

image

  • 6、懒加载类 + 2个懒加载分类 + 1个非懒加载分类

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

image

  • 7、懒加载类 + 1个懒加载分类 + 2个非懒加载分类

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

image
  • 8、懒加载类 + 3个非懒加载类

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

image

推荐阅读更多精彩内容