Category 相关的方法加载顺序

2,213 阅读8分钟

1 底层数据结构

添加一些初始代码

(下面的代码是在命令行项目中测试的)

Cat

@interface Cat : NSObject

- (void)run;

@end

@implementation Cat

- (void)run {
    NSLog(@"Cat run");
}

@end

Cat 的分类:Cat (White)Cat (Black)

// -------------------- Cat (White) --------------------

@interface Cat (White)

- (void)runWhite;

@end

@implementation Cat (White)

- (void)runWhite {
    NSLog(@"Cat (White) run");
}

@end

// -------------------- Cat (Black) --------------------

@interface Cat (Black)

- (void)runBlack;

@end

@implementation Cat (Black)

- (void)runBlack {
    NSLog(@"Cat (Black) run");
}

@end

我们知道,如果一个方法是对象方法,那么它会添加到类里面,如果是类方法,那么它会添加到元类中,上面 Cat 类,有两个分类 Cat (White)Cat (Black)。每个分类里面都定义了一个测试的对象方法,那么它们是否会被添加到 Cat 类里面呢?

添加如下测试代码,查看 Cat 类里面的方法:

#import <Foundation/Foundation.h>
#import "Cat.h"
#import <objc/runtime.h>

// 主要是使用runtime输出
void printMethodsNameForClass(Class cls) {
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableString *methodsName = [NSMutableString string];
    for (int i = 0; i < count; i ++) {
        Method item = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(item));
        [methodsName appendFormat:@"\n%@", methodName];
    }
    free(methodList);
    NSLog(@"class = %@, methods = %@", cls, methodsName);
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 输出 Cat 类的对象方法
        printMethodsNameForClass([Cat class]);
    }
    return 0;
}

输出如下:

2021-01-04 17:00:44.901796+0800 Categoy[78852:3578720] class = Cat, methods = 
runBlack
runWhite
run

嗯,分类添加的方法的确被添加到原来类的方法列表里面了。

分类的数据结构

执行下面命令,把分类 Cat+White 的实现代码转成 C++ 的形式查看一下里面的实际数据结构

clang -rewrite-objc Cat+White.m -o Cat+White.cpp

可以发现分类的数据结构是这样的:

它是一个名字为 _category_t 的结构体。里面是这样的:

struct _category_t {
	const char *name;                                // 类的名字
	struct _class_t *cls;                            // 指向的类
	const struct _method_list_t *instance_methods;   // 实例对象方法列表
	const struct _method_list_t *class_methods;      // 类方法列表
	const struct _protocol_list_t *protocols;        // 协议列表
	const struct _prop_list_t *properties;           // 属性列表
};

所以在开发中我们是能在分类里面添加 实例对象方法, 类方法, 协议, 属性 相关信息的。

2 分类里面的方法是如何添加到原始类的

从上面的内容我们知道,分类里的内容编译完以后的数据结构是一个 _category_t 结构体。里面存储了一些方法相关等信息,那么它里面的方法是如何加载到原始类里面的呢?

其实通过查看 objc 源码 objc 源码下载地址 ,我们会发现是:使用 runtime 在加载类和分类的时候把分类里面的方法合并到原始类里面的。

查看一下 objc 源代码

(我这里使用的源代码版本是 objc4-781

首先 objc-os.mm 可以看成是 runtime 的入口文件,然后 void _objc_init(void) 是入口方法。

大致流程:

objc-os.mm 文件中:_objc_init -> _dyld_objc_notify_register(&map_images, load_images, unmap_image); -> map_images_nolock

然后在 objc-runtime-new.mm 文件中: _read_images -> load_categories_nolock -> attachCategories -> attachLists

下面是一些大致的信息:

看注释也能知道这是把分类里面方法等相关信息添加到原始类里面。

memmovememcpy 也可知主要是把分类的方法添加到原始类的方法列表的的前面(当然也可能原始类没有实现方法,但是分类实现了)。

(上面的分析对应的是 objc4-781 版本的代码,具体的情况需要看自己下载的版本号)

3 方法的加载顺序

通过上面分类的方法加载顺序可以知道:在方法列表中,分类的方法会在原始类的方法的前面;同一个类的不同分类情况下,后编译的分类方法在前编译的方法前面

验证一下

添加如下方法实现:

Cat

@interface Cat : NSObject

- (void)sleep;

@end

@implementation Cat

- (void)sleep {
    NSLog(@"Cat sleep");
}

@end

Cat 的分类:Cat (White)Cat (Black)

// -------------------- Cat (White) --------------------

@implementation Cat (White)

- (void)sleep {
    NSLog(@"Cat (White) sleep");
}

@end

// -------------------- Cat (Black) --------------------

@implementation Cat (Black)

- (void)sleep {
    NSLog(@"Cat (Black) sleep");
}

@end

在类和分类中添加了 3 个同样的 sleep 方法的实现。这是编译顺序:

添加测试输出:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[Cat alloc] sleep];
    }
    return 0;
}

