在之前的博客《iOS 分类扩展与协议总结》中介绍了分类的用法以及性质,该博客将会从分类的源码对分类进一步分析。

首先我们先看下分类的数据结构:

struct category_t {
const char *name; //类的名字
classref_t cls; //类
struct method_list_t *instanceMethods; //catogies 中所有给类添加的实例方法列表
struct method_list_t *classMethods; //catogies 中所有给类添加的类方法列表
struct protocol_list_t *protocols; //catogies 中所有给类添加的协议列表
struct property_list_t *instanceProperties; //catogies 中所有给类添加的属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties; //catogies 中所有给类添加的类属性

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;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();

//向dyld注册了回调函数,当image map到内存中,当初始化完成image时和卸载image的时候都会回调注册者
_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) {
//......
// Discover categories.
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);
// ....
// Process this category.
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");

//.......
// +load handled by prepare_load_methods()
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();

// DO NOT use cat->cls! cls may be cat->cls->isa instead
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;
// fixme initial map size
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();
// Re-methodizing: check for more categories
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));

// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
//i 表示分类列表序号
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()) {
// many lists -> many lists
// 原有数组的元素个数
uint32_t oldCount = array()->count;
// 待添加数组元素个数
uint32_t newCount = oldCount + addedCount;
// 为新数组开辟对应的空间
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//原有的数据向后腾出addedCount大小的空间,给addedLists。
memmove(array()->lists + addedCount/*指向用于存储复制内容的目标数组的起始地址*/, array()->lists/*指向要复制的数据源*/, oldCount * sizeof(array()->lists[0]/*要被复制的字节数*/));
//将addedLists添加到原有数据的前面。
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
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的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找。

  • 在编译路径中位于后面的分类会先添加到列表,所以最后编译的分类最终生效。

Contents