Category 详解(一)—— 源码层面解析分类冲突问题

写在前面

这个系列的文章仅仅对 Category 的实如今源码层面作必定的分析,你们仍是要多上手尝试,你会发现不少有意思的问题,例如:
若是你的工程和你使用的第三方库,产生了方法重名,在你开启了 LTO 优化的先后,你会发现分别调用到了第三方库和你的主工程方法中,这又是为何呢?容许我在开头就挖个坑,这是个颇有意思的问题,等我有空会写一篇文章。c++

这里有几点要说明的:数组

  1. 苹果开源的代码对于咱们的意义仅仅只指导,不可所有相信
  2. rewrite 是 Xcode 工具链中的一项,实际上编译后产生的 c++ 代码不等于真正执行的代码,若是想要知道真正运行的代码,应当采起汇编调试的方式。

前情

Category相信你们都有使用过,利用分类能够对一些系统类进行扩展,分模块等等。咱们先来构造个分类看一下:markdown

// Person类
- (void)run {
    NSLog(@"run");
}
复制代码
// Person+Test 分类
- (void)test {
    NSLog(@"test");
}
 
+ (void)test2 {
    NSLog(@"test2");
}
复制代码

这样咱们就能够在引入头文件后调用对应的方法。函数

咱们都知道,调用方法的时候是经过objc_msgSend(self, SEL)来实现的,那么会产生问题:工具

person的类对象中有没有存储分类中的实例方法?Person+Test会不会有本身的分类对象,将分类中的实例方法存放在分类对象中呢?
先说结论:不论有没有分类,每一个类都只有一个类对象,分类中的方法也是存储在Person类的类对象中的。
优化

结构体解析

首先咱们 cd 到 Person+Eat 的目录下,而后执行 clang rewrite-objc Person+Test.m,这样Person+Test.m就被转化为了c++的代码 Person+Test.cppui

这个文件比较长,咱们直接看 _category_t 这个结构体,这个结构体是每个分类的结构spa


而后再找到这个结构体初始化的地方:调试


经过这里咱们就能看出,Person+Test的初始化时,类名叫作 Person,有实例方法和类方法,其余都是空,那么让咱们来看看实例方法和类方法初始化的地方:code



不难看出也正好对应这 Person+Test 分类中的两个方法,- (void)test 和 + (void)test2

方法合并

实际上这些方法、属性、协议,都是在运行时经过runtime进行合并的,要了解这个合并的过程,就须要阅读runtime的源码。下面是本次阅读runtime源码的一些过程:

  • 下载 runtime 源码,老的版本须要经过 cmake 来生成 Xcode 项目,而比较新的版本则是已经帮你作好了,此次下载的版本是objc4-781。
  • 找到 objc-os.mm 这个文件,这个文件是 runtime 的入口文件。
  • 在 objc-os.mm 中找到 void _objc_init(void)这个方法,这个方法是运行时的初始化方法。
  • 在 _objc_init 方法最后,调用了_dyld_objc_notify_register 函数,这个函数里有个参数是 map_images,咱们点进 map_images 看一下。
  • 注意map_images 调了一个函数 map_images_nolock(count, pahts, mhdrs),咱们点进去。
  • 这个函数比较长,咱们到最下面找到 _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses),点进去。
  • 在中间偏下的部分,咱们能够看到这么一段代码,调用了 load_categories_nolock(hi),点进去。

  • 在中间位置找到了核心函数,核心函数是 attachCategories(cls, &lc, 1, ATTACH_EXISTING),点进去,看一下方法注释。

这个方法中,将全部的category中的方法、属性、协议,整合到二维数组method_list_t mlists中,而后调用 attachLists 方法进行attach,咱们来看一下这个方法里的代码

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;
    // 将array()原有的list拷贝到新list的尾部
    memmove(array()->lists + addedCount, array()->lists,
            oldCount * sizeof(array()->lists[0]));
    // 将新的方法list拷贝到list的头部
    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]));
    }
}
复制代码

总结:

这里咱们看到,最终合并的方法列表的存储结构是顺序表,而查找时也是顺序查找的。

分类方法会覆盖原类方法的缘由是由于原类方法被放在了list最后(memmove操做)。

后加载的分类方法会覆盖前面的分类方法是由于,在 map_images_nolock 函数中,采用的是while(i--)的方式,所以后加载的分类会排在前面。

写在后面

这篇文章中没有把全部的源码拿出来,你们应当对着源码亲自上手看一看。 文中若有错误,欢迎指出。

相关文章
相关标签/搜索