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

本文深入探讨了Objective-C中Category的加载与执行顺序,并解释了Category与动态库(dylib)结合时可能出现的问题及原因。Category作为Objective-C的重要特性之一,在实际开发中有着广泛的应用。

******阅读完此文,大概需要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]));
}

<span style="color:#000000"><span style="color:#666666"><span style="color:#2f2f2f">这段代码很简单,其实就是先调用 </span><code>realloc()</code><span style="color:#2f2f2f"> 函数将原来的空间拓展,然后把原来的数组复制到后面,最后再把新数组复制到前面。</span></span></span>

查看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中。

三、参考文档

1、ios-category解析 - 簡書

2、【iOS】Category VS Extension 原理详解 - CocoaChina_一站式开发者成长社区

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值