+load & +initialize

有时,我们需要在应用启动阶段,或者类初始化之前处理一些指定的需求,这时你可能需要这俩方法:

1
2
+ (void)load;
+ (void)initialize;

1.load

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

1.1.触发时机

  1. dyld 加载镜像文件;
  2. 初始化runtime;
  3. 注册OC类与分类;
  4. +load

+load

所以,+load是在应用启动阶段,即加载应用可执行文件时、进入程序主入口前触发的。

1.2.调用顺序

父类->子类->分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//父类
@implementation Initializer
+ (void)load{
NSLog(@"++++ super loaded~");
}
@end

//子类1
@interface SubInitializer : Initializer
@end
@implementation SubInitializer
+(void)load{
NSLog(@"+++ SubInitializer loaded~");
}
@end

//子类2
@interface SubInitializer2 : Initializer
@end

@implementation SubInitializer2
+(void)load{
NSLog(@"++++ SubInitializer2 loaded~");
}
@end

//分类
@interface Initializer (Category)
@end
@implementation Initializer (Category)
+(void)load{
NSLog(@"++++ category loaded~");
}
@end

输出日志:

1
2
3
4
++++ super loaded~
++++ SubInitializer2 loaded~
+++ SubInitializer loaded~
++++ category loaded~

结论:

  • 父类和子类都重写此方法时,都会触发,且子类晚于父类触发;
  • 原类和分类都重写此方法时,都会触发,且分类晚于原类触发;
  • 父类子类和它们的分类都重写此方法时,都会触发,顺序为:父类>子类>子类分类>父类分类;
  • 不在同一继承树上的类之间的触发顺序不确定;

1.3.示例

检测添加到数组中的对象是否为 nil:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import <objc/runtime.h>
@implementation NSMutableArray (Extension)

+(void)load{
[self swizzle];
}

+ (void)swizzle
{
Method method1 = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(addObject:));
Method method2 = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(s_ddObject:));
//交换方法
method_exchangeImplementations(method1, method2);
}

-(void)s_ddObject:(id)anObject
{
if (nil == anObject){
@try {
[self s_ddObject:anObject];
} @catch (NSException *exception) {
NSLog(@"Crash reason:\n %@", [exception callStackSymbols]);
} @finally {}
}
else{
[self s_ddObject:anObject];
}
}
@end

交换方法之后,调用addObject:实际上会去调用s_ddObject:;而s_ddObject:内调用s_ddObject:实际上是调用数组的addObject:方法;所以s_ddObject实际上起到了中间层的作用,用来做异常的检测和其他任务。

随后通过+load函数在程序启动时执行该替换过程。下面是调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import "NSMutableArray+Extension.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSMutableArray *mutArr = [NSMutableArray array];
[mutArr addObject:nil];//添加nil也不会崩了

return YES;
}
@end

这样再往数组中添加 nil 对象时,就不再会崩溃,并且一旦发现 nil 对象就会将信息打印出来。

需要注意,不宜在+load内处理复杂任务,这会减慢应用的启动速度。优化应用启动时间时,也可从这里入手。

2.initialize

The runtime sends initialize to each class in a program just before the class, or any class that inherits from it, is sent its first message from within the program. Superclasses receive this message before their subclasses.

2.1.触发时机

在类或其子类收到第一个消息之前调用;如果类一直没被调用,则+initialize一直不会触发;

+load

2.2.调用顺序

父类->子类;分类覆盖原类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import "Initializer.h"

@implementation Initializer

+(void)initialize
{
NSLog(@"+++ super Initialized~");
}
@end

//子类
@interface SubInitializer : Initializer
@end

@implementation SubInitializer
+(void)initialize{
NSLog(@"+++ SubInitializer Initialized~");
}
@end

//分类
@interface Initializer (Category)
@end

@implementation Initializer (Category)
+(void)initialize{
NSLog(@"++++ category Initialized~");
}
@end

调用及日志:

1
2
3
4
5
//调用
SubInitializer *subInitial = [[SubInitializer alloc] init];
//日志
++++ category Initialized~
+++ SubInitializer Initialized~
  • 原类和分类都重写此方法时,只有分类中的会触发,因为查询方法列表时先查到的是分类中的 initialize;
  • 父类重写而子类未重写此方法时,对于每个子类,第一次向其发送消息时,都会触发父类中的 initialize,所以父类的 initialize 可能会触发多次;
  • 父类和子类都重写此方法,首次向父类发送消息,则只会触发父类中的方法;首次向子类发送消息,则两个都会触发且子类晚于父类;先后向子和类父类发送消息,则调用到父类时只触发父类的 initialize;调用到子类时只触发子类的 initialize(因为父类中的 initialize 已经触发过了)。

注意:父类中的+initialize可能被多次调用,若在父类的+initialize中单独处理某些需求,要做好判断:

1
2
3
4
5
6
7
8
9
10
@implementation Initializer

+(void)initialize
{
NSLog(@"++++class \"%@\" initialized~",[self class]);
if (self == [Initializer self]) {
// ... do the initialization ...
}
}
@end

另外,+initialize是线程安全的,也就是说当 runtime 在第一个线程中调用此方法后,其他线程中需要向此类发送消息时都会被阻塞,直到+initialize执行完毕。所以,此方法内不适合用来处理复杂的任务,可用来初始化全局变量。

2.3.示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//Initializer.h
#import <UIKit/UIKit.h>

UIKIT_EXTERN int aGlobalInt;
UIKIT_EXTERN NSString *className;
UIKIT_EXTERN NSMutableDictionary *paramDic;

@interface Initializer : NSObject
@end

//Initializer.m
#import "Initializer.h"

int aGlobalInt = 0;
NSString *className = @"defaultName";
NSMutableDictionary *paramDic = nil;

@implementation Initializer

+(void)initialize
{
paramDic = [NSMutableDictionary dictionary];

if (self == [Initializer self]) {
aGlobalInt = 1;
className = @"Initializer";
}else{
aGlobalInt = 2;
className = NSStringFromClass([self class]);
}
paramDic[@"name"] = className;
}
@end

调用及日志:

1
2
3
4
5
6
7
//调用
Initializer *initial = [[Initializer alloc] init];
NSLog(@"++++Int:%d, String:%@, Dic:%@",aGlobalInt,className,paramDic);
//日志
++++Int:1, String:Initializer, Dic:{
name = Initializer;
}

3.比较

从触发的时间序列上来看:

  • +load 发生在二进制文件加载阶段,在 UIApplicationMain 这个程序主入口之前;
  • +initialize 发生在程序完全启动之后,也就是在 +load 之后。

从各类中方法的加载顺序来看:

  • load 发生在加载镜像期间,各类的 load 方法加载顺序不确定,所以不建议在一个类中调用另一个类;
  • initialize 在程序启动之后,类均已在运行时环境中创建和加载,所以可以正常调用。

从触发次数上来看:

  • load 在启动阶段会全部触发,且每个load只会触发一次;
  • initialize 只在调用当前类时才会触发,不调用则永远不触发;父类中的initialize可能会触发多次;

调用顺序上:

  • 两个方法在父类、子类和分类等情况下的调用顺序有所不同,注意区分~

相关参考:

#©AppleDev-load

#©AppleDev-initialize


+load & +initialize
https://davidlii.cn/2017/12/25/load.html
作者
Davidli
发布于
2017年12月25日
许可协议