OC中有两个特殊的类方法,分别是load
和initialize
。
先来看看NSObject Class Reference里对这两个方法说明:
The
load
message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.// 只要类或类别实现了load方法, 不论这个类或类别是动态加载还是静态链接, 类/类别都会受到load消息。
The order of initialization is as follows:
- All initializers in any framework you link to. // 调用所有的Framework中的初始化方法
- All
+load
methods in your image. // 调用所有的+load方法- All C++ static initializers and C/C++
__attribute__(constructor)
functions in your image. // 调用C++的静态初始化方及C/C++中的attribute(constructor)函数- All initializers in frameworks that link to you. // 调用所有链接到目标文件的framework中的初始化方法
In addition:
- A class’s
+load
method is called after all of its superclasses’+load
methods.- A category
+load
method is called after the class’s own+load
method.- 先父类load-> 在子类load->最后category中load(父类的category优先级也低于子类)
In a custom implementation of
load
you can therefore safely message other unrelated classes from the same image, but anyload
methods implemented by those classes may not have run yet. // 在自定义的实现+load方法中,可以安全地向同一二进制包中的其它无关的类发送消息,但接收消息的类中的+load方法可能尚未被调用。
The runtime sends
initialize
to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses. //在这个类接受第一个消息之前,
runtime人会给每一个类
发送initialize消息,
先父类在子类,initialize会
保证线程安全
Apple的文档很清楚地说明了initialize和load的区别在于:load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用进来,但是没有使用,那么initialize也不会被调用。
load
顾名思义,load
方法在这个文件被程序装载时调用。只要是在Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,load
方法总是在main
函数之前调用。
调用规则
如果一个类实现了load
方法,在调用这个方法前会首先调用父类的load
方法。而且这个过程是自动完成的,并不需要我们手动实现,由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。:
// In Parent.m
+ (void)load {
NSLog(@"Load Class Parent");
}
// In Parent+load.m
+ (void)load {
NSLog(@"Load Class Parent+load");
}
// In Child.m,继承自Parent
+ (void)load {
NSLog(@"Load Class Child");
}
// In Child+load.m,Child类的分类
+ (void)load {
NSLog(@"Load Class Child+load");
}
// 运行结果: Parent+load和Child+load的顺序和文件编译顺序有关
/*
2016-02-01 21:28:14.379 load[11789:1435378] Load Class Parent
2016-02-01 21:28:14.380 load[11789:1435378] Load Class Child
2016-02-01 21:28:14.380 load[11789:1435378] Load Class Parent+load
2016-02-01 22:28:14.381 load[11789:1435378] Load Class Child+load
*/
执行顺序
load
方法调用时,系统处于脆弱状态,如果调用别的类的方法,且该方法依赖于那个类的load
方法进行初始化设置,那么必须确保那个类的load
方法已经调用了,比如demo中的这段代码,打印出的字符串就为null
:
// In Child.m
+ (void)load {
NSLog(@"Load Class Child");
Other *other = [Other new];
[other originalFunc];
// 如果不先调用other的load,下面这行代码就无效,打印出null
[Other printName];
}
load
方法的调用顺序其实有迹可循,在Compile Sources中,文件的排放顺序就是其装载顺序,自然也就是load
方法调用的顺序。这一点也证明了load
方法中会自动调用父类的方法,因为在demo的输出结果中,Parent
的load
方法先于Child
调用,而它的装载顺序其实在Child
之后。
虽然在这种简单情况下我们可以辨别出各个类的load
方法调用的顺序,但永远不要依赖这个顺序完成你的代码逻辑。一方面,这在后期的开发中极容易导致错误,另一方面,你实际上并不需要这么做。
使用场景
由于调用load
方法时的环境很不安全,我们应该尽量减少load
方法的逻辑。另一个原因是load
方法是线程安全的,它内部使用了锁,所以我们应该避免线程阻塞在load
方法中。
一个常见的使用场景是在load
方法中实现Method Swizzle:
// In Other.m
+ (void)load {
Method originalFunc = class_getInstanceMethod([self class], @selector(originalFunc));
Method swizzledFunc = class_getInstanceMethod([self class], @selector(swizzledFunc));
method_exchangeImplementations(originalFunc, swizzledFunc);
}
在Child
类的load
方法中,由于还没调用Other
的load
方法,所以输出结果是"Original Output",而在main函数中,输出结果自然就变成了"Swizzled Output"。
一般来说,除了Method Swizzle,别的逻辑都不应该放在load
方法中实现。
runtime中的源码
load调用方式
+load是通过函数指针指向函数,拿到函数地址,分开来直接调用的,直接通过内存地址查找调用的。
initialize
这个方法在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次。initialize
方法实际上是一种惰性调用,也就是说如果一个类一直没被用到,那它的initialize
方法也不会被调用,这一点有利于节约资源。
调用规则
与load
方法类似的是,在initialize
方法内部也会调用父类的方法,而且不需要我们显示的写出来。与load
方法不同之处在于,即使子类没有实现initialize
方法,也会调用父类的方法,这会导致一个很严重的问题:
// In Parent.m
+ (void)initialize {
NSLog(@"Initialize Parent, caller Class %@", [self class]);
}
// In Child.m
// 注释掉initialize方法
// In main.m
Child *child = [Child new];
运行后发现父类的initialize
方法竟然调用了两次:
2016-02-01 22:57:02.985 load[12772:1509345] Initialize Parent, caller Class Parent
2016-02-01 22:57:02.985 load[12772:1509345] Initialize Parent, caller Class Child
这是因为在创建子类对象时,首先会调用一次父类的initialize
方法,然后创建子类时,尽管child自己没有实现initialize
方法,但是通过obj_msgsend调用,会找到父类的方法,然后又调用了一次。
虽然initialize
方法对一个类而言只会调用一次,但这里由于出现了两个类,所以调用两次符合规则,但不符合我们的需求。正确使用initialize
方法的姿势如下(官方文档上这么说的):
// In Parent.m
+ (void)initialize {
if (self == [Parent class]) {
NSLog(@"Initialize Parent, caller Class %@", [self class]);
}
}
加上判断后,就不会因为子类而调用到自己的initialize
方法了。
最后结论(参考链接 https://www.jianshu.com/p/c52d0b6ee5e9) :
- 1.父类的initialize方法会比子类先执行
- 2.当子类未实现initialize方法时,会调用父类initialize方法,子类实现initialize方法时,会覆盖父类initialize方法.
- 3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)
使用场景
initialize
方法主要用来对一些不方便在编译期初始化的对象进行赋值。比如NSMutableArray
这种类型的实例化依赖于runtime的消息发送,所以显然无法在编译器初始化:
// In Parent.m
static int someNumber = 0; // int类型可以在编译期赋值
static NSMutableArray *someObjects;
+ (void)initialize {
if (self == [Parent class]) {
// 不方便编译期复制的对象在这里赋值
someObjects = [[NSMutableArray alloc] init];
}
}
initialize调用方式
在类第一次接收到消息时调用,消息转发机制调用的
void _class_initialize(Class cls)
{
... ...
// 递归调用父类的_class_initialize方法
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
// 将要调用SEL_initialize
if (PrintInitializing) {
_objc_inform("INITIALIZE: calling +[%s initialize]",
cls->nameForLogging());
}
// 通过消息机制调用SEL_initialize
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
// 调用完成
if (PrintInitializing) {
_objc_inform("INITIALIZE: finished +[%s initialize]",
cls->nameForLogging());
}
总结
load
和initialize
方法都会在实例化对象之前调用,在runtime初始化完成后调用,后者在收到第一条消息前调用。这两个方法会被自动调用,不能手动调用它们 , 不要写 [super load] / [superinitialize
]。load
和initialize
方法都不用显示的调用父类的方法而是自动调用,即使子类没有initialize
方法也会调用父类的方法,而load
方法则不会调用父类。load
方法通常用来进行Method Swizzle,initialize
方法一般用于初始化全局变量或静态变量。load
和initialize
方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。-
+load调用方式:+load是通过函数指针指向函数,拿到函数地址,分开来直接调用的,直接通过内存地址查找调用的。
+initialize:在类第一次接收到消息时调用,消息转发机制调用的(objc_msgSend).
+initialize方法调用顺序, 所以类别中的initialize会覆盖主类的initialize.
如果子类没有实现+initialize方法,会调用父类的+initialize(所以父类的+initialize方法可能会被调用多次)
原文链接:http://www.jianshu.com/p/d25f691f0b07
+(void)load | +(void)initialize | |
执行时机 | 在程序运行后立即执行,runtime初始化中,main函数前 | 在类的方法第一次被调时执行 |
若自身未定义,是否沿用父类的方法? | 否 | 是 |
类别中的定义 | 全都执行,但后于类中的方法 | 覆盖类中的方法,只执行一个 |