Category 詳解(一)—— 源碼層面解析分類衝突問題

寫在前面

這個系列的文章僅僅對 Category 的實如今源碼層面作必定的分析,你們仍是要多上手嘗試,你會發現不少有意思的問題,例如:
若是你的工程和你使用的第三方庫,產生了方法重名,在你開啓了 LTO 優化的先後,你會發現分別調用到了第三方庫和你的主工程方法中,這又是爲何呢?容許我在開頭就挖個坑,這是個頗有意思的問題,等我有空會寫一篇文章。c++

這裏有幾點要說明的:數組

  1. 蘋果開源的代碼對於咱們的意義僅僅只指導,不可所有相信
  2. rewrite 是 Xcode 工具鏈中的一項,實際上編譯後產生的 c++ 代碼不等於真正執行的代碼,若是想要知道真正運行的代碼,應當採起彙編調試的方式。

前情

Category相信你們都有使用過,利用分類能夠對一些系統類進行擴展,分模塊等等。咱們先來構造個分類看一下:markdown

// Person類
- (void)run {
    NSLog(@"run");
}
複製代碼
// Person+Test 分類
- (void)test {
    NSLog(@"test");
}
 
+ (void)test2 {
    NSLog(@"test2");
}
複製代碼

這樣咱們就能夠在引入頭文件後調用對應的方法。函數

咱們都知道,調用方法的時候是經過objc_msgSend(self, SEL)來實現的,那麼會產生問題:工具

person的類對象中有沒有存儲分類中的實例方法?Person+Test會不會有本身的分類對象,將分類中的實例方法存放在分類對象中呢?
先說結論:不論有沒有分類,每一個類都只有一個類對象,分類中的方法也是存儲在Person類的類對象中的。
優化

結構體解析

首先咱們 cd 到 Person+Eat 的目錄下,而後執行 clang rewrite-objc Person+Test.m,這樣Person+Test.m就被轉化爲了c++的代碼 Person+Test.cppui

這個文件比較長,咱們直接看 _category_t 這個結構體,這個結構體是每個分類的結構spa


而後再找到這個結構體初始化的地方:調試


經過這裏咱們就能看出,Person+Test的初始化時,類名叫作 Person,有實例方法和類方法,其餘都是空,那麼讓咱們來看看實例方法和類方法初始化的地方:code



不難看出也正好對應這 Person+Test 分類中的兩個方法,- (void)test 和 + (void)test2

方法合併

實際上這些方法、屬性、協議,都是在運行時經過runtime進行合併的,要了解這個合併的過程,就須要閱讀runtime的源碼。下面是本次閱讀runtime源碼的一些過程:

  • 下載 runtime 源碼,老的版本須要經過 cmake 來生成 Xcode 項目,而比較新的版本則是已經幫你作好了,此次下載的版本是objc4-781。
  • 找到 objc-os.mm 這個文件,這個文件是 runtime 的入口文件。
  • 在 objc-os.mm 中找到 void _objc_init(void)這個方法,這個方法是運行時的初始化方法。
  • 在 _objc_init 方法最後,調用了_dyld_objc_notify_register 函數,這個函數裏有個參數是 map_images,咱們點進 map_images 看一下。
  • 注意map_images 調了一個函數 map_images_nolock(count, pahts, mhdrs),咱們點進去。
  • 這個函數比較長,咱們到最下面找到 _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses),點進去。
  • 在中間偏下的部分,咱們能夠看到這麼一段代碼,調用了 load_categories_nolock(hi),點進去。

  • 在中間位置找到了核心函數,核心函數是 attachCategories(cls, &lc, 1, ATTACH_EXISTING),點進去,看一下方法註釋。

這個方法中,將全部的category中的方法、屬性、協議,整合到二維數組method_list_t mlists中,而後調用 attachLists 方法進行attach,咱們來看一下這個方法裏的代碼

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
 
    if (hasArray()) {
    // many lists -> many lists
    uint32_t oldCount = array()->count;
    uint32_t newCount = oldCount + addedCount;
    // 從新分配內存,由於要進行合併,因此原來分配的內存不夠了
    setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
    array()->count = newCount;
    // 將array()原有的list拷貝到新list的尾部
    memmove(array()->lists + addedCount, array()->lists,
            oldCount * sizeof(array()->lists[0]));
    // 將新的方法list拷貝到list的頭部
    memcpy(array()->lists, addedLists,
            addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    }
    else {
        // 1 list -> many lists
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        array()->count = newCount;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists,
                addedCount * sizeof(array()->lists[0]));
    }
}
複製代碼

總結:

這裏咱們看到,最終合併的方法列表的存儲結構是順序表,而查找時也是順序查找的。

分類方法會覆蓋原類方法的緣由是由於原類方法被放在了list最後(memmove操做)。

後加載的分類方法會覆蓋前面的分類方法是由於,在 map_images_nolock 函數中,採用的是while(i--)的方式,所以後加載的分類會排在前面。

寫在後面

這篇文章中沒有把全部的源碼拿出來,你們應當對着源碼親自上手看一看。 文中若有錯誤,歡迎指出。

相關文章
相關標籤/搜索