iOS-+load和+initialize方法調用時機


Objective-C 有兩個神奇的方法:+load 和 +initialize,這兩個方法在類被使用時會自動調用。但是兩個方法的不同點會導致應用層面上性能的顯著差異。

一、+ initialize 方法和+load 調用時機

先來看一個表

方法 +(void)load +(void)initialize
執行時機 在程序運行后立即執行 在類的方法第一次被調時執行
若自身未定義,是否沿用父類的方法?
(這是由於+load方法是根據方法地址直接調用,
並不是經過objc_msgSend函數調用
。下面有解釋)
分類中的定義 全都執行,但后於類中的方法 覆蓋類中的方法, 只執行一個
執行次數(非主動調用的情況下) 必然一次 0、1、多 次(調用者會不同)

先看官方解釋

  1. 首先說一下 + initialize 方法:蘋果官方對這個方法有這樣的一段描述:這個方法會在 第一次初始化這個類之前 被調用,我們用它來初始化靜態變量。
  2. load 方法會在加載類的時候就被調用,也就是 ios 應用啟動的時候,就會加載所有的類,就會調用每個類的 + load 方法。
  3. 之后我們結合代碼來探究一下 + initialize 與 + load 兩個方法的調用時機,首先是 + load
#pragram ---main函數中的代碼---
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
    NSLog(@"%s",__func__);
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
#pragram ---基於NSObject的Person類---
#import "Person.h"
@implementation Person
 + (void)load{
    NSLog(@"%s",__func__);
}
 + (void)initialize{
    [super initialize];
    NSLog(@"%s %@",__func__,[self class]);
}
 - (instancetype)init{
    if (self = [super init]) {
        NSLog(@"%s",__func__);
    }
    return self;
}
@end
#pragram ---基於Person的Son類---
#import "Girl.h"
@implementation Girl
 + (void)load{
    NSLog(@"%s ",__func__);
}
 + (void)initialize{
    [super initialize];
    NSLog(@"%s ",__func__);
}
 - (instancetype)init{
    if (self = [super init]) {
        NSLog(@"%s",__func__);
    }
    return self;
}
@end

 

運行程序,我們看一下輸出日志:

2015-10-27 15:21:07.545 initialize[11637:334237] +[Person load] 2015-10-27 15:21:07.546 initialize[11637:334237] +[Girl load] 2015-10-27 15:21:07.546 initialize[11637:334237] main 

這說明在我並沒有對類做任何操作的情況下,+load 方法會被默認執行,並且是在 main 函數之前執行的

 

二、接下來我們來查看一下 + initialize 方法,先在 ViewController 中創建 Person 和 Girl 對象:

#import "ViewController.h"
#import "Person.h"
#import "Son.h"
#import "Girl.h"
@interface ViewController ()
@end
@implementation ViewController
 - (void)viewDidLoad {
    [super viewDidLoad];
    Person * a = [Person new];
    Person * b = [Person new];
    Girl *c = [Girl new];
    Girl *d = [Girl new];
}
@end

 

下面我們來看一下輸出日志:

2015-10-27 15:33:56.195 initialize[11711:342410] +[Person load] 2015-10-27 15:33:56.196 initialize[11711:342410] +[Girl load] 2015-10-27 15:33:56.197 initialize[11711:342410] main 2015-10-27 15:33:56.259 initialize[11711:342410] +[Person initialize] Person 2015-10-27 15:33:56.259 initialize[11711:342410] -[Person init] 2015-10-27 15:33:56.259 initialize[11711:342410] -[Person init] 2015-10-27 15:33:56.259 initialize[11711:342410] +[Girl initialize] 2015-10-27 15:33:56.260 initialize[11711:342410] -[Girl init] 2015-10-27 15:33:56.260 initialize[11711:342410] -[Girl init] 

通過這個實驗我們可以確定兩點:
- + initialize 方法類似一個懶加載,如果沒有使用這個類,那么系統默認不會去調用這個方法,且默認只加載一次;
- + initialize 的調用發生在 +init 方法之前。

 

三、接下來再探究一下 + initialize 在父類與子類之間的關系,創建一個繼承自 Person 類的 Son類:

#pragram ---ViewController 中的代碼---
#import "ViewController.h"
#import "Person.h"
#import "Son.h"
#import "Girl.h"
@interface ViewController ()
@end
@implementation ViewController
 - (void)viewDidLoad {
    [super viewDidLoad];
    Person * a = [Person new];
    Person * b = [Person new];
    Son*z = [Son new];
}
@end

 

看一下輸出日志:

