神奇的load方法

load方法说明

在Objective-C中,绝大多数类都继承自NSObject这个根类,而该类有load方法,可以用来实现初始化操作。其原型如下:

1
+ (void)load

对于加入运行期系统中的每个类(class)及分类(category)来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法(通常指应用程序启动)。如程序是iOS平台设计的,则肯定会在此时执行。Mac OS X应用程序更自由一些,它们可以使用“动态加载”(dynamic loading)之类的特性,等应用程序启动好之后再去加载程序库。如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。
执行load方法时,运行期系统处于“脆弱状态”。在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。

load方法的妙用

简化AppDelegate类

随着项目功能的不断增加,我们有很多功能或者第三库需要启动项目时就加载,AppDelegate类就会越来越庞大。这样结构既不够清晰,而且耦合性比较强。

改进前:

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
35
36
37
38
39
40
41
42
//设置NUI配置
[self setNUIConfig];
//开启统计
[[KSStatisticalMgr sharedInstance] start];
//初始化数据库
[KSDBUtils startInitDB];
//注册统计平台
if (!TARGET_IPHONE_SIMULATOR)
{
[[SocialService sharedInstance] registerPlatforms];
}
//检测服务器状态
[[KSServerMgr sharedInstance] doGetServerStatus];
//获取用户数据
[USER_MGR updateUserAssets];
//启动图界面
KSLaunchVC *splashVC = [[KSLaunchVC alloc] initWithNibName:@"KSLaunchVC" bundle:nil];
UIWindow *keywindow = [UIApplication sharedApplication].keyWindow;
[keywindow addSubview:splashVC.view];
[keywindow bringSubviewToFront:splashVC.view];
[self.window makeKeyAndVisible];
//自适应屏幕键盘控件
IQKeyboardManager * manager = [IQKeyboardManager sharedManager];
manager.enable = YES;
manager.shouldResignOnTouchOutside = YES;
manager.shouldToolbarUsesTextFieldTintColor = YES;
manager.enableAutoToolbar = YES;
//设置首页
BYCircleListViewController *homePageVC = [[BYCircleListViewController alloc] init];
BYNavigationViewController *navVC = [[BYNavigationViewController alloc] initWithRootViewController:homePageVC];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = navVC;
[self.window makeKeyAndVisible];

改进后

目录结构如下:
改进后AppDelegate目录

初始化第三方库BYThirdPartService.m的代码如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#import "BYThirdPartService.h"
@implementation BYThirdPartService
+ (void)load{
static dispatch_once_t onceToken;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//设置NUI配置
[self setNUIConfig];
//开启统计
[self startStatistics];
//键盘初始化
[self initKeyboard];
});
}
//设置NUI配置
- (void)setNUIConfig{
//判断屏幕尺寸
CGFloat scale = [UIScreen mainScreen].scale;
int scaleInt = (int)scale;
NSString *nuiStyleStartName = @"KSDefault";
NSString *nuiStyleName = @"KSDefault.NUI";
[NUISettings initWithStylesheet:nuiStyleName];
if([NUISettings hasProperty:@"translucent" withClass:@"NavigationBar"])
{
[[UINavigationBar appearance] setTranslucent:[NUISettings getBoolean:@"translucent" withClass:@"NavigationBar"]];
}
if ([NUISettings hasProperty:@"tint-color" withClass:@"NavigationBar"]) {
[[UINavigationBar appearance] setTintColor:[NUISettings getColor:@"tint-color" withClass:@"NavigationBar"]];
}
}
//键盘初始化
- (void)initKeyboard{
IQKeyboardManager * manager = [IQKeyboardManager sharedManager];
manager.enable = YES;
manager.shouldResignOnTouchOutside = YES;
manager.shouldToolbarUsesTextFieldTintColor = YES;
manager.enableAutoToolbar = YES;
}
//开始统计
- (void)startStatistics{
[MobClick startWithConfigure:UMConfigInstance];
}

