新闻中心

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

  • 时间:
  • 浏览:220
  • 来源:怪兽分发

写在前面

这个系列的文章仅仅对 Category 的实现在源码层面做一定的分析,大家还是要多上手尝试,你会发现很多有意思的问题,例如:
如果你的工程和你使用的第三方库,产生了方法重名,在你开启了 LTO 优化的前后,你会发现分别调用到了第三方库和你的主工程方法中,这又是为什么呢?允许我在开头就挖个坑,这是个很有意思的问题,等我有空会写一篇文章。

这里有几点要说明的:

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

前情

Category相信大家都有使用过,利用分类可以对一些系统类进行扩展,分模块等等。我们先来构造个分类看一下:

// 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.cpp

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

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

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

不难看出也正好对应这 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--)的方式,因此后加载的分类会排在前面。

写在后面

这篇文章中没有把所有的源码拿出来,大家应当对着源码亲自上手看一看。文中如有错误,欢迎指出。

作者:leeluanxin

链接:https://juejin.im/post/6875127886513176589

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。