2015-10-27 15:44:55.762 initialize[12024:351576] +[Person load] 2015-10-27 15:44:55.764 initialize[12024:351576] +[Son load] 2015-10-27 15:44:55.764 initialize[12024:351576] +[Girl load] 2015-10-27 15:44:55.764 initialize[12024:351576] main 2015-10-27 15:44:55.825 initialize[12024:351576] +[Person initialize] Person 2015-10-27 15:44:55.825 initialize[12024:351576] -[Person init] 2015-10-27 15:44:55.825 initialize[12024:351576] -[Person init] 2015-10-27 15:44:55.826 initialize[12024:351576] +[Person initialize] Son 2015-10-27 15:44:55.826 initialize[12024:351576] -[Person init] 

我們會發現 Person 類的 + initialize 方法又被調用了,但是查看一下是子類 Son 調用的,也就是創建子類的時候,子類會去調用父類的 + initialize 方法。

四、總結

  • 如果你實現了 + load 方法,那么當類被加載時它會自動被調用。這個調用非常早。
    如果你實現了一個應用或框架的 + load,並且你的應用鏈接到這個框架上了,那么 + load 會在 main() 函數之前被調用。
    如果你在一個可加載的 bundle 中實現了 + load,那么它會在 bundle 加載的過程中被調用。比如方法交換等
  • + initialize 方法的調用看起來會更合理,通常在它里面寫代碼比在 + load 里寫更好。
    + initialize 很有趣,因為它是懶調用的,也有可能完全不被調用。類第一次被加載時,

 

五、其他內部實現原理補充

一、+load 調用時機和順序原理解析

load 方法在什么時候調用?
官方解釋是:運行時,添加類或者分類的時候調用。實現此方法以在加載時執行特定於類的行為。

+load方法是一定會在runtime中被調用的,只要類被添加到runtime中了,就會調用+load方法,即只要是在Compile Sources中出現的文件總是會被裝載,與這個類是否被用到無關,因此+load方法總是在main函數之前調用。所以我們可以自己實現+laod方法來在這個時候執行一些行為。

換句話說,load是在app啟動的時候加載並在main函數之前調用,從源碼中找

_objc_init —>
_dyld_objc_notify_register(&map_images, load_images, unmap_image);

 

這里面的2個方法 map_images 和 load_images, map_images的作用就是加載所有的類/協議/分類,加載完成之后,就開始調用load_images,在這個方法里面看:

load_images(const char *path __unused, const struct mach_header *mh)
{
    ......
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);    // 把所有需要load的類 加載一個list里面
    }
    call_load_methods();    // 調用load方法
}


void call_load_methods(void)
{
   ......
    do {
        while (loadable_classes_used > 0) {
            call_class_loads();                     // 先加載類的load 
        }
        more_categories = call_category_loads();    // 在加載category的load

    } while (loadable_classes_used > 0  ||  more_categories);   
   ......
}

另外有意思的一點是:

+load方法不會覆蓋(因為由於+load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用)。
也就是說,如果子類實現了+load方法,那么會先調用父類的+load方法,然后又去執行子類的+load方法。
但要注意的時+load方法只會調用一次。而且執行順序是:類 ->父類-> 子類 ->分類。而不同類之間的順序不一定。

但是這里依然有一個疑問,官方解釋沒有說清楚,1、分類的加載順序是怎樣的?
其實在源碼中有可以看到:

 
void prepare_load_methods(const headerType *mhdr)
{
......
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);    // 1、按照編譯順序加載所有的類(不包括分類)
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));  // 2、在這里  按照先父類 在子類的方式加入列表
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);  // 分類也是類似的方式
    for (i = 0; i < count; i++) {
       ......
        add_category_to_loadable_list(cat);
    }
}


static void schedule_class_load(Class cls)
{
  .......
    schedule_class_load(cls->superclass);    //遞歸加載,先加載父類

    add_class_to_loadable_list(cls);
}

 

由以上代碼中 1、2備注,得出 :

1、先加載類的load:類的加載是按照編譯順序,同時遵循先父類再子類的方式
2、再加載分類的load:分類直接按照編譯順序,和其綁定類的繼承沒有關系

總結

1、先加載類的load
2、再加載分類的load
3、不同的類之間加載load順序為:有繼承關系的,先加載父類load、再加載子類的load,無繼承關系的,按照編譯順序
----比如順序二 Student、OtherClass、Person,先加載Student的load,由於Person是Student的父類,所以Person的順序比OtherClass早
4、分類的加載順序是完全按照編譯順序,也就是誰在前面,誰先加載。和其綁定類的繼承關系無關
----比如順序二中,Student繼承Person,但是其分類的順序是 Student+JE2、Student+JE1、Person+JE,順序是什么樣,加載load就是什么樣。
5、即使有類的源文件,但是編譯列表中沒有,那么這個類就不會被編譯,也就不會執行其load方法

 

