iOS類方法load和initialize詳解

朱曉輝Allen發表於2018-01-02

iOS類方法load和initialize詳解

iOS開發中總能看到+load和+initialize的身影,網上對於這兩個方法有很多解釋,官方也有說明,但有些細節不夠清楚,今天我們來詳細扒一扒這兩個方法.

load

Apple文件是這樣描述的

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

當類(Class)或者類別(Category)加入Runtime中時(就是被引用的時候)。
實現該方法,可以在載入時做一些類特有的操作。

Discussion

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.

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.
一個類的+load方法在其父類的+load方法後呼叫

A category +load method is called after the class’s own +load method.
一個Category的+load方法在被其擴充套件的類的自有+load方法後呼叫

In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.
在+load方法中,可以安全地向同一二進位制包中的其它無關的類傳送訊息,但接收訊息的類中的+load方法可能尚未被呼叫。

複製程式碼

文件地址:https://developer.apple.com/reference/objectivec/nsobject/1418815-load?language=objc

load函式呼叫特點如下:

當類被引用進專案的時候就會執行load函式(在main函式開始執行之前),與這個類是否被用到無關,每個類的load函式只會自動呼叫一次.由於load函式是系統自動載入的,因此不需要呼叫父類的load函式,否則父類的load函式會多次執行。

  • 1.當父類和子類都實現load函式時,父類的load方法執行順序要優先於子類
  • 2.當子類未實現load方法時,不會呼叫父類load方法
  • 3.類中的load方法執行順序要優先於類別(Category)
  • 4.當有多個類別(Category)都實現了load方法,這幾個load方法都會執行,但執行順序不確定(其執行順序與類別在Compile Sources中出現的順序一致)
  • 5.當然當有多個不同的類的時候,每個類load 執行順序與其在Compile Sources出現的順序一致

下面通過例項來一起驗證下:

我們新建2個類:Person繼承NSObject,Student和Teacher均繼承Person


Person : NSObject
Student : Person
Teacher : Person
複製程式碼

新建3個Person分類:

Person (Category)
Person (Category2)
Person (Category3)
複製程式碼

在Person,Student,Person (Category),Person (Category2),Person (Category3)的.m檔案中實現下面方法(Teacher除外)

//In Person.m
+(void)load
{
    NSLog(@"%s",__FUNCTION__);
}
//In Student.m(繼承自Person)
+(void)load
{
    NSLog(@"%s",__FUNCTION__);
}

//In Teacher.m(繼承Person)
不實現load方法

//In Person+Category.m
+(void)load
{
    NSLog(@"%s",__FUNCTION__);
}
//In Person+Category2.m
+(void)load
{
    NSLog(@"%s",__FUNCTION__);
}
//In Person+Category3.m
+(void)load
{
    NSLog(@"%s",__FUNCTION__);
}

複製程式碼

執行結果:


2017-05-04 11:11:40.612 LoadAndInitializeExample[27745:856244] +[Person load]
2017-05-04 11:11:40.615 LoadAndInitializeExample[27745:856244] +[Student load]
2017-05-04 11:11:40.616 LoadAndInitializeExample[27745:856244] +[Person(Category3) load]
2017-05-04 11:11:40.617 LoadAndInitializeExample[27745:856244] +[Person(Category) load]
2017-05-04 11:11:40.617 LoadAndInitializeExample[27745:856244] +[Person(Category2) load]

複製程式碼

可以看到執行順序依次為:

  • 1.首先執行的是父類Person load方法,再執行的是子類Student load方法,說明父類的load方法執行順序要優先於子類
  • 2.子類Teacher中沒有實現load方法,沒有列印,說明子類沒有實現load方法時並不會呼叫父類load方法
  • 3.最後執行的是Person 3個Category的load方法,並且沒有順序,說明類別(Category)中的load方法要晚於類,多個類別(Category)都實現了load方法,這幾個load方法都會執行,但執行順序不確定
  • 4.同時我們也可以看到這幾個Category load 執行順序與其在Compile Sources中出現的順序一致
  • 5.當然多個不同的類 其load執行順序,也與其在Compile Sources出現的順序一致

