神奇的load方法

load方法說明

在Objective-C中,絕大多數類都繼承自NSObject這個根類,而該類有load方法,可以用來實現初始化操作。其原型如下:

+ (void)load

對於加入運行期系統中的每個類(class)及分類(category)來說,必定會調用此方法,而且僅調用一次。當包含類或分類的程序庫載入系統時,就會執行此方法(通常指應用程序啓動)。如程序是iOS平臺設計的,則肯定會在此時執行。Mac OS X應用程序更自由一些,它們可以使用“動態加載”(dynamic loading)之類的特性,等應用程序啓動好之後再去加載程序庫。如果分類和其所屬的類都定義了load方法,則先調用類裏的,再調用分類裏的。
執行load方法時,運行期系統處於“脆弱狀態”。在執行子類的load方法之前,必定會先執行所有超類的load方法,而如果代碼還依賴了其他程序庫,那麼程序庫裏相關類的load方法也必定會先執行。

load方法的妙用

簡化AppDelegate類

隨着項目功能的不斷增加,我們有很多功能或者第三庫需要啓動項目時就加載,AppDelegate類就會越來越龐大。這樣結構既不夠清晰,而且耦合性比較強。

改進前:

    //設置NUI配置
    [self setNUIConfig];

    //開啓統計
   [MobClick startWithConfigure:UMConfigInstance];

    //初始化數據庫
    [BYDBUtils startInitDB];

    //註冊統計平臺
    if (!TARGET_IPHONE_SIMULATOR)
    {
        [[SocialService sharedInstance] registerPlatforms];
    }

    //檢測服務器狀態
    [BYServerMgr sharedInstance]  doGetServerStatus];

    //獲取用戶數據
    [USER_MGR updateUserAssets];

    //啓動圖界面
    BYLaunchVC *splashVC = [[KSLaunchVC alloc] initWithNibName:@"BYLaunchVC" 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的代碼如下:

#import "BYThirdPartService.h"

@implementation BYThirdPartService

+ (void)load{

    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 = @"BYDefault";
    NSString *nuiStyleName = @"BYDefault.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的代碼(思路,具體代碼根據自身項目的實際情況進行修改)

#import "BYInitData.h"

@implementation BYInitData

+ (void)load{

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        //初始化數據庫
        [self initDB];

        //檢測網絡狀態
        [self GetServerStatus];

        //獲取用戶信息
        [self  GetUserinfo];
    });

}

//初始化數據庫
- (void)initDB{

    [[BYDBHelper sharedInstance] startInitOrUpdate];
}

- (void)GetServerStatus{
   //檢測網絡狀態
    ...........
}

- (void)GetServerStatus{
   //獲取用戶信息
    ...........
}

@end

簡化後AppDelegate如下:

#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)的一種實現。
替換兩個方法的實現,代碼如下:


#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);

    //class_addMethod的返回BOOL代表的是isNotExist,即當前類未實現該方法時才能添加成功
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

    if (didAddMethod ) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else {
        method_exchangeImplementations(originalMethod, swizzledMethod);

    }       
}
@end

BYStatistics統計類下文會用到。利用神奇的load方法統計兩個頁面的展示與離開次數


#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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章