2、+initialize

initialize方法在什么時候調用?
官方解釋是:在類收到第一條消息之前初始化它。
換句話說,就是第一次用到它之前調用,比如初始化一個對象(其父類也會調用initialize)、調用類方法等。 從源碼中找:

  • 說明:沒有發送消息的類不會調用initialize
  • 如果主類有相應的分類(或多個分類),會調用分類中的initialize方法,具體調用的是哪個分類的方法,由編譯順序決定。
  • 當子類沒有重寫initialize方法,這個時候回去執行父類的initialize方法
class_initialize -> initializeNonMetaClass()
void initializeNonMetaClass(Class cls)
{
    .......
    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);        // 遞歸加載父類的initialize
    }
    ........
}

 

+initialize方法是在類或它的子類收到第一條消息之前被調用的,這里所指的消息包括實例方法和類方法的調用,並且只會調用一次。
+initialize方法實際上是一種惰性調用,也就是說如果一個類一直沒被用到,那它的+initialize方法也不會被調用,這一點有利於節約資源。

+load方法不同,卻更符合我們預期的就是,+initialize方法會覆蓋是因為調用的objc_msgSend。
也就是說如果子類實現了+initialize方法,就不會執行父類的了,直接執行子類本身的。
如果分類實現了+initialize方法,也不會再執行主類的。
所以+initialize方法的執行覆蓋順序是:分類 -> 子類 ->類。且只會有一個+initialize方法被執行。

總結

1、initialize的執行順序為先父類、在子類
2、分類的initialize方法會覆蓋主類的方法(假覆蓋,方法都在,只是沒有執行)
3、只有在這個類有發送消息的時候才會執行initialize,比如初始化對象、調用類方法等。
4、多個分類的情況,只執行一次,具體執行哪個分類的initialize,有編譯順序決定(Build Phases -> Compile Sources 中的順序)
5、如果子類沒有重寫initialize,那么會調用其父類的initialize方法

 

3、兩者的異同

1、相同點

1).load和initialize會被自動調用,不能手動調用它們。
2).子類實現了load和initialize的話,會隱式調用父類的load和initialize方法。
3).load和initialize方法內部使用了鎖,因此它們是線程安全的。

2、不同點

1).調用順序不同,

  1. 以main函數為分界,+load方法在main函數之前執行,
  2. +initialize在main函數之后執行。

2).若自身未定義,是否沿用父類的方法:

  1. 子類中沒有實現+load方法的話,不會調用父類的+load方法;(不會)
  2. 而子類如果沒有實現+initialize方法的話,也會自動調用父類的+initialize方法。(會)

3).調用時機:

  1. +load方法是在類被裝載進來的時候就會調用,
  2. +initialize在第一次給某個類發送消息時調用(比如實例化一個對象),並且只會調用一次,是懶加載模式,如果這個類一直沒有使用,就不回調用到+initialize方法。

4).調用方式:

  1. load是根據函數地址直接調用
  2. initialize是通過objc_msgSend調用

 

4、使用場景

1.+load一般是用來交換方法Method Swizzle,由於它是線程安全的,而且一定會調用且只會調用一次,通常在使用UrlRouter的時候注冊類的時候也在+load方法中注冊。
2.+initialize方法主要用來對一些不方便在編譯期初始化的對象進行賦值,或者說對一些靜態常量進行初始化操作。

 

5、為什么原本類的+load方法不會被分類的+load方法覆蓋呢?

這是由於+load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用

+load方法相關源碼閱讀順序:

  1. _objc_init
  2. load_images
  3. prepare_load_methods
    • schedule_class_load
    • add_class_to_loadable_list
    • add_category_to_loadable_list
  4. calls
    • call_class_loads
    • call_category_loads
    • (*load_method)(cls, SEL_load)

 

3、+initialize方法調用的時機

+initialize方法會在類第一次接收到消息時調用

調用順序:先調用父類的+initialize,再調用子類的+initialize,每個類只會初始化1次

+initialize方法相關源碼閱讀順序:

  1. objc-msg-arm64.s

    • objc_msgSend
  2. objc-runtime-new.mm

    • class_getInstanceMethod
    • lookUpImpOrNil
    • lookUpImpOrForward
    • _class_initialize
    • callInitialize
    • objc_msgSend(cls, SEL_initialize)

+initialize和+load的最大區別是,+initialize是通過objc_msgSend進行調用的,所以有以下特點:

  • 如果子類沒有實現+initialize,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)
  • 如果分類實現了+initialize,就覆蓋類本身的+initialize調用

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM