Runtime源碼 +load 和 +initialize

一、前言

在iOS的開發(fā)中,Runtime的方法交換都是寫在+load之中硫痰,為什么不是+initialize中呢?可能不少朋友對此或多或少有一點(diǎn)點(diǎn)的疑問窜护。 我們知道:OC中幾乎所有的類都繼承自NSObject效斑,而+load+initialize用于類的初始化,這兩者的區(qū)別和聯(lián)系到底何在呢柱徙?接下來我們一起來看看這二者的區(qū)別缓屠。

二、+load

根據(jù)官方文檔的描述:

  1. +load是當(dāng)一個類或者分類被添加到Objective-C運(yùn)行時調(diào)用的护侮。
  2. 本類+load的調(diào)用在其所有的父類+load調(diào)用之后敌完。
  3. 分類的+load在類的調(diào)用之后。

也就是說調(diào)用順序:父類 > 類 > 分類
但是不同類之間的+load方法的調(diào)用順序是不確定的羊初。

由于+load是在類第一次加載進(jìn)Runtime運(yùn)行時才調(diào)用滨溉,由此我們可以知道:

  • 它只會調(diào)用一次,這也是為什么么方法交換寫在+load的原因长赞。
  • 它的調(diào)用時機(jī)在main函數(shù)之前晦攒。

三、+load的實現(xiàn)

在Runtime源碼的objc-runtime-new.mmobjc-runtime-old.mm中的load_images方法中都存在這關(guān)鍵代碼:

prepare_load_methods((const headerType *)mh);

call_load_methods();

3.1 prepare_ load_methods

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

這里就是準(zhǔn)備好滿足 +load 方法調(diào)用條件的類和分類得哆,而對classcategory分開做了處理脯颜。

  • 在處理class的時候,調(diào)用了schedule_class_load:
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

從倒數(shù)第三句代碼看出這里對參數(shù)class的父類進(jìn)行了遞歸調(diào)用贩据,以此確保父類的優(yōu)先級

然后調(diào)用了add_class_to_loadable_list,把class加到了loadable_classes中:

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
  • 對于分類則是調(diào)用add_category_to_loadable_list把category加入到loadable_categories之中:
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

3.2 call_ load_methods

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

從注釋我們可以明確地看出:

  1. 重復(fù)地調(diào)用class里面的+load方法
  2. 一次調(diào)用category里面的+load方法

還是看一下調(diào)用具體實現(xiàn)栋操,以call_class_loads為例:

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

這就是真正負(fù)責(zé)調(diào)用類的 +load 方法了。它從上一步獲取到的全局變量 loadable_classes 中取出所有可供調(diào)用的類乐设,并進(jìn)行清零操作讼庇。

  • loadable_classes 指向用于保存類信息的內(nèi)存的首地址,
  • loadable_classes_allocated 標(biāo)識已分配的內(nèi)存空間大小近尚,
  • loadable_classes_used 則標(biāo)識已使用的內(nèi)存空間大小蠕啄。

然后,循環(huán)調(diào)用所有類的 +load 方法。注意歼跟,這里是(調(diào)用分類的 +load 方法也是如此)直接使用函數(shù)內(nèi)存地址的方式(*load_method)(cls, SEL_load); 對 +load 方法進(jìn)行調(diào)用的和媳,而不是使用發(fā)送消息 objc_msgSend 的方式。

這樣的調(diào)用方式就使得 +load 方法擁有了一個非常有趣的特性哈街,那就是子類留瞳、父類和分類中的 +load 方法的實現(xiàn)是被區(qū)別對待的。也就是說如果子類沒有實現(xiàn) +load 方法骚秦,那么當(dāng)它被加載時 runtime 是不會去調(diào)用父類的 +load 方法的她倘。同理,當(dāng)一個類和它的分類都實現(xiàn)了 +load 方法時作箍,兩個方法都會被調(diào)用硬梁。因此,我們常嘲茫可以利用這個特性做一些“有趣”的事情荧止,比如說方法交換method-swizzling

四阶剑、+initialize

根據(jù)官方文檔initialize的描述:

  1. Runtime發(fā)送+initialize消息是在類或者其子類第一次收到消息時跃巡,而且父類會在類之前接收到消息
  2. +initialize的實現(xiàn)是線程安全的,多線程下會有線程等待
  3. 父類的+initialize可能會被調(diào)用多次

也就是說 +initialize 方法是以懶加載的方式被調(diào)用的牧愁,如果程序一直沒有給某個類或它的子類發(fā)送消息素邪,那么這個類的 +initialize 方法是永遠(yuǎn)不會被調(diào)用的。那這樣設(shè)計有什么好處呢递宅?好處是顯而易見的娘香,那就是節(jié)省系統(tǒng)資源,避免浪費(fèi)办龄。

五、+initialize的實現(xiàn)

在runtime源碼objc-runtime-new.mm的方法lookUpImpOrForward中有如下代碼片段:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ···
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
}

