Objective-C Runtime 解析(二)——NSObject的load與initialize方法

NSObject類作爲Objective-C中絕大多數類的父類,向其子類提供了基本的Runtime接口與Objective-C Class的一些方法默認實現。

在NSObject中有兩個類方法,load與initialize方法,由Runtime動態調用,用於配置Class或Category。(這種配置對於所有的Class實例,均有效)

在Apple文檔中,它們被分類爲Initializing a Class方法。

+initialize
Initializes the class before it receives its first message.
+load
Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

load方法

在程序運行時,Runtime會將所有的Class和Category加載到內存中,這時,會調用類的load方法,通知我們Class或Category已經被加載到內存中。

 +(void)load;

在Objective-C中,初始化的順序爲

The order of initialization is as follows:

  1. All initializers in any framework you link to.
  1. All +load methods in your image.
  1. All C++ static initializers and C/C++ attribute(constructor)
    functions in your image.
  1. All initializers in frameworks that link to you.

其中對於Objective-C的Class與Category的load方法的調用順序爲:

  1. superclass在subclass前調用load
  2. category在它所擴展的類之後被調用load方法

注意,在load方法中,你可以向其他類發送消息(即方法調用),Objective-C會保證消息發送成功。但是並不保證接受消息的load函數已經被調用。

initialize方法

initialize方法是用來初始化一個Class或Category的另一個方法。它與load方法調用的不同之處在於,它會在該Class或其subclass 接受代碼中第一個消息之前時候被調用。如果subclass沒有重寫initialize方法的話,則super class的initialize函數會被多次調用。
superclass會在subclass之前被調用initialize方法

+(void)initialize;

對於initialize方法,需要注意兩點:

  1. Runtime對於initialize方法的調用是線程安全的(注意這裏並沒有包含並自己顯示調用的情況),這意味着:
    (1)initialize會在class接受第一個消息的線程中被調用。
    (2)initialize會阻塞class的message sending,只有當initialize方法執行完畢,其餘的message纔會被執行。這裏有dead lock的危險,因此在initialize中的代碼不應過於複雜。

  2. 同一個class的initialize方法可能會被多次調用,這種情況發生在subclass沒有實現initialize方法的時候或者顯示調用[super initialize]時。
    爲了防止initialize多次調用,可以在initialize方法中加入判斷當前調用者是否爲當前類本身。

+(void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

load VS initialize

load 與 initialize 方法是沒有先後關係的。

它們調用的時機不一樣:

  • load當runtime將類加載時被調用,而且這個時機要先於appdelegate中的
    (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  • initialize方法發生在類第一次接受消息時調用。關於這裏的第一個消息需要特別說明一下,對於 NSObject 的 runtime 機制而言,其在調用 NSObject 的 + (void)load 消息不被視爲第一個消息,但是,如果像普通函數調用一樣直接調用 NSObject 的 + (void)load 消息,則會引起 + (void)initialize 的調用。反之,如果沒有向 NSObject 發送第一個消息,+ (void)initialize 則不會被自動調用。

load 與 initialize的繼承性不一樣

  • load方法針對於每一個類而言,它不會被繼承。如果子類沒有重寫load方法,也不會調用superclass的load方法。
  • initialize 方法可以被繼承,當subclass沒有重寫initialize方法時,如果superclass中有實現,則會調用之。
  • 對於Category,會覆蓋其對應類的initialize方法。
  • 當一個類被runtime加載時,只會調用其自身的load方法,其superclass的load方法不會被調用。而當initialize被調用時,其superclass的initialize函數會被依次調用。如下代碼
@interface Car : NSObject

@end

@interface AutoCar : Car

@end

@interface BMWCar : AutoCar

@end

@interface BMWCar(MyCar)

@end

@implementation Car
+(void) initialize
{
    NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

@implementation AutoCar

+(void) initialize
{
    NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

@implementation BMWCar
+(void) initialize
{
    NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    BMWCar *myCar = [BMWCar new];
    
}

輸出結果爲:

Car +[Car initialize]
AutoCar +[AutoCar initialize]
BMWCar +[BMWCar initialize]

參考文章

CaryaLiu 博客
Apple文檔

應用實例

Method Swizzling 和 AOP 實踐
Notification Once

補充:一種另類的單例實現

突然想到了一個問題,既然load方法與initialize方法都會被Runtime自動調用一次,並且在Runtime情況下,這兩個方法都是線程安全的,那麼是否可以作爲單例類的一個實現呢?
不廢話,上代碼:

@interface MySpecialObject : NSObject
+(MySpecialObject *) sharedInstance;
@end
// 利用initialize函數實現
static MySpecialObject *singalObject = nil;
@implementation MySpecialObject
+(void) initialize
{
    if (self == [MySpecialObject self] && singalObject == nil) {
        singalObject = [[MySpecialObject alloc] init];
    }
}

+(MySpecialObject *) sharedInstance
{
    return singalObject;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    MySpecialObject *obj = [MySpecialObject sharedInstance];
    NSLog(@"%@", obj);
}

-(void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    MySpecialObject *obj = [MySpecialObject sharedInstance];
    NSLog(@"%@", obj);
}

-(void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    MySpecialObject *obj = [MySpecialObject sharedInstance];
    NSLog(@"%@", obj);
}

輸出:

2016-08-13 16:13:07.951 SpecialSingalObject[1304:110175] <MySpecialObject: 0x7fc5d849f400>
2016-08-13 16:13:08.826 SpecialSingalObject[1304:110175] <MySpecialObject: 0x7fc5d849f400>
2016-08-13 16:13:09.556 SpecialSingalObject[1304:110175] <MySpecialObject: 0x7fc5d849f400>

確實,得到了同一個實例。
對於initialize 函數,的解釋:
(1) if (self == [MySpecialObject self] …
是爲了保證 initialize函數只有在本類而非subclass時才執行單例初始化函數。

(2)if(…&& singalObject == nil) 是爲了防止initialize多次調用而產生多個實例(除了Runtime調用,我們也可以顯示調用initialize方法)。經過測試,當我們將initialize方法本身作爲class的第一個方法執行時,Runtime的initialize會被先調用(這保證了線程安全),然後我們自己顯示調用的initialize函數再被調用。
由於initialize方法的第一次調用一定是Runtime調用,而Runtime又保證了線程安全性,因此這裏只簡單的檢測 singalObject == nil即可。

上面的代碼確實是實現了線程安全的單例模式,但是由於load與initialize方法的本意是對於Class做設置,並且考慮到load或initialize方法有可能是在程序運行的早期執行,可能我們的單例所依賴的環境尚未形成,所有我們還是不要用這種方式實現單例吧。

經過測試,當我們的MySpecialObject類中包含其他自定義class的屬性時,如Book。
當我們在MySepcialObject 的init函數中初始化Book屬性時,此時雖然Book類的load方法沒被調用,但是我們的Book屬性仍然能被正常初始化。
也許,對於load方法的調用,Runtime是在將所有的Class加載到內存中後,再統一調用的原因嗎?

關於自己最後一個關於Book屬性的問題,在查閱了Runtime源碼之後,確實如推測那樣,Runtime會在所有類都加載入內存後,在統一的調用load方法。當初在寫這篇博客時,還沒有去深入的分析Runtime的源碼(還沒有能力:)),現在回過頭來看一下,這也就是對一個問題不斷深入理解的過程吧~

附上相關代碼, 當dyld將所有的class image都映射到內存後,會調用load images來加載class,最後,會調用load 方法:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
發佈了89 篇原創文章 · 獲贊 47 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章