美文网首页
OC基础-(一)Category

OC基础-(一)Category

作者: 码小六 | 来源:发表于2018-10-27 16:12 被阅读0次

    什么是Category(分类)?

    Category是Objctive-C 2.0之后所添加的语言特性,可用于为类添加方法(实例方法,类方法,协议,属性)。我们通常使用Category做什么呢?

    • 声明私有方法
    • 分解体积庞大的文件
    • 把Framework私有方法公开
    • 模拟多继承

    分类这种特性对于纯动态语言来说,不算什么,但是对于C、C++这种编译期决定一切的语言来说,还是非常有用的。

    先来说说分类的特性:

    • 运行时决议。当我们写好分类后,并没有立刻将写好的分类的方法添加到对应的宿主类中。而是在运行时通过Runtime,在运行时才添加到对应的宿主类上。
    • 可以为系统类添加分类。
    • 可以为类添加方法(实例方法,类方法,协议),但是不能添加成员变量(因为在运行时,内存布局就已经确定了,如果添加成员变量,会破坏类的内部结构)
    • 可以为类添加属性,这里要注意的是,和我们在类中正常定义属性不同,我们在分类中添加属性,只是生成了对应的set/get方法。并没添加成员变量。

    关于运行时决议这点,就不得不说说分类和Extension的区别了。我们通常用Extension来隐藏类的私有信息。

    • Extension只是一个声明文件(.h,而Category有声明[.h]和实现[.m])
    • Extension是编译时决议的,伴随类的产生而产生,随之消亡而消亡
    • 只能为已知的类添加Extension
    • Extension可以添加成员变量

    Category编译后的源码分析

    到这里,大致说明了分类的特性。但是需要了解其工作原理,还需要看objc的源码。我们打开源码,然后找到分类的结构体定义:

    struct category_t {
        const char *name;   // 分类名(小括号里写的名字)
        classref_t cls;     // 宿主对象 实际上是 class 类型 编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
        struct method_list_t *instanceMethods;  // 实例方法列表
        struct method_list_t *classMethods;     // 类方法列表
        struct protocol_list_t *protocols;      // 协议列表
        struct property_list_t *instanceProperties; // 属性列表(不过这个property不会@synthesize成员变量,一般有需求添加成员变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,但这种方法生成的与一个普通的成员变量完全是两码事。)
        // Fields below this point are not always present on disk.
        struct property_list_t *_classProperties;   // 类属性列表(@property (class,nonatomic, strong) NSString *name; 然后还需要现实+的getter setter方法,然后就可以用.号使用
        // 这两个是用来筛选方法的,不用关注
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    };
    

    看到了分类的内部结构,可以很清楚的看到,分类能够为类添加哪些东西。

    我们在看看编译器是如何帮我们编译我们创建的分类的

    定义一个Person类,其中不做任何的事情。然后定义一个分类Person+MyCat。添加一个run方法

    #import "Person+MyCat.h"
    
    @implementation Person (MyCat)
    
    - (void)run {
        NSLog(@"Person run ...");
    }
    
    @end
    

    我们使用【clang -rewrite-objc -fobjc-arc 文件名.m】,编译器命令来编译我们的分类文件Person+MyCat,会得到一个.cpp文件。其中大多数对我们要研究的分类是无用的,我们只要关注最下方的几百行代码即可。

    这里我们截取对我们探究有用的代码:

    /* @implementation Person (MyCat) 这里是我们在分类中声明的方法的函数实现
       我们可以先看这个方法的命名_I(表示这是一个实例方法)_Person(宿主类名)_MyCat(分类名)_run(方法名)
       其中传入2个参数,Person * self(宿主对象) 和 SEL _cmd(方法选择器)
       那么既然函数为我们定义好了,这个函数是在哪里调用的,我们通过方法名找到调用位置
     */
    static void _I_Person_MyCat_run(Person * self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_zv4vj4y51q1fjd_d9402351m0000gn_T_Person_MyCat_ba0f5f_mi_0);
    }
    
    /*
     我们定位到了这里。这里又是一个结构体 _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_MyCat 这个名称就不再做解释了
     我们关注调用我们函数的那一行代码
     {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_MyCat_run}}
     来理解一下,为什么函数被包装成了这样
     (struct objc_selector *)"run": 这里将"run"显示转换成了一个方法选择器指针
     "v16@0:8": 这涉及到苹果的type encoding技术,相关的技术,会在之后的章节说明
     (void *)_I_Person_MyCat_run: 这里是我们的函数
     
     那么,又是谁调用到了_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_MyCat?
     */
    static struct /*_method_list_t*/ {  // 这里用一个method_t数组来存储方法
        unsigned int entsize;  // sizeof(struct _objc_method) 方法大小
        unsigned int method_count;  // 方法数量
        struct _objc_method method_list[1]; // 方法本身
    } _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_MyCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_MyCat_run}}
    };
    
    // 上面的结构体在这里被调用了 _category_t 实际上就是category_t的结构体 我们可以按照属性对应一下
    // 要注意的是,为了方便查看各个不同情况的方法,协议所添加的位置,又在分类中添加了一个静态方法和一个属性以及一个协议
    static struct _category_t _OBJC_$_CATEGORY_Person_$_MyCat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Person", // 宿主类名
        0, // &OBJC_CLASS_$_Person,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_MyCat,// 实例方法名
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_MyCat,// 类方法名
        (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_MyCat,// 协议
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_MyCat,// 属性
    };
    

    通过上面的源码 我们可以看出,分类就是将方法封装在一个method_list_t的数组当中,其中的每个对象都是method_tmethod_t仅仅是一个封装了函数选择器,函数返回值和参数,函数实现的结构体。

    struct method_t {
        SEL name;   // 方法选择器
        const char *types;  // 函数返回值和参数
        IMP 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; }
        };
    };
    

    在回到编译的源码,我们可以看到,_OBJC_$_CATEGORY_Person_$_MyCat最后被放在了DATA段下的objc_catlist中,其中保存了一个大小为1的category_t数组。如果有多个分类,就是多个category_t啦!

    static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
        &_OBJC_$_CATEGORY_Person_$_MyCat,
    };
    

    大致结构如下:

    Xnip2018-10-27_14-57-40.png

    Category是如何加载的?

    前面说到了,Category是在运行时加载的,所以,到底是在哪一步被加载的?先来看看Category的主要的几个调用方法。

    1. 当程序启动后,对象在运行时会调用_objc_init方法
    2. 在初始化过程中会调用map_2_images函数,这里函数名中的images表示的是镜像
    3. 之后又会调用map_images_nolock
    4. 再接下来会调用_read_images,读取镜像,加载一些可执行文件到内存当中,我们在上面说到编译时被添加的catlist在这里就用到了
    5. 最后调用remethodizeClass,真正分类的加载就在这一步,我们就从这一步开始,分析分类的加载流程
    /***********************************************************************
    * remethodizeClass
    * Attach outstanding categories to an existing class.
    * Fixes up cls's method list, protocol list, and property list.
    * Updates method caches for cls and its subclasses.
    * Locking: runtimeLock must be held by the caller
    **********************************************************************/
    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
    
        runtimeLock.assertWriting();
        
        /*
         这里我们按照被添加的方法是实例方法进行说明,即 isMeta = NO
         关于Meta是什么,在后面的章节中,会做讲解
         */
        isMeta = cls->isMetaClass();
    
        // Re-methodizing: check for more categories
        // 获取cls中未完成整合的所有分类 获取的是分类的列表
        if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
            if (PrintConnecting) {
                _objc_inform("CLASS: attaching categories to class '%s' %s", 
                             cls->nameForLogging(), isMeta ? "(meta)" : "");
            }
            // 获取到了 就将分类cats拼接到cls上
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    
    // 再来看看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, category_list *cats, bool flush_caches)
    {
        // 这里做了一些空值判断
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats); // Debug调试的无关内容
        // 依照上面的前提 isMeta = NO
        bool isMeta = cls->isMetaClass();
    
        
        /* 声明了一些变量。这些变量的类型都是二位数组,以method_list_t为例,其结构如下
            [[method_t, method_t, ...], [method_t], [method_t, method_t, method_t], ...]
         */
        
        // fixme rearrange to remove these intermediate allocations
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first
        // 通过cats向后计数以获得最新的类别
        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); // 获取实例方法列表(因为我们假定isMeta = NO)
            if (mlist) {// 如果方法列表存在
                mlists[mcount++] = mlist;   // 添加实例方法列表到二维数组的第mcount位
                fromBundle |= entry.hi->isBundle();
            }
            // 下面的属性 协议 也一样 顺次遍历 最终的添加结果就是上面我们说到的method_list_t的内部结构(二维数组)
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
        // 这里获取了宿主类当中的rw数据,这个rw包含了宿主类的信息(在runtime中会讲到这个rw)
        auto rw = cls->data();
    
        // 如果分类中有一些关于内存管理相关的方法的情况下 做一些特殊处理
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        
        /******
         这里就是关键方法了,将分类方法,将mcount个元素的二维数组mlists 拼接到宿主类的对于方法列表中。
         也就是说,在这行代码执行后,分类才被真正的添加到宿主类中。
         ******/
        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内部的具体实现
    /*
     addedLists 是传递过来的二位数组,我们假定以下3个分类A,B,C
     
     [[method_t, method_t, ...], [method_t], [method_t, method_t, method_t], ...]
     |____分类A中的方法列表(A)___|  |____B___|  |________________C_________________|
     
     addedCount = 3
     */
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        // 空值判断
        if (addedCount == 0) return;
    
        /*
         这里我们只分析第一个 if 中的判断,因为下面两个else中的判断是列表是采用array还是list_t这种结构而做的不同处理,
         对我们分析分类的实现逻辑不产生影响
         */
        
        if (hasArray()) {
            // many lists -> many lists
            // 获取列表中原有数据个数 如果原本有2个元素 需要被添加的有3个元素
            uint32_t oldCount = array()->count;
            // 拼接之后的元素总个数  2 + 3
            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]));
            /* 内存拷贝
             [
                addedLists中的第一个元素   ---->  [A],
                addedLists中的第二个元素   ---->  [B],
                addedLists中的第三个元素   ---->  [C],
                [原有的第一个元素],
                [原有的第二个元素],
             ]
             */
            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]));
        }
    }
    

    到这里,我们对分类的加载情况,就有一个大致的理解了。总结一下:

    • 分类添加的方法可以覆盖原类方法
    • 同名分类方法谁能生效取决于编译顺序

    同时,我们也可以解释下面几个问题:

    Q: 同一对象的不同分类拥有相同的方法,在程序执行时,会执行哪个?

    这个我们在上面的源码中可以看到,最后被加载的方法,会覆盖掉之前的方法。(这里要说明的是,这个覆盖,可不是替换,而且在方法查找的过程中,因为分类的方法被加到了最前面,因此,在方法选择器找到响应方法后,就直接返回了,不会再执行后面的方法。)

    Q: 分类为什么会覆盖掉父类的方法实现?

    我们在上面的题中也说到了,因为分类的方法列表被插入到了原方法之前,所以分类方法被优先找到了。换句话说,实际上父类的方法一直都存在,只是因为查找顺序的关系,一直不会被执行而已。


    到这里,分类的基本内容就讲述完了,再回过头想想,我们如何通过分类来添加成员变量呢?

    相关文章

      网友评论

          本文标题:OC基础-(一)Category

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