OC基础-(一)Category

2018-10-27  本文已影响0人  码小六

什么是Category(分类)?

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

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

先来说说分类的特性:

关于运行时决议这点,就不得不说说分类和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: 分类为什么会覆盖掉父类的方法实现?

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


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

上一篇下一篇

猜你喜欢

热点阅读