Compile Sources.png

initialize:

Apple文件是這樣描述的

Initializes the class before it receives its first message.

在這個類接收第一條訊息之前呼叫。

Discussion

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在一個程式中每一個類的一個程式中傳送一個初始化一次,或是從它繼承的任何類中,都是在程式中傳送第一條訊息。(因此,當該類不使用時,該方法可能永遠不會被呼叫。)執行時傳送一個執行緒安全的方式初始化訊息。父類的呼叫一定在子類之前。

複製程式碼

文件地址:https://developer.apple.com/reference/objectivec/nsobject/1418639-initialize?language=objc

initialize函式呼叫特點如下:

initialize在類或者其子類的第一個方法被呼叫前呼叫。即使類檔案被引用進專案,但是沒有使用,initialize不會被呼叫。由於是系統自動呼叫,也不需要再呼叫 [super initialize] ,否則父類的initialize會被多次執行。假如這個類放到程式碼中,而這段程式碼並沒有被執行,這個函式是不會被執行的。

  • 1.父類的initialize方法會比子類先執行
  • 2.當子類未實現initialize方法時,會呼叫父類initialize方法,子類實現initialize方法時,會覆蓋父類initialize方法.
  • 3.當有多個Category都實現了initialize方法,會覆蓋類中的方法,只執行一個(會執行Compile Sources 列表中最後一個Category 的initialize方法)

我們同樣通過例項來一起驗證下:

我們新增Person類,在.m中實現+ initialize方法

//In Person.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

複製程式碼

執行結果:

無列印...
複製程式碼

啥也沒列印

說明:只是把類檔案被引用進專案,沒有使用的話,initialize不會被呼叫

我們在Teacher(繼承Person).m中不實現initialize方法


//In Person.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

//In Teacher.m(繼承自Person)

Teacher.m中不實現initialize方法

複製程式碼

我們呼叫下Teacher的new方法,執行


[Teacher new];

複製程式碼

執行結果:

2017-05-04 14:06:45.051 LoadAndInitializeExample[29238:912579] +[Person initialize]
2017-05-04 14:06:45.051 LoadAndInitializeExample[29238:912579] +[Person initialize]
複製程式碼

執行後發現父類的initialize方法竟然呼叫了兩次:

可見當子類未實現initialize方法,會呼叫父類initialize方法.

但為什麼會列印2次呢?

我的理解: 子類不實現initialize方法,會把繼承父類的initialize方法並呼叫一遍。在此之前,父類初始化時,會先呼叫一遍自己initialize方法.所以出現2遍,所以為了防止父類initialize中程式碼多次執行,我們應該這樣寫:

//In Person.m
+(void)initialize
{
	if(self == [Person class])
	{
          NSLog(@"%s",__FUNCTION__);
	}
}

複製程式碼

下面我們在 Teacher.m中實現initialize方法

//In Person.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

//In Teacher.m(繼承自Person)
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

複製程式碼

同樣呼叫Teacher 的new方法,執行


[Teacher new];

複製程式碼

執行結果:

2017-05-04 14:07:45.051 LoadAndInitializeExample[29238:912579] +[Person initialize]
2017-05-04 14:07:45.051 LoadAndInitializeExample[29238:912579] +[Teacher initialize]
複製程式碼

可以看到 當子類實現initialize方法後,會覆蓋父類initialize方法,這一點和繼承思想一樣

我們在Person.m和Person+Category.m中實現initialize方法

//In Person.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

//In Person+Category.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

複製程式碼

呼叫Person 的 new方法,執行

[Person new];

複製程式碼

執行結果:

2017-05-05 09:46:51.054 LoadAndInitializeExample[38773:1226306] +[Person(Category) initialize]
複製程式碼

執行後可以看到Person的initialize方法並沒有被執行,已經被Person+Category中的initialize取代了

當有多個Category時會怎樣了,我們在Person,Person+Category,Person+Category2,Person+Category3 的.m中都實現initialize方法

//In Person.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

//In Person+Category.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}
//In Person+Category2.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}
//In Person+Category3.m
+(void)initialize
{
     NSLog(@"%s",__FUNCTION__);
}

