美文网首页Objective - C 底层
Objective - C Category(一)定义及底层实现

Objective - C Category(一)定义及底层实现

作者: 爱玩游戏的iOS菜鸟 | 来源:发表于2020-04-14 16:52 被阅读0次

    Categoty要点

    1. Category的使用场合是什么?
    2. Category的实现原理
    3. Category和Class Extension的区别是什么?
    4. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
    5. load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
    6. Category能否添加成员变量?如果可以,如何给Category添加成员变量?

    下面我们一条一条的来学习和理解Categoty主要的几点

    (一)什么是分类(Category)? Category主要使用场合

    1. 什么是Category?
    • Category编译之后是一个struct(category_t),
      结构体主要包含分类定义的实例方法以及类方法
    1. Category主要使用场合
      1. 声明私有方法
      1. 分解体积庞大的类文件
      1. 把FrameWork的私有方法公开化(“覆盖”原方法)

    (二)Category的实现原理

    在学习Category的实现原理之前,我们需要先知道Category的底层结构

    (1)Category的底层结构
    1. Category的定义在objc-runtime-new.h文件中:
    //重点属性已注解 其余可暂时忽略
    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;//属性列表
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta) {
            if (isMeta) return nil; // classProperties;
            else return instanceProperties;
        }
    };
    

    另外也可通过将指定的分类编译生成.cpp文件,也可以查看。

    1. 如何在runtime源码中,查看category底层具体的实现原理呢?

    源码解读顺序:
    (1) objc-os.mm文件

    • _objc_init
    • map_images
    • map_images_nolock 找到_read_images(hList, hCount);方法

    (2) objc-runtime-new.mm文件

    • _read_images
    • attachCategories
    • attachLists
    • reallocmemmovememcpy

    其中非常重要的方法attachCategories

    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations
        //定义3个数组
        //二维数组 [[method_t,method_t],[method_t],[method_t,method_t]]
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        //属性数组
        //二维数组 [[property_t,property_t],[property_t],[property_t,property_t]]
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        //协议数组
        //二维数组 [[protocol_t,protocol_t],[protocol_t],[protocol_t,protocol_t]]
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first
        //记录对应的数组中的长度
        int mcount = 0;
        int propcount = 0;
        int protocount = 0;
        int i = cats->count;
        bool fromBundle = NO;
        while (i--) {
            //取出某个分类
            auto& entry = cats->list[i];
            //分别将分类的方法、属性、协议列表放到对应的大数组中
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
    
            property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
    
        //得到类对象里面的数据(方法数组、属性数组、协议数组)
        auto rw = cls->data();
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    
        //将所有分类的对象方法,附加到类对象的对象方法列表中
        rw->methods.attachLists(mlists, mcount);
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
        //同上
        rw->properties.attachLists(proplists, propcount);
        free(proplists);
        //同上
        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }
    

    接下来调用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;
                //将原方法列表的内存往后挪动新增分类方法列表数量的位置 即将原方法放到数组最后面
                //具体分类添加方法的顺序,看编译顺序
                //[[category1_method_t,category1_method_t],[category2_method_t],[old_method_t,old_method_t]]
                memmove(array()->lists + addedCount, array()->lists, 
                        oldCount * sizeof(array()->lists[0]));
                //
                memcpy(array()->lists, addedLists, 
                       addedCount * sizeof(array()->lists[0]));
            }
            else if (!list  &&  addedCount == 1) {
                // 0 lists -> 1 list
                list = addedLists[0];
            } 
            else {
                // 1 list -> many lists
                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;
                memcpy(array()->lists, addedLists, 
                       addedCount * sizeof(array()->lists[0]));
            }
        }
    

    因此,通过上面对runtime源码的分析,我们还可以看出:

    1. 如果分类方法与原方法同名,则分类方法优先调用(并没有“覆盖”,“掩盖”更恰当)
    2. 不同的分类方法同名,则看编译顺序,最后参与编译的优先级更高

    编译顺序可以通过编译日志看出,可以在Build Phase -> Compile Sources 中文件的顺序更改

    memcpy函数与memmove函数

    上面添加方法时,用到了两个函数:memcpymemmove

    1. void *memcpy(void *__dst, const void *__src, size_t __n);
      说明:memcpy功能和memmove相同,但是memcpy中dest和source中的区域不能重叠,否则会出现未知结果。
    2. void *memmove(void *__dst, const void *__src, size_t __len);
      说明:memmove用于从source拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中

    最后将Category的加载处理流程进行整理总结:

    Category的加载处理流程
    1. 通过Runtime加载某个类的所有Category数据
    2. 把所有Category的方法、属性、协议数据,合并到一个新的大数组中
    3. 将合并后的Category数据(方法、属性、协议),插入到类原来数据的前面,并优先调用
    Category的实现原理

    Category在编译之后的底层结构是struct category_t ,存储着对象方法、类方法、协议、属性信息,在程序启动的runtime阶段,会将Category中的数据,合并到(类对象、元类对象)中

    Category和Class Extenstion的区别
    • Extenstion在编译时数据就已经包含在类信息中 ;Category在runtime才会将数据合并到类信息中
    • Extenstion可直接添加私有成员变量,而Category需要通过关联对象才可以;
    • Extenstion不能为系统库添加扩展,而Category可以系统库添加分类

    PS:重新走一遍分析流程,并将自己所用的分类进行整理,并思考其底层原理

    相关文章

      网友评论

        本文标题:Objective - C Category(一)定义及底层实现

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