天天看點

iOS原理分析之從源碼看load與initialize方法(二)

四、initialize方法分析

   我們可以采用和分析load方法時一樣的政策來對initialize方法的執行情況,進行測試,首先将測試工程中所有類中添加initialize方法的實作。此時如果直接運作工程,你會發現控制台沒有任何輸出,這是由于隻有第一次調用類的方法時,才會執行initialize方法,在main函數中編寫如下測試代碼:

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       [MySubObjectOne new];

       [MyObjectOne new];

       [MyObjectTwo new];

       NSLog(@"------------");

   }

   return 0;

}

運作代碼控制台列印效果如下:

2021-02-18 21:29:55.761897+0800 KCObjc[43834:23521232] initialize-cateOne:MyObjectOne

2021-02-18 21:29:55.762526+0800 KCObjc[43834:23521232] initialize:MySubObjectOne

2021-02-18 21:29:55.762622+0800 KCObjc[43834:23521232] initialize-cate:MyObjectTwo

2021-02-18 21:29:55.762665+0800 KCObjc[43834:23521232] ------------

可以看到,列印資料都出現在分割線前,說明一旦一個類的initialize方法被調用後,後續再向這個類發送消息,也不會在調用initialize方法,還有一點需要注意,需要注意,如果對子類發送消息,父類的initialize會先調用,再調用子類的initialize,同時,分類中如果實作了initialize方法則會覆寫類本身的,并且分類的加載順序靠後的會覆寫之前的。下面我們就通過源碼來分析下initialize方法的這種調用特點。

   首先,在調用類的類方法時,會執行runtime中的class_getClassMethod方法來尋找實作函數,這個方法在源碼中的實作如下:

Method class_getClassMethod(Class cls, SEL sel)

{

   if (!cls  ||  !sel) return nil;

   return class_getInstanceMethod(cls->getMeta(), sel);

通過源碼可以看到,調用一個類的類方法,實際上是調用其元類的示例方法,getMeta函數用來擷取類的元類,關于類和元類的相關組織原理,我們這裡先不擴充。我們需要關注的是class_getInstanceMethod這個函數,這個函數的實作也非常簡單,如下:

Method class_getInstanceMethod(Class cls, SEL sel)

   // 做查詢方法清單,嘗試方法解析相關工作

   lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

   // 從類對象中擷取方法

   return _class_getMethod(cls, sel);

在class_getInstanceMethod方法的實作中,_class_getMethod是最終擷取要調用的方法的函數,在這之前,lookUpImpOrForward函數會做一些前置操作,其中就有initialize函數的調用邏輯,我們去掉無關的邏輯,lookUpImpOrForward中核心的實作如下:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

   IMP imp = nil;

   // 核心在于!cls->isInitialized() 如果目前類未初始化過,會執行initializeAndLeaveLocked函數

   if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {

       cls = initializeAndLeaveLocked(cls, inst, runtimeLock);

   return imp;

initializeAndLeaveLocked會直接調用initializeAndMaybeRelock函數,如下:

static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)

   return initializeAndMaybeRelock(cls, obj, lock, true);

initializeAndMaybeRelock函數中會做類的初始化邏輯,這個過程是線程安全的,其核心相關代碼如下:

static Class initializeAndMaybeRelock(Class cls, id inst,

                                     mutex_t& lock, bool leaveLocked)

   // 如果已經初始化過,直接傳回

   if (cls->isInitialized()) {

       return cls;

   // 找到目前類的非元類

   Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

   // 進行初始化操作

   initializeNonMetaClass(nonmeta);

   return cls;

initializeNonMetaClass函數會采用遞歸的方式沿着繼承鍊向上查詢,找到所有未初始化過的父類進行初始化,核心實作簡化如下:

void initializeNonMetaClass(Class cls)

   Class supercls;

   // 标記是否需要初始化

   bool reallyInitialize = NO;

   // 父類如果存在,并且沒有初始化過,則遞歸進行父類的初始化

   supercls = cls->superclass;

   if (supercls  &&  !supercls->isInitialized()) {

       initializeNonMetaClass(supercls);

   SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;

   {

       // 如果目前不是正在初始化,并且目前類沒有初始化過

       if (!cls->isInitialized() && !cls->isInitializing()) {

           // 設定初始化标志,此類标記為初始化過

           cls->setInitializing();

           // 标記需要進行初始化

           reallyInitialize = YES;

       }

   // 是否需要進行初始化

   if (reallyInitialize) {

       @try

       {

           // 調用初始化函數

           callInitialize(cls);

       @catch (...) {

           @throw;

       return;

callInitialize函數最終會調用objc_msgSend函數來向類發送initialize初始化消息,如下:

void callInitialize(Class cls)

   ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));

   asm("");

需要注意,initialize方法與load方法最大的差別在于其最終是通過objc_msgSend來實作的,每個類如果未初始化過,都會通過objc_msgSend來向類發送一次initialize消息,是以,如果子類沒有對initialize實作,按照objc_msgSend的消息機制,其是會沿着繼承鍊一路向上找到父類的實作進行調用的,所有initialize方法并不是隻會被調用一次,假如父類中實作了這個方法,并且它有多個未實作此方法的子類,則當每個子類第一次接受消息時,都會調用一遍父類的initialize方法,這點非常重要,在實際開發中一定要牢記。

五、結語

   load和initialize方法是iOS開發中非常簡單也也非常常用的兩個方法,然而其與普通的方法比起來,還有有一些特殊,通過對源碼的解讀,我們可以更加深刻的了解這些特殊之處的原因及原理,程式設計的過程就像修行,知其然也知其是以然,與大家共勉。

繼續閱讀