可以知道:在方法調(diào)用過程中淋昭,如果類沒有被初始化的時候俐填,會調(diào)用_class_initialize對類進(jìn)行初始化,方法細(xì)節(jié)如下:

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
   * supercls = cls->superclass;
   * if (supercls  &&  !supercls->isInitialized()) {
   *     _class_initialize(supercls);
   * }
    
    // Try to atomically set CLS_INITIALIZING.
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }
    
    if (reallyInitialize) {
        // We successfully set the CLS_INITIALIZING bit. Initialize the class.
        
        // Record that we're initializing this class so we can message it.
        _setThisThreadIsInitializingClass(cls);

        if (MultithreadedForkChild) {
            // LOL JK we don't really call +initialize methods after fork().
            performForkChildInitialize(cls, supercls);
            return;
        }
        
        // Send the +initialize message.
        // Note that +initialize is sent to the superclass (again) if 
        // this class doesn't implement +initialize. 2157218
        if (PrintInitializing) {
            _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
                         pthread_self(), cls->nameForLogging());
        }

        // Exceptions: A +initialize call that throws an exception 
        // is deemed to be a complete and successful +initialize.
        //
        // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
        // bootstrapping problem of this versus CF's call to
        // objc_exception_set_functions().
        
        // Exceptions: A +initialize call that throws an exception 
        // is deemed to be a complete and successful +initialize.
        //
        // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
        // bootstrapping problem of this versus CF's call to
        // objc_exception_set_functions().
#if __OBJC2__
        @try
#endif
        {
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
        ...
}

這源碼我們可以可出結(jié)論:

  1. 從前面*的行數(shù)知道翔忽,_class_initialize方法會對class的父類進(jìn)行遞歸調(diào)用英融,由此可以確保父類優(yōu)先于子類初始化。
  2. 在截出的代碼末尾有著如下方法:callInitialize(cls);
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

這里指明了+initialize的調(diào)用方式是objc_msgSend,它和普通方法一樣是由Runtime通過發(fā)消息的形式歇式,調(diào)用走的都是發(fā)送消息的流程驶悟。換言之,如果子類沒有實現(xiàn) +initialize 方法材失,那么繼承自父類的實現(xiàn)會被調(diào)用痕鳍;如果一個類的分類實現(xiàn)了 +initialize 方法,那么就會對這個類中的實現(xiàn)造成覆蓋。

因此笼呆,如果一個子類沒有實現(xiàn) +initialize 方法熊响,那么父類的實現(xiàn)是會被執(zhí)行多次的。有時候诗赌,這可能是你想要的汗茄;但如果我們想確保自己的 +initialize 方法只執(zhí)行一次,避免多次執(zhí)行可能帶來的副作用時铭若,我們可以使用下面的代碼來實現(xiàn):

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

總結(jié)

+load +initialize
調(diào)用時機(jī) 加載到runtime時 收到第一條消息時洪碳,可能永遠(yuǎn)不調(diào)用
調(diào)用方式(本質(zhì)) 函數(shù)調(diào)用 runtime調(diào)度(和普通的方法一樣,通過objc_msgSend)
調(diào)用順序 父類 > 類 > 分類 父類 > 類
調(diào)用次數(shù) 一次 不定叼屠,可能多次可能不調(diào)用
是否沿用父類的實現(xiàn)
分類的中實現(xiàn) 類和分類都執(zhí)行 "覆蓋"類中的方法偶宫,只執(zhí)行分類的實現(xiàn)

后記

應(yīng)該盡可能減少initialize的調(diào)用,節(jié)省資源环鲤,截取官方原文的供大家參考:

Because initialize is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. Specifically, any code that takes locks that might be required by other classes in their initialize methods is liable to lead to deadlocks. Therefore, you should not rely on initialize for complex initialization, and should instead limit it to straightforward, class local initialization.

更多資料:
load
initialize
Objective-C +load vs +initialize

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纯趋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子冷离,更是在濱河造成了極大的恐慌吵冒,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件西剥,死亡現(xiàn)場離奇詭異痹栖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瞭空,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門揪阿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咆畏,你說我怎么就攤上這事南捂。” “怎么了旧找?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵溺健,是天一觀的道長。 經(jīng)常有香客問我钮蛛,道長鞭缭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任魏颓,我火速辦了婚禮岭辣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘甸饱。我一直安慰自己沦童,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搞动,像睡著了一般躏精。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹦肿,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天矗烛,我揣著相機(jī)與錄音,去河邊找鬼箩溃。 笑死瞭吃,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涣旨。 我是一名探鬼主播歪架,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼霹陡!你這毒婦竟也來了和蚪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤烹棉,失蹤者是張志新(化名)和其女友劉穎攒霹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浆洗,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡催束,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了伏社。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抠刺。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖摘昌,靈堂內(nèi)的尸體忽然破棺而出速妖,到底是詐尸還是另有隱情,我是刑警寧澤第焰,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布买优,位于F島的核電站,受9級特大地震影響挺举,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烘跺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一湘纵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滤淳,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汇歹。三九已至,卻和暖如春偿凭,著一層夾襖步出監(jiān)牢的瞬間产弹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工弯囊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痰哨,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓匾嘱,卻偏偏與公主長得像斤斧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子霎烙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容