刨根問底+load方法

??首先我們都知道在iOS應用啟動的時候會調用所有類和其分類的+load方法。子類的load方法會在父類方法執行完成之后執行,分類的+load會在主類執行之后執行。不可繼承,子類沒有實現的時候,文件加載的時候是不會調用父類的load方法的。那么為什么+load的方法會有這樣的特性,runtime又有哪些巧妙的處理呢。今天我們來刨根問底一下+load方法。
??首先解釋兩個變量和兩個結構體,可以先看一眼有個印象,后面遇到再回來仔細察看

//每次調用add_class_to_loadable_list()方法都會++,記錄這個方法的調用次數,也相當于Class中+load方法列表的個數 
static int loadable_classes_used = 0;
//同上,只不過對應的是add_category_to_loadable_list()方法和Category中的+load方法列表
static int loadable_categories_used = 0;
//存儲了+load方法所屬的Class和+load方法的IMP
struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};
//同上,只不過對應的是Category
struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};
//存儲Class和categoty這兩個struct數據的是數組

1. Category方法列表的裝載

1.1 調用棧

??我們還是通過runtime的源碼來分析這個問題首先看分類的方法是什么時候加到主類的methed_list_t列表中的。下圖為調用棧:
調用棧.png

??可以看到調用次序是這樣的,我們重點看_read_images方法。

map_images() —>map_images_nolock()—>_read_images()
1.2 _read_images()

??源碼較長,這里我只貼對我們有用的部分

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//前面的都省略
// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
//后面的也省略
}

??注意到里面的addUnattachedCategoryForClass方法了嗎,OC里面都喜歡這種見文知義的命名方法,直譯過來就是為類添加未附加的類別。在這個方法里面,會找到未添加的類別列表對Class和Category做一個映射關聯。而在remethodizeClass()方法里面會調用attachCategories把Category中的方法列表加到Class的methed_list_t里面去。而且是插入到Class方法列表的前面(這就是Category中重寫主類的方法導致的方法覆蓋的原因)

2. +load方法的調用

2.1 調用棧

??我們接著來看+load的方法的調用,如下圖所示,這些會在Category方法裝載之后!下圖為調用棧

屏幕快照 2018-08-20 下午9.56.36.png

2.2 load_images()

??load_images()方法會多次調用(每個類都會調用一次)。我們來看load_images()方法的具體內容

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();
}

??里面有prepare_load_methods()和call_load_methods()這兩個主要方法,先來看第一個

2.3 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]));
    }
#pragma mark - 我是分割線—————————————————————————————————
    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);
    }
}

??這里面又分兩個重要的部分,上面分割線之前是Class中的+load方法加入到對應的list中去,那怎么保證的父類先調用呢

2.3.1 schedule_class_load() & add_class_to_loadable_list()
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); 
}
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[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

??schedule_class_load()方法做了遞歸調用一直調用到superclass為空,在schedule_class_load()方法中會調用add_class_to_loadable_list。這樣就保證了父類的+load方法是加載到list前面的,從父類到子類依次往數組中添加。執行的時候也是從前往后遍歷數組調用。
??add_class_to_loadable_list()方法的實現,我們可以很清楚的看到loadable_classes的初始化策略與溢出時的擴容策略。每次需要擴容都會在原來的基礎之上*2+16。

2.3.2 add_category_to_loadable_list()
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[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

??我們來看分割線之后的部分,在Class的+load方法處理完成時候才會來處理Category中的+load方法,這里用到了add_category_to_loadable_list()可以看到與add_class_to_loadable_list()方法的內容基本一樣只是標志位和數組的名字不同而已。add_category_to_loadable_list()在Category中的+load方法加入到對應的列表中。至此prepare_load_methods()方法執行完畢。

2.4 call_load_methods()

??接下來我們來看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;
}

??看代碼這里還用到了aotureleasePool,這個是題外話了,撇開不談。我們來看今天的重點,首先是一個do while循環,循環條件有兩個。第一個loadable_classes_used(查看文章開頭),第二個是call_category_loads()方法的返回值,這個返回值是什么呢,其實就是loadable_categories_used>0。在這里先后調用所有Class列表中的+load方法,Category列表中的+load方法call_class_loads()和call_category_loads()兩個方法的代碼我就不貼了。代碼有點多,影響閱讀,有興趣可以去看runtime的源碼。

3. 總結與問題

3.1總結

??分析下來,基本回答了文章開頭所述的+load方法特征的原因。總結如下:
??1、Category方法列表的裝載是在_read_images的時候發生的,這個調用比較早在map_images之后,load_images前。
??2、+load方法的調用是在load_images是發生的,而且load_image會重復調用(每個類都會調用)。而load_images調在_read_images之后,也就是說是在Category中的方法插入到Class中的方法列表之后調用的。
??3、+load方法的調用是又專門的方法負責的,Class和Category分別有一個數組保存+load方法(數組內保存的是loadable_class,和loadable_category結構體參考文章開頭的介紹)。所以不會被Category方法的裝載導致方法覆蓋。
??4、Class的數組,加入的次序也是有保證的,從最高級的父類到子類一次加入,調用的時候能保證先父類后子類。
??5、Category數組,調用是在Class中的方法列表調用完成之后,保證了次序。

3.2問題

??call_class_loads()的實現比較簡單,就是一個for循環依次調用+load。call_category_loads()內部的實現就復雜了不少,還有許多復雜的代碼,筆者也是沒有太看懂內部的那些判斷和循環,也不敢在這里誤人子弟。如果有哪位明白的請不吝指教。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容