iOS开发笔记之六十七——Category使用过程中的一些注意事项

******阅读完此文,大概需要10分钟******

一、不同Category中同名方法的加载与执行顺序

1、先来看看如下的例子,针对TestClass类有两个Category分别为TestClass+A、TestClass+B,类结构如下:


而打印结果始终如下:


2、Category的方法执行原理

先看下Category数据结构

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;
};
可见一个 category 持有了一个  method_list_t  类型的数组, method_list_t  又继承自  entsize_list_tt ,这是一种泛型容器:

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    // 成员变量和方法
};
  
 template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
};

这里的 entsize_list_tt 可以理解为一个容器,拥有自己的迭代器用于遍历所有元素。 Element 表示元素类型,List 用于指定容器类型,最后一个参数为标记位。

虽然这段代码实现比较复杂,但仍可了解到 method_list_t 是一个存储 method_t 类型元素的容器。method_t 结构体的定义如下:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};
最后,我们还有一个结构体  category_list  用来存储所有的 category,它的定义如下:

struct locstamped_category_list_t {
    uint32_t count;
    locstamped_category_t list[0];
};
struct locstamped_category_t {
    category_t *cat;
    struct header_info *hi;
};
typedef locstamped_category_list_t category_list;
除了标记存储的 category 的数量外, locstamped_category_list_t  结构体还声明了一个长度为零的数组,这其实是 C99 中的一种写法,允许我们在运行期动态的申请内存。

查看Category扩展方法如何被objc/runtime保存:

在OC运行时,入口方法如下(在objc-os.mm文件中),category被附加到类上面是在map_images的时候发生的,而map_images最终会调用objc-runtime-new.mm里面的_read_images方法。

void _objc_init(void)
└──const char *map_2_images(...)
    └──const char *map_images_nolock(...)
        └──void _read_images(header_info **hList, uint32_t hCount)

而真正起作用的是attachCategoryMethods方法:【详细方法调用参考源码】,下面来看看

attachCategoryMethods代码:

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
    if (!cats) return;
    bool isMeta = cls->isMetaClass();
 
    method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int i = cats->count;
    while (i--) {
        auto& entry = cats->list[i];
 
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
        }
    }
 
    auto rw = cls->data();
 
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
}

首先,通过 while 循环,我们遍历所有的 category,也就是参数 cats 中的 list 属性。对于每一个 category,得到它的方法列表 mlist 并存入 mlists 中。

换句话说,我们将所有 category 中的方法拼接到了一个大的二维数组中,数组的每一个元素都是装有一个 category 所有方法的容器。这句话比较绕,但你可以把 mlists 理解为旧版本的 objc_method_list **methodLists

扩展方法是被覆盖?被追加?被差人list头位置?关键看attachLists源码:

 void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    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]));
}

这段代码很简单,其实就是先调用 realloc() 函数将原来的空间拓展,然后把原来的数组复制到后面,最后再把新数组复制到前面。

查看Category扩展方法如何被objc/runtime读取:

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){
    for (auto mlists = cls->data()->methods.beginLists(),
              end = cls->data()->methods.endLists();
         mlists != end;
         ++mlists) {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
 
    return nil;
}
 
static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
    for (auto& meth : *mlist) {
        if (meth.name == sel) return &meth;
    }
}
可见搜索的过程是按照从前向后的顺序进行的,一旦找到了就会停止循环。因此 category 中定义的同名方法不会替换类中原有的方法,但是对原方法的调用实际上会调用 category 中的方法。

至此,对于1中的始终输出“TestClass B...”就知道原因了。

二、Category与动态库dylib结合的注意事项

如果我们把TestClass+B类添加到动态库中,将会发生什么?无论我们怎么执行,都会是如下结果:


为什么会是这样子的结果呢?因为因为ClassesDomainObject方法是被编译时Add到MachO可执行文件中,动态库并没有Add进来,所以执行程序时总是调用

TestClass+A中。


三、参考文档

1http://www.jianshu.com/p/e917e7d95f69

2http://www.cocoachina.com/ios/20170502/19163.html

3、余康(美团点评)个人博客








猜你喜欢

转载自blog.csdn.net/lizitao/article/details/77196620
今日推荐