******阅读完此文,大概需要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中。
三、参考文档
2、【iOS】Category VS Extension 原理详解 - CocoaChina_一站式开发者成长社区
3、余康(美团点评)个人博客
本文深入探讨了Objective-C中Category的加载与执行顺序,并解释了Category与动态库(dylib)结合时可能出现的问题及原因。Category作为Objective-C的重要特性之一,在实际开发中有着广泛的应用。
860

被折叠的 条评论
为什么被折叠?



