******阅读完此文,大概需要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中。
三、参考文档
1、http://www.jianshu.com/p/e917e7d95f69
2、http://www.cocoachina.com/ios/20170502/19163.html
3、余康(美团点评)个人博客