在之前的博客《iOS 分类扩展与协议总结》中介绍了分类的用法以及性质,该博客将会从分类的源码对分类进一步分析。
首先我们先看下分类的数据结构:
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; struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; }
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); };
|
从上述对象中可以看出通过category可以添加实例方法,类方法,协议,属性,但是不能为对象添加实例变量。因为category是在运行时添加到类中的,这时候类的内存布局已经确定,如果添加实例变量就会破坏类的内部布局。
接下来我们就从runtime最开始介绍分类是如何添加到类中的。
void _objc_init(void) { static bool initialized = false; if (initialized) return; initialized = true; environ_init(); tls_init(); static_init(); lock_init(); exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image); }
|
在_objc_init方法中会向dyld注册三个十分关键的回调函数:map_images,load_images,unmap_image
在镜像被加载后会回调map_images,分类的添加就是在这个阶段完成的,这以后会调用map_images_nolock方法来处理被dyld映射进来的镜像。map_images_nolock方法很长,但是和我们今天代码相关的只有
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
|
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) { for (EACH_HEADER) { category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) { category_t *cat = catlist[i]; Class cls = remapClass(cat->cls); bool classExists = NO; if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized()) { remethodizeClass(cls); classExists = YES; } } if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { addUnattachedCategoryForClass(cat, cls->ISA(), hi); if (cls->ISA()->isRealized()) { remethodizeClass(cls->ISA()); } } } }
ts.log("IMAGE TIMES: discover categories");
if (DebugNonFragileIvars) { realizeAllClasses(); } }
|
_read_images 会从镜像中读取很多数据,这里只关注分类的部分。
首先通过****_getObjc2CategoryList****从_DATA_部分的__objc_catlist获取到当前镜像中全部的分类。__objc_catlist这个数据是哪里来的呢?其实是编译时候生成的。
那么编译阶段,编译器做了哪些事情呢?
- 编译器生成了实例方法列表
- 编译器生成了category本身
- 编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组
objc_catlist section就是在加载镜像的时候被加载进来的。
我们继续,然后针对每个分类调用addUnattachedCategoryForClass
static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader) { runtimeLock.assertLocked();
NXMapTable *cats = unattachedCategories(); category_list *list; list = (category_list *)NXMapGet(cats, cls); if (!list) { list = (category_list *) calloc(sizeof(*list) + sizeof(list->list[0]), 1); } else { list = (category_list *) realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1)); } list->list[list->count++] = (locstamped_category_t){cat, catHeader}; NXMapInsert(cats, cls, list); }
|
在分析addUnattachedCategoryForClass之前我们先看下unattachedCategories方法:
static NXMapTable *unattachedCategories(void) { runtimeLock.assertLocked(); static NXMapTable *category_map = nil; if (category_map) return category_map; category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16); return category_map; }
|
注意category_map是静态的,首次的时候会通过NXCreateMapTable(NXPtrValueMapPrototype, 16)创建一个MapTable,后续会将同一个category_map返回。category_map存放的是什么呢?它存放的是 class => categories 映射关系,这些分类是未添加到对应类上的所以叫做unattachedCategories。我们再回到addUnattachedCategoryForClass:
在调用unattachedCategories获取到全部类以及它的分类映射map之后,通过NXMapGet(cats, cls) 获取到的是cats map中以cls类作为key的,为添加到cls的分类列表,然后开辟一个list->count + 1 再加sizeof(*list)大小的空间,用于存放cat以及catHeader。然后再将这个列表添加到category_map中。所以经过addUnattachedCategoryForClass处理后。我们获得到的数据将会以如下的形式组织起来:
接下来我们来看下分类的添加过程:
static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertLocked(); isMeta = cls->isMetaClass(); if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { attachCategories(cls, cats, true /*flush caches*/); free(cats); } }
|
remethodizeClass方法中先通过unattachedCategoriesForClass获取到当前类的未添加的分类cats:
static category_list * unattachedCategoriesForClass(Class cls, bool realizing) { runtimeLock.assertLocked(); return (category_list *)NXMapRemove(unattachedCategories(), cls); }
|
注意这里返回分类列表后会调用NXMapRemove从category_map中移除。
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)); property_list_t **proplists = (property_list_t **)malloc(cats->count * sizeof(*proplists)); protocol_list_t **protolists = (protocol_list_t **)malloc(cats->count * sizeof(*protolists));
int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) { auto& entry = cats->list[i]; method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); } property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { proplists[propcount++] = proplist; } protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; } }
//将catories中的方法属性以及协议添加到rw中 auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle); rw->methods.attachLists(mlists, mcount); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount); free(proplists);
rw->protocols.attachLists(protolists, protocount); free(protolists); }
|
分类的添加的逻辑位于attachCategories方法中。这里要特别注意理解数据结构的组织,为了帮助大家理解,我这里画了一张图:
mlists,proplists,protolists如上图所示,从横向看,mlists,proplists,protolists是当前类所有分类的方法列表,属性列表以及协议列表。从纵向看,分别是某个分类的方法列表,协议列表,以及属性列表。这里需要十分注意的是上面的遍历是逆序遍历,所以最后加载的分类最先添加到类中,也就是说后添加的分类的方法,协议,以及属性会覆盖先添加的同名的方法,协议,以及属性,这就导致查找某个方法的时候会逆着分类加载链寻找,一旦找到立刻停止。
紧接着将当前类的所有方法列表,属性列表和分类列表追加到类的可写区域中。这里我们再重点看下attachLists方法。
void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { 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])); } else if (!list && addedCount == 1) { list = addedLists[0]; } else { List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } }
|
attachLists其实是将该类所有的分类对应的方法列表,属性列表,协议列表,添加到类的可读写区域中,attachLists方法会重新开辟一个内存空间,将原有的数据向后腾出空间,给分类对应的元素。这意味着什么?意味着分类的方法,协议,以及属性,会被添加到类原有的方法,协议,以及属性之前。
通过上面的源码解析不难理解下面的一些细节点:
category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA。只不过category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找。
在编译路径中位于后面的分类会先添加到列表,所以最后编译的分类最终生效。