输出:

2021-01-04 19:26:47.257958+0800 Categoy[84106:3646487] Cat (White) sleep

结果是符合之前的代码分析的。在测试的时候也可以自己移动一下类和分类的编译顺序,发现也是符合结论的。

一个问题:如果分类的方法和原始类一样,那么它会覆盖原始类方法吗?

由于在方法列表中,分类的方法会添加在原始类方法的前面。

所以如果通过 runtimeobjc_msgSend 方法进行消息发送,那么会先通过自己的 isa 指针找方法的实现,也就是说如果分类和原始类的方法一样,那么它会调用分类的实现的方法,但是原始类的方法还是在方法列表中的,它不是真正的方法覆盖。

4 +load 方法的加载顺序

在开发中 load 一般是用来添加一些方法交换的地方。主要是由于它有: 在 runtime 加载所有类和分类的时候, 里面的 +load 方法会保证被调用一次

添加如下测试代码:

Cat

@implementation Cat

+ (void)load {
    NSLog(@"Cat +load");
}

@end

Cat 的分类:Cat (White)Cat (Black)

// -------------------- Cat (White) --------------------

@implementation Cat (White)

+ (void)load {
    NSLog(@"Cat (White) +load");
}

@end

// -------------------- Cat (Black) --------------------

@implementation Cat (Black)

+ (void)load {
    NSLog(@"Cat (Black) +load");
}

@end

编译运行:

// 什么测试代码都没调用
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

文件的编译顺序:

输出如下:

2021-01-04 20:41:06.419960+0800 Categoy[32191:2890455] Cat +load
2021-01-04 20:41:06.420267+0800 Categoy[32191:2890455] Cat (Black) +load
2021-01-04 20:41:06.420295+0800 Categoy[32191:2890455] Cat (White) +load

看看 objc 源码大致是如何使用 runtime 加载 +load 方法

大致流程:

找到 runtime 入口: objc-os.mm 文件的 _objc_init

然后:

objc-runtime-new.mm 文件里面大致流程: load_images -> prepare_load_methods -> schedule_class_load -> add_class_to_loadable_list -> call_load_methods(在 load_images 函数里) -> call_class_loads

一些关键调用源代码:

先找出所有的类和分类的 +load 方法,再调用。

保证调用所有分类的 +load 方法前,先调用所有原始类的 +load 方法:

在原始类中,保证如果有父类 +load 方法,先调用父类的:

直接找到 +load 方法的地址进行调用,不使用 runtime 的 objc_msgSend :

总结

  • +load 方法会在 runtime 加载所有分类的时候保证都只调用一次;
  • +load 方法比分类 的先调用;
  • 所有 +load 方法会按照 先编译先调用 原则(如果当前类有父类 +load,会先调用父类的);
  • 分类+load 方法会先编译先调用

验证一下

测试编译顺序:

继承关系:

Cat: Animal

Dog: Animal

输出:

2021-01-04 23:07:12.699538+0800 Categoy[37844:2996662] Animal +load
2021-01-04 23:07:12.699869+0800 Categoy[37844:2996662] Cat +load
2021-01-04 23:07:12.699902+0800 Categoy[37844:2996662] Dog +load
2021-01-04 23:07:12.699922+0800 Categoy[37844:2996662] Cat (White) +load
2021-01-04 23:07:12.699938+0800 Categoy[37844:2996662] Dog (White) +load
2021-01-04 23:07:12.699954+0800 Categoy[37844:2996662] Dog (Black) +load
2021-01-04 23:07:12.699970+0800 Categoy[37844:2996662] Cat (Black) +load

