美文网首页
iOS底层原理19:类和分类的加载

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

作者: 黑白森林无间道 | 来源:发表于2021-08-09 22:57 被阅读0次

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

    分类的本质

    准备工作

    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

    相关文章

      网友评论

          本文标题:iOS底层原理19:类和分类的加载

          本文链接:https://www.haomeiwen.com/subject/ouzbvltx.html