美文网首页
iOS-类的加载(下)

iOS-类的加载(下)

作者: Y丶舜禹 | 来源:发表于2020-10-28 16:34 被阅读0次

    前言

    在之前的文章iOS-类的加载(上),我们探究了是如何加载到内存中以及懒加载类非懒加载类,这篇文章下我们将探寻一下分类的加载情况。

    分类的本质

    在main文件中新建一个ZGPerson分类,

    @interface ZGPerson (ZG)
    
    @property (nonatomic, strong) NSString *cate_name;
    @property (nonatomic, assign) int cate_age;
    
    - (void)cateA_instanceMethod1;
    
    - (void)cateA_instanceMethod3;
    
    - (void)cateA_instanceMethod2;
    
    - (void)cateA_classMethod3;
    
    @end
    
    @implementation ZGPerson (ZG)
    
    
    + (void)load{
        
    }
    
    - (void)cateA_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_instanceMethod3{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_instanceMethod2{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_classMethod3{
        NSLog(@"%s",__func__);
    }
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
    
            ZGPerson *person = [ZGPerson alloc];
            NSLog(@"%p",person);
        }
        return 0;
    }
    

    我们可以通过下面几种方式探寻分类本质

    • 通过clang
    • 通过Xcode文档搜索Category
    • 通过objc源码搜索 category_t
    通过clang

    通过终端命令clang -rewrite-objc main.m -o main.cpp 查看底层编译,即 生成main.cpp,打开。

    ZGPerson分类
    _category_t

    可见,分类的本质是_category_t

    通过Xcode文档搜索Category

    可以通过Xcode文档搜索Category文档

    Category
    通过objc源码搜索 category_t

    在源码中搜索_category_t发现以下定义

    struct category_t {
        const char *name;
        classref_t cls;
        struct method_list_t *instanceMethods;
        struct method_list_t *classMethods;
        struct protocol_list_t *protocols;
        struct property_list_t *instanceProperties;
        // Fields below this point are not always present on disk.
        struct property_list_t *_classProperties;
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
        
        protocol_list_t *protocolsForMeta(bool isMeta) {
            if (isMeta) return nullptr;
            else return protocols;
        }
    };
    

    其中

    • name:分类的名字
    • cls:对应的原类
    • instanceMethods:实例方法列表
    • classMethods:类方法列表
    • protocols:协议列表
    • instanceProperties:实例属性列表

    分类加载流程

    首先,创建两个分类ZGPerson + ZGAZGPerson + ZGB

    @interface ZGPerson (ZGA)
    
    
    @property (nonatomic, strong) NSString *cate_name;
    @property (nonatomic, assign) int cate_age;
    
    - (void)cateA_instanceMethod1;
    
    - (void)cateA_instanceMethod3;
    
    - (void)cateA_instanceMethod2;
    
    - (void)cateA_classMethod3;
    
    @end
    
    @implementation ZGPerson (ZGA)
    
    + (void)load{
        
    }
    
    - (void)cateA_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_instanceMethod3{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_instanceMethod2{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_classMethod3{
        NSLog(@"%s",__func__);
    }
    
    @end
    

    ZGPerson (ZGB)

    @interface ZGPerson (ZGB)
    
    
    @property (nonatomic, strong) NSString *cate_name;
    @property (nonatomic, assign) int cate_age;
    
    - (void)cateA_instanceMethod1;
    
    - (void)cateA_instanceMethod3;
    
    - (void)cateA_instanceMethod2;
    
    - (void)cateA_classMethod3;
    
    @end
    
    @implementation ZGPerson (ZGB)
    
    + (void)load{
        
    }
    
    - (void)cateA_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_instanceMethod3{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_instanceMethod2{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateA_classMethod3{
        NSLog(@"%s",__func__);
    }
    
    @end
    

    在上一篇iOS-类的加载(上)文章中分析了类的加载流程:realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加载,其中分析了分类的data数据 时如何 加载到中的,分类的加载顺序是:是编译时加载进内存的顺序决定,并且越晚加进来,越在前面

    methodizeClass的源码实现中,我们发现类的数据分类的数据是分开处理的

    static void methodizeClass(Class cls, Class previously)
    {
        .....省略一些代码
    
        // Attach categories. 链接分类
        if (previously) {
            if (isMeta) {
                objc::unattachedCategories.attachToClass(cls, previously,
                                                         ATTACH_METACLASS);
            } else {
                // When a class relocates, categories with class methods
                // may be registered on the class itself rather than on
                // the metaclass. Tell attachToClass to look for those.
                objc::unattachedCategories.attachToClass(cls, previously,
                                                         ATTACH_CLASS_AND_METACLASS);
            }
        }
        objc::unattachedCategories.attachToClass(cls, cls,
                                                 isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
    
    #if DEBUG
        // Debug: sanity-check all SELs; log method list contents
        for (const auto& meth : rw->methods()) {
            if (PrintConnecting) {
                _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(meth.name));
            }
            ASSERT(sel_registerName(sel_getName(meth.name)) == meth.name); 
        }
    #endif
    }
    

    那么为什么是这样处理的呢?原来是因为在编译阶段,类就已经被确认好了内存位置,是clean memory(即实例方法存储在中,类方法存储在元类中),而分类是后面才加进来的

    分类通过attatchToClass添加到本类,下面我们查看attatchToClass源码

    attatchToClass源码
    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 *ZGPersonName = "ZGPerson";
    
            if (strcmp(mangledName, ZGPersonName) == 0) {
                bool kc_isMeta = cls->isMetaClass();
                auto kc_rw = cls->data();
                auto kc_ro = kc_rw->ro();
                printf("%s: 这个是我要研究的 %s \n",__func__, ZGPersonName);
    
            }
            
            
            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);
            }
        }
    

    但是当我们运行的时候却发现并未走进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);
        auto rwe = cls->data()->extAllocIfNeeded();// rwe 初始化 --> 要对copy出的干净内存进行方法插入操作
        // 自己加的调试代码
        const char *mangledName  = cls->mangledName();
        const char *ZGPersonName = "ZGPerson";
    
        if (strcmp(mangledName, ZGPersonName) == 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__,ZGPersonName);
            }
        }
    
        // 遍历 分类数据的准备(method property protocol)
        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);
    }
    
    

    可以看出,这确实是链接分类的方法,但是为什么没调用呢?这个我们稍后再说,这里我们先分析一下这个方法做了什么?其中我们主要分析一下排序方法prepareMethodLists和插入方法attachLists

    prepareMethodLists
    static void 
    prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                       bool baseMethods, bool methodsFromBundle)
    {
        runtimeLock.assertLocked();
        
        const char *mangledName  = cls->mangledName();
        const char *ZGPersonName = "ZGPerson";
    
        if (strcmp(mangledName, ZGPersonName) == 0) {
            bool kc_isMeta = cls->isMetaClass();
            auto kc_rw = cls->data();
            auto kc_ro = kc_rw->ro();
            printf("%s: 这个是我要研究的 %s \n",__func__,ZGPersonName);
        }
        
    
        if (addedCount == 0) return;
    
        // There exist RR/AWZ/Core special cases for some class's base methods.
        // But this code should never need to scan base methods for RR/AWZ/Core:
        // default RR/AWZ/Core cannot be set before setInitialized().
        // Therefore we need not handle any special cases here.
        if (baseMethods) {
            ASSERT(cls->hasCustomAWZ() && cls->hasCustomRR() && cls->hasCustomCore());
        }
    
        // Add method lists to array.
        // Reallocate un-fixed method lists.
        // The new methods are PREPENDED to the method list array.
    
        for (int i = 0; i < addedCount; i++) {
            method_list_t *mlist = addedLists[I];
            ASSERT(mlist);
    
            // Fixup selectors if necessary
            if (!mlist->isFixedUp()) {
                fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
            }
        }
    
        // If the class is initialized, then scan for method implementations
        // tracked by the class's flags. If it's not initialized yet,
        // then objc_class::setInitialized() will take care of it.
        if (cls->isInitialized()) {
            objc::AWZScanner::scanAddedMethodLists(cls, addedLists, addedCount);
            objc::RRScanner::scanAddedMethodLists(cls, addedLists, addedCount);
            objc::CoreScanner::scanAddedMethodLists(cls, addedLists, addedCount);
        }
    }
    
    

    从其中的排序方法fixupMethodList中可以看出,排序是通过方法的内存地址排序的即SEL name

    static void 
    fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
    {
        runtimeLock.assertLocked();
        ASSERT(!mlist->isFixedUp());
    
        // fixme lock less in attachMethodLists ?
        // dyld3 may have already uniqued, but not sorted, the list
        if (!mlist->isUniqued()) {
            mutex_locker_t lock(selLock);
        
            // Unique selectors in list.
            for (auto& meth : *mlist) {
                const char *name = sel_cname(meth.name);
                meth.name = sel_registerNameNoLock(name, bundleCopy);
            }
        }
        // sel - imp
        // Sort by selector address.//通过内存地址排序
        if (sort) {
            method_t::SortBySELAddress sorter;
            std::stable_sort(mlist->begin(), mlist->end(), sorter);
        }
        
        // Mark method list as uniqued and sorted
        mlist->setFixedUp();
    }
    
    struct method_t {
        SEL name;
        const char *types;
        MethodListIMP imp;
    
        struct SortBySELAddress :
            public std::binary_function<const method_t&,
                                        const method_t&, bool>
        {
            bool operator() (const method_t& lhs,
                             const method_t& rhs)
            { return lhs.name < rhs.name; }
        };
    };
    
    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;
                setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));// 扩容
                array()->count = newCount;
                memmove(array()->lists + addedCount, array()->lists,
                        oldCount * sizeof(array()->lists[0]));// 移动旧的去后面
                memcpy(array()->lists, addedLists,
                       addedCount * sizeof(array()->lists[0]));// cpy 新的在前面
            }
            else if (!list  &&  addedCount == 1) {
                // 0 lists -> 1 list
                // 0到1 - list 没有数据第一次 - 第0个元素给list,此时 list 是一维的
                list = addedLists[0];
            }
            else {
                // 1 list -> many lists
                // 1+many - 举例 many lists 是3个
                List* oldList = list;
                uint32_t oldCount = oldList ? 1 : 0;
                uint32_t newCount = oldCount + addedCount;// 容量计算,旧+新的和 1+3=4
                setArray((array_t *)malloc(array_t::byteSize(newCount)));// 开辟总大小的空间 newCount
                array()->count = newCount;// array 的数量:是新的添加 manylists 后的数量
                if (oldList) array()->lists[addedCount] = oldList;// 旧的list 放最后面 第3个位置
                // memcpy(位置, 放谁, 大小)
                // 把新的 lists 从起始位置0开始放
                memcpy(array()->lists, addedLists,
                       addedCount * sizeof(array()->lists[0]));
            }
        }
    

    这里加了一些注释,可以看出加入的array(方法列表,属性列表,协议列表)总是会加在的前面,这也就解释了为什么分类的方法会在本类之前调用

    attachLists
    attachCategories分析

    刚才我们一直没调用到attachCategories方法,那我们只能想一下其他的办法,在源码中搜索attachCategories,看看都会在哪里调用,发现只有两处调用

    • attachToClass()
    • load_categories_nolock()

    经过探索,如果想要调用attachCategories方法,我们只需要在分类里面加一个+ load方法,变会调用attachCategories方法

    attachCategories

    其中的函数调用栈如下


    load_categories_nolock

    而在 attachToClass方法中,这里经过调试发现,基本不会进到if流程调用attachCategories,除非加载两次,一般的类一般只会加载一次

    所以接下来我们只需要研究load_categories_nolock()的调用,我们全局搜索load_categories_nolock的调用的地方,发现只有两次调用

    static void loadAllCategories() {
        mutex_locker_t lock(runtimeLock);
    
        for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
            load_categories_nolock(hi);
        }
    }
    
    if (didInitialAttachCategories) {
            for (EACH_HEADER) {
                load_categories_nolock(hi);
            }
        }
    

    经过我们调试发现,只有loadAllCategories方法才会调用load_categories_nolock,另外一个基本不会调用。

    继续全局搜索loadAllCategories,发现只在load_images中调用

    void
    load_images(const char *path __unused, const struct mach_header *mh)
    {
        if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
            didInitialAttachCategories = true;
            loadAllCategories();
        }
    
        // Return without taking locks if there are no +load methods here.
        if (!hasLoadMethods((const headerType *)mh)) return;
    
        recursive_mutex_locker_t lock(loadMethodLock);
    
        // Discover load methods
        {
            mutex_locker_t lock2(runtimeLock);
            prepare_load_methods((const headerType *)mh);
        }
    
        // Call +load methods (without runtimeLock - re-entrant)
        call_load_methods();
    }
    

    所以综上所述,该情况下的分类的数据加载时机的反推路径为:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images

    而我们的分类加载正常的流程的路径为:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories

    分类加载

    分类和类的各种加载时机

    我们可以大致将类 和 分类 是否实现+load的情况分为4种

    类+分类
    分类实现+load 分类未实现+load
    类实现+load 非懒加载类+非懒加载分类 非懒加载类+懒加载分类
    类未实现+load 懒加载类+非懒加载分类 懒加载类+懒加载分类
    • 【情况1】非懒加载类 + 非懒加载分类

    • 【情况2】非懒加载类 + 懒加载分类

    • 【情况3】懒加载类 + 懒加载分类

    • 【情况4】懒加载类 + 非懒加载分类

    非懒加载类 + 非懒加载分类

    • 类的数据加载是通过 _getObjc2NonlazyClassList 加载,即对 rorw 的操作和对 rwe 赋值初始化,在extAlloc 方法中
    • 分类的数据加载是通过 load_images 加载到类中的

    调用路径为

    • map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass,此时的 mlists 是一维数组,然后走到 load_images 部分。
    • load_images -> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此时的 mlists 是二维数组。
    非懒加载类 + 非懒加载分类

    非懒加载类 + 懒加载分类

    非懒加载类 + 懒加载分类
    • 类和分类的加载在 read_images 就加载好数据了
    • 其中 data 数据在编译期就已经完成了

    懒加载类 + 非懒加载分类

    懒加载类 + 非懒加载分类
    • 此情况下会迫使主类提前加载,即主类强行转换为 非懒加载类样式

    懒加载类 + 懒加载分类

    懒加载类 + 懒加载分类
    • 此情况下懒加载类与懒加载分类的数据加载是在 消息第一次调用时加载

    总结

    • 非懒加载类 + 非懒加载分类,其数据的加载在load_images 方法中,首先对类进行加载,然后把分类的信息贴到类中。
    • 非懒加载类 + 懒加载分类,其数据加载在read_image 就加载数据,数据来自datadata在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起。
    • 懒加载类 + 懒加载分类,其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成。
    • 懒加载类 + 非懒加载分类,只要分类实现了load,会迫使主类提前加载,即在_read_images 中不会对类做实现操作,需要在 load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据。

    相关文章

      网友评论

          本文标题:iOS-类的加载(下)

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