複製程式碼

呼叫Person new方法,執行

[Person new];
複製程式碼

執行結果:

2017-05-05 09:49:38.853 LoadAndInitializeExample[38825:1227819] +[Person(Category2) initialize]
複製程式碼

可以看到,當存在多個Category時,也只執行一個,具體執行哪一個Category中的initialize方法,測試後便可發現,會執行Compile Sources 列表中最後一個Category 的initialize方法

Compile Sources.png

什麼情況下使用:

####+load

由於呼叫load方法時的環境很不安全,我們應該儘量減少load方法的邏輯。另一個原因是load方法是執行緒安全的,它內部使用了鎖,所以我們應該避免執行緒阻塞在load方法中

load很常見的一個使用場景,交換兩個方法的實現

//摘自MJRefresh
+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
    [self exchangeInstanceMethod1:@selector(reloadRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_reloadRowsAtIndexPaths:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(deleteRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_deleteRowsAtIndexPaths:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(insertRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_insertRowsAtIndexPaths:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(reloadSections:withRowAnimation:) method2:@selector(mj_reloadSections:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(deleteSections:withRowAnimation:) method2:@selector(mj_deleteSections:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(insertSections:withRowAnimation:) method2:@selector(mj_insertSections:withRowAnimation:)];
}

+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}
複製程式碼

####+initialize initialize方法主要用來對一些不方便在編譯期初始化的物件進行賦值。比如NSMutableArray這種型別的例項化依賴於runtime的訊息傳送,所以顯然無法在編譯器初始化:


// In Person.m
// int型別可以在編譯期賦值
static int someNumber = 0; 
static NSMutableArray *someArray;
+ (void)initialize {
    if (self == [Person class]) {
        // 不方便編譯期複製的物件在這裡賦值
        someArray = [[NSMutableArray alloc] init];
    }
}
複製程式碼

總結:

load和initialize的共同點

1.如果父類和子類都被呼叫,父類的呼叫一定在子類之前

+load方法要點

當類被引用進專案的時候就會執行load函式(在main函式開始執行之前),與這個類是否被用到無關,每個類的load函式只會自動呼叫一次.由於load函式是系統自動載入的,因此不需要再呼叫[super load],否則父類的load函式會多次執行。

  • 1.當父類和子類都實現load函式時,父類的load方法執行順序要優先於子類
  • 2.當一個類未實現load方法時,不會呼叫父類load方法
  • 3.類中的load方法執行順序要優先於類別(Category)
  • 4.當有多個類別(Category)都實現了load方法,這幾個load方法都會執行,但執行順序不確定(其執行順序與類別在Compile Sources中出現的順序一致)
  • 5.當然當有多個不同的類的時候,每個類load 執行順序與其在Compile Sources出現的順序一致

注意: load呼叫時機比較早,當load呼叫時,其他類可能還沒載入完成,執行環境不安全. load方法是執行緒安全的,它使用了鎖,我們應該避免執行緒阻塞在load方法.


+initialize方法要點

initialize在類或者其子類的第一個方法被呼叫前呼叫。即使類檔案被引用進專案,但是沒有使用,initialize不會被呼叫。由於是系統自動呼叫,也不需要顯式的呼叫父類的initialize,否則父類的initialize會被多次執行。假如這個類放到程式碼中,而這段程式碼並沒有被執行,這個函式是不會被執行的。

  • 1.父類的initialize方法會比子類先執行
  • 2.當子類不實現initialize方法,會把父類的實現繼承過來呼叫一遍。在此之前,父類的方法會被優先呼叫一次
  • 3.當有多個Category都實現了initialize方法,會覆蓋類中的方法,只執行一個(會執行Compile Sources 列表中最後一個Category 的initialize方法)

注意: 在initialize方法收到呼叫時,執行環境基本健全。 initialize內部也使用了鎖,所以是執行緒安全的。但同時要避免阻塞執行緒,不要再使用鎖


以上總結可能並不完全,歡迎留言補充..... 程式碼地址:github.com/CoderZhuXH/…

相關文章