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

来源:互联网 发布:mac截图快捷键 保存 编辑:程序博客网 时间:2024/05/18 00:35

******阅读完此文,大概需要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、余康(美团点评)个人博客








原创粉丝点击