初始化数据 BYInitData.m的代码(思路,具体代码根据自身项目的实际情况进行修改)

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
35
36
37
38
39
40
#import "BYInitData.h"
@implementation BYInitData
+ (void)load{
static dispatch_once_t onceToken;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//初始化数据库
[self initDB];
//检测网络状态
[self GetServerStatus];
//获取用户信息
[self GetUserinfo];
});
}
//初始化数据库
- (void)initDB{
[[KSDBHelper sharedInstance] startInitOrUpdate];
}
- (void)GetServerStatus{
//检测网络状态
...........
}
- (void)GetServerStatus{
//获取用户信息
...........
}
@end

简化后AppDelegate如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "AppDelegate.h"
#import "BYCircleListViewController.h"
#import "BYNavigationViewController.h"
//只需增加相应的两个头文件
#import "BYThirdPartService.h"
#import "BYInitData.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
BYCircleListViewController *homePageVC = [[BYCircleListViewController alloc] init];
BYNavigationViewController *navVC = [[BYNavigationViewController alloc] initWithRootViewController:homePageVC];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = navVC;
[self.window makeKeyAndVisible];
return YES;
}

当类被引入项目时, runtime 会向每一个类对象发送 load 消息. 神奇的load 方法, 会在每一个类甚至分类被引入时仅调用一次, 调用的顺序是父类优先于子类, 子类优先于分类. 而且 load 方法不会被类自动继承, 每一个类中的 load 方法都不需要像 viewDidLoad 方法一样调用父类的方法。

埋点统计

在iOS中,在运行时替换两个方法的实现,达到“勾住”某个方法并注入代码的目的。具体方法如下:

重载类的“+(void)load”方法,在程序加载到内存时利用Runtime的method_exchangeImplementations等接口将方法的实现互相交换。当方法M被调用时就会被勾住(Hook),执行我们的方法。

该技术称为Method Swizzling,属于面向切面编程(Aspect-Oriented Programming)的一种实现。
替换两个方法的实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "BYStatistics.h"
#import <objc/runtime.h>
@implementation BYStatistics
+ (void)swizzlingClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{
Class class = cls;
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL isExistMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (isExistMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end

BYStatistics统计类下文会用到。利用神奇的load方法统计两个页面的展示与离开次数

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#import "UIViewController+Stastistics.h"
#import "BYStatistics.h"
@implementation UIViewController (Stastistics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(swizzling_viewWillAppear:);
[BYStatistics swizzlingClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];
SEL originalSelector2 = @selector(viewWillDisappear:);
SEL swizzledSelector2 = @selector(swizzling_viewWillDisappear:);
[BYStatistics swizzlingClass:[self class] originalSelector:originalSelector2 swizzledSelector:swizzledSelector2];
});
}
#pragma mark - Method Swizzling
- (void)swizzling_viewWillAppear:(BOOL)animated{
//插入需要执行的代码
[self inject_viewWillAppear];
[self swizzling_viewWillAppear:animated];
}
//利用hook,统计页面的停留时间
- (void)inject_viewWillAppear{
NSString *pageName = [self pageEventName:YES];
if (pageName) {
//统计代码
}
}
- (void)swizzling_viewWillDisappear:(BOOL)animated{
[self inject_viewWillDisappear];
[self swizzling_viewWillDisappear:animated];
}
- (void)inject_viewWillDisappear
{
NSString *pageName = [self pageEventName:YES];
if (pageName) {
//统计代码
}
}
@end

load方法与initialize方法

NSObject的load方法和initialize方法都是用来实现初始化操作。

load方法
对于加入运行期系统中的每个类及分类来说,必定会调用此方法,而且近调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若程序是为iOS平台设计的,则肯定会在此时执行。
如果分类和其所属的类都定义了load方法,则先调用类里的,在调用分类里的。在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。
在整个应用程序执行load方法时都会阻塞

initialize方法
它是“惰性”调用的,也就是说,只有当程序用到了相关的类时,才会调用。因此,如果某个类一直都没有使用,那么其initialize方法就一直不会运行。这也就等于说,应用程序无须先把每个类的initialize都执行一遍

注意事项

  • 与其他方法不同,load方法不参与覆写机制
  • load方法实现得精简一些,有助于保持应用程序的响应能力,也能

如有写的不对地方,请在评论区指出,谢谢!
请指明出处:

https://jingwanli6666.github.io/2016/11/08/%E7%A5%9E%E5%A5%87%E7%9A%84load%E6%96%B9%E6%B3%95/

感谢!我会继续努力,谢谢!