可以看到输出的确和总结是符合的(在测试的时候可以把编译顺序随便调整验证来加深印象)。

5 +initialize 方法的加载顺序

添加测试代码:


// ------------------- Animal -------------------

@implementation Animal

+ (void)initialize {
    NSLog(@"Animal +initialize");
}

// 等一下来测试的
+ (void)run {
    NSLog(@"Animal +run");
}

@end

// ------------------- Cat -------------------

@implementation Cat

+ (void)initialize {
    NSLog(@"Cat +initialize");
}

@end

@implementation Cat (White)

+ (void)initialize {
    NSLog(@"Cat (White) +initialize");
}

@end

@implementation Cat (Black)

+ (void)initialize {
    NSLog(@"Cat (Black) +initialize");
}

@end

// ------------------- Dog -------------------

@implementation Dog

+ (void)initialize {
    NSLog(@"Dog +initialize");
}

@end

继承关系:

Cat: Animal

Dog: Animal

测试编译运行:

// 不添加测试代码
int main(int argc, const char * argv[]) {
   @autoreleasepool {
   }
   return 0;
}

输出:

Program ended with exit code: 0

嗯,控制台没有输出 +initialize 方法的相关信息。

这主要是因为 +initialize 方法会在类发送第一条消息前才会调用。

测试编译运行:

// 添加发送消息
int main(int argc, const char * argv[]) {
   @autoreleasepool {
       [Animal run];
   }
   return 0;
}

输出:

2021-01-04 23:25:42.070657+0800 Categoy[38603:3012352] Animal +initialize
2021-01-04 23:25:42.070932+0800 Categoy[38603:3012352] Animal +run

发现的确是 Animal 发送 run 消息前调用了 +initialize 方法。

打个断点:

打开汇编语言调试,可以看到一个这样的东西

表明 Animal 发送 run 消息走的是 runtimeobjc_msgSend 这个流程的,所以这就和方法的查找有关了。

然后可以猜测:

Animal 调用 +initialize 方法应该是和消息查找有关(主要是 objc_msgSend 这个函数查看的结果是一个汇编实现而不是C++的,所以在方法查找里面大概会有线索)。

看看 objc 源代码的大致流程

objc-runtime-new.mm文件中: class_getInstanceMethod -> lookUpImpOrForward -> class_initialize -> initializeAndMaybeRelock ->

objc-initialize.mm文件中: initializeNonMetaClass -> callInitialize

一些源代码调用:

首先要知道类其实也是对象,下面是 objc 源代码的类方法的实现(cls->getMeta() 传入元类):

/***********************************************************************
* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

开始方法的查找:

保证如果有父类,先调用父类的 +initialize 方法:

最终的方法调用:

总结

  • 每个类的 +initialize 方法只会在发送第一条消息前调用一次;
  • 父类的 +initialize 方法在子类的前面调用;

由于 +initialize 方法的调用使用的是 objc_msgSend 来进行消息发送,

所以有:

  • 如果子类没有实现 +initialize 方法,那么子类会使用自己的 superclass 指针到父类里面查找实现,这就导致父类的 +initialize 方法可能会调用多次。(当然,如果子类实现了,就调用自己的);
  • 如果分类也实现了 +initialize 方法,那么进行方法查找的时候会调用分类的 +initialize 方法,而不会调用原始类的。(在方法列表中,分类方法在原始类的前面)

+initialize 方法和 +load 方法区别:

  • +load 方法是在 runtime 加载类和分类的时候调用的,所有的类和分类如果有实现 +load 方法,那么都会调用;
  • +load 方法的调用没有使用 objc_msgSend,而是直接进行方法地址的调用;
  • +initialize 方法是在类发送第一个消息前使用 objc_msgSend 调用的,如果类没有使用,那么就不会调用。

其它

看苹果的 +initialize 方法的使用说明其实也是描述的很详细的:

还有就是不要在这个方法里面做太复杂的操作,也不要把在里面出现死锁的情况。