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
下面是一些大致的信息:
看注释也能知道这是把分类里面方法等相关信息添加到原始类里面。
从 memmove 和 memcpy 也可知主要是把分类的方法添加到原始类的方法列表的的前面(当然也可能原始类没有实现方法,但是分类实现了)。
(上面的分析对应的是 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
结果是符合之前的代码分析的。在测试的时候也可以自己移动一下类和分类的编译顺序,发现也是符合结论的。
一个问题:如果分类的方法和原始类一样,那么它会覆盖原始类方法吗?
由于在方法列表中,分类的方法会添加在原始类方法的前面。
所以如果通过 runtime 的 objc_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 消息走的是 runtime 的 objc_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 方法的使用说明其实也是描述的很详细的:
还有就是不要在这个方法里面做太复杂的操作,也不要把在里面出现死锁的情况。