結合 category 工做原理分析 OC2.0 中的 runtime

絕大多數 iOS 開發者在學習 runtime 時都閱讀過 runtime.h 文件中的這段代碼:html

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;複製代碼

能夠看到其中保存了類的實例變量,方法列表等信息。git

不知道有多少讀者思考過 OBJC2_UNAVAILABLE 意味着什麼。其實早在 2006 年,蘋果在 WWDC 大會上就發佈了 Objective-C 2.0,其中的改動包括 Max OS X 平臺上的垃圾回收機制(現已廢棄),runtime 性能優化等。github

這意味着上述代碼,以及任何帶有 OBJC2_UNAVAILABLE 標記的內容,都已經在 2006 年就永遠的告別了咱們,只停留在歷史的文檔中。objective-c

Category 的原理

雖然上述代碼已通過時,但仍具有必定的參考意義,好比 methodLists 做爲一個二級指針,其中每一個元素都是一個數組,數組中的每一個元素則是一個方法。數組

接下來就介紹一下 category 的工做原理,在美團的技術博客 深刻理解Objective-C:Category 中已經有了很是詳細的解釋,然而可能因爲時間問題,其中的很多內容已通過時,我根據目前最新的版本(objc-680) 作一些簡單的分析,爲了便於閱讀,在不影響代碼邏輯的前提下有可能刪除部分可有可無的內容。性能優化

概述

首先 runtime 依賴於 dyld 動態加載,在 objc-os.mm 文件中能夠找到入口,它的調用棧簡單整理以下:數據結構

void _objc_init(void)
└──const char *map_2_images(...)
    └──const char *map_images_nolock(...)
        └──void _read_images(header_info **hList, uint32_t hCount)複製代碼

以上四個方法能夠理解爲 runtime 的初始化過程,咱們暫且不深究。在 _read_images 方法中有以下代碼:函數

if (cat->classMethods  ||  cat->protocols  
    /* || cat->classProperties */) {
    addUnattachedCategoryForClass(cat, cls->ISA(), hi);
    if (cls->ISA()->isRealized()) {
        remethodizeClass(cls->ISA());
    }
}複製代碼

根據註釋可見蘋果曾經計劃利用 category 來添加屬性。在 addUnattachedCategoryForClass 方法中會找到當前類的全部 category,而後在 remethodizeClass 真正的去作處理。不過到目前爲止尚未接觸到相關的 category 處理,咱們繼續沿着調用棧向下走:佈局

void _read_images(header_info **hList, uint32_t hCount)
└──static void remethodizeClass(Class cls)
    └──static void attachCategories(Class cls, category_list *cats, bool flush_caches)複製代碼

這裏的 attachCategories 就是處理 category 的核心所在,不過在閱讀這段代碼以前,咱們有必要先熟悉一下相關的數據結構。性能

Category 相關的數據結構

首先來了解一下一個 Category 是如何存儲的,在 objc-runtime-new.h 中能夠看到以下定義,我只列出了其中成員變量:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};複製代碼

可見一個 category 持有了一個 method_list_t 類型的數組,method_list_t 又繼承自 entsize_list_tt,這是一種泛型容器:

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    // 成員變量和方法
};

template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
};複製代碼

這裏的 entsize_list_tt 能夠理解爲一個容器,擁有本身的迭代器用於遍歷全部元素。 Element 表示元素類型,List 用於指定容器類型,最後一個參數爲標記位。

雖然這段代碼實現比較複雜,但仍可瞭解到 method_list_t 是一個存儲 method_t 類型元素的容器。method_t 結構體的定義以下:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};複製代碼

最後,咱們還有一個結構體 category_list 用來存儲全部的 category,它的定義以下:

struct locstamped_category_list_t {
    uint32_t count;
    locstamped_category_t list[0];
};
struct locstamped_category_t {
    category_t *cat;
    struct header_info *hi;
};
typedef locstamped_category_list_t category_list;複製代碼

除了標記存儲的 category 的數量外,locstamped_category_list_t 結構體還聲明瞭一個長度爲零的數組,這實際上是 C99 中的一種寫法,容許咱們在運行期動態的申請內存。

以上就是相關的數據結構,只要瞭解到這個程度就能夠繼續讀源碼了。

處理 Category

對 Category 中方法的解析並不複雜,首先來看一下 attachCategories 的簡化版代碼:

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
    if (!cats) return;
    bool isMeta = cls->isMetaClass();

    method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int i = cats->count;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
}複製代碼

首先,經過 while 循環,咱們遍歷全部的 category,也就是參數 cats 中的 list 屬性。對於每個 category,獲得它的方法列表 mlist 並存入 mlists 中。

換句話說,咱們將全部 category 中的方法拼接到了一個大的二維數組中,數組的每個元素都是裝有一個 category 全部方法的容器。這句話比較繞,但你能夠把 mlists 理解爲文章開頭所說,舊版本的 objc_method_list **methodLists

在 while 循環外,咱們獲得了拼接成的方法,此時須要與類原來的方法合併:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);複製代碼

這兩行代碼讀不懂是必然的,由於在 Objective-C 2.0 時代,對象的內存佈局已經發生了一些變化。咱們須要先了解對象的佈局模型才能理解這段代碼。

Objective-C 2.0 對象佈局模型

objc_class

相信讀到這裏的大部分讀者都學習過文章開頭所說的對象佈局模型,所以在這一部分,咱們採用類比的方法,來看看 Objective-C 2.0 下發生了哪些改變。

首先,Classid 指針的定義並無發生改變,他們一個指向類對應的結構體,一個指向對象對應的結構體:

// objc.h
typedef struct objc_class *Class;
typedef struct objc_object *id;複製代碼

比較有意思的一點是,objc_class 結構體是繼承自 objc_object 的:

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
};複製代碼

這一點也很容易理解,早在 Objective-C 1.0 時代,咱們就知道一個對象的結構體只有 isa 指針,指向它所屬的類。而類的結構體也有 isa 指針指向它的元類。所以讓類結構體繼承自對象結構體就很容易理解了。

可見 Objective-C 1.0 的佈局模型中,cachesuper_class 被原封不動的移過來了,而剩下的屬性則彷佛消失不見。取而代之的是一個 bits 屬性,以及 data() 方法,這個方法調用的實際上是 bits 屬性的 data() 方法,並返回了一個 class_rw_t 類型的結構體指針。

class_data_bits_t

如下是簡化版 class_data_bits_t 結構體的定義:

struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}複製代碼

可見這個結構體只有一個 64 位的 bits 成員,存儲了一個指向 class_rw_t 結構體的指針和三個標誌位。它實際上由三部分組成。首先因爲 Mac OS X 只使用 47 位內存地址,因此前 17 位空餘出來,提供給 retain/release 和 alloc/dealloc 方法使用,作一些優化。其次,因爲內存對齊,指針地址的後三位都是 0,所以能夠用來作標誌位:

// class is a Swift class
#define FAST_IS_SWIFT (1UL<<0) // class or superclass has default retain/release/autorelease/retainCount/ // _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference #define FAST_HAS_DEFAULT_RR (1UL<<1) // class's instances requires raw isa #define FAST_REQUIRES_RAW_ISA (1UL<<2) // data pointer #define FAST_DATA_MASK 0x00007ffffffffff8UL複製代碼

若是計算一下就會發現,FAST_DATA_MASK 這個 16 進制常量的二進制表示剛好後三位爲0,且長度爲47位: 11111111111111111111111111111111111111111111000,咱們經過這個掩碼作按位與運算便可取出正確的指針地址。

引用 Draveness 在 深刻解析 ObjC 中方法的結構 中的圖片作一個總結:

bits 示意圖

class_rw_t

bits 中包含了一個指向 class_rw_t 結構體的指針,它的定義以下:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
}複製代碼

注意到有一個名字很相似的結構體 class_ro_t,這裏的 'rw' 和 ro' 分別表示 'readwrite' 和 'readonly'。由於 class_ro_t 存儲了一些由編譯器生成的常量。

These are emitted by the compiler and are part of the ABI.

正是因爲 class_ro_t 中的兩個屬性 instanceStartinstanceSize 的存在,保證了 Objective-C2.0 的 ABI 穩定性。由於即便父類增長方法,子類也能夠在運行時從新計算 ivar 的偏移量,從而避免從新編譯。

關於 ABI 穩定性的問題,本文不作贅述,讀者能夠參考 Non Fragile ivars

若是閱讀 class_ro_t 結構體的定義就會發現,舊版本實現中類結構體中的大部分紅員變量如今都定義在 class_ro_tclass_rw_t 這兩個結構體中了。感興趣的讀者能夠自行對比,本文再也不贅述。

class_rw_t 結構體中還有一個 methods 成員變量,它的類型是 method_array_t,繼承自 list_array_tt

list_array_tt 是一個泛型結構體,用於存儲一些元數據,而它其實是元數據的二維數組:

template <typename Element, typename List>{
    struct array_t {
        uint32_t count;
        List* lists[0];
    };
}
class method_array_t : public list_array_tt<method_t, method_list_t>複製代碼

其中 Element 表示元數據的類型,好比 method_t,而 List 則表示用於存儲元數據的一維數組,好比 method_list_t

list_array_tt 有三種狀態:

  1. 自身爲空,能夠類比爲 [[]]
  2. 它只有一個指針,指向一個元數據的集合,能夠類比爲 [[1, 2]]
  3. 它有多個指針,指向多個元數據的集合,能夠類比爲 [[1, 2], [3, 4]]

當一個類剛建立時,它可能處於狀態 1 或 2,但若是使用 class_addMethod 或者 category 來添加方法,就會進入狀態 3,並且一旦進入狀態 3 就不再可能回到其餘狀態,即便新增的方法後來又被移除掉。

方法合併

掌握了這些 runtime 的基礎只是之後就能夠繼續鑽研剩下的 category 的代碼了:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);複製代碼

這是剛剛卡住的地方,如今來看,rw 是一個 class_rw_t 類型的結構體指針。根據 runtime 中的數據結構,它有一個 methods 結構體成員,並從父類繼承了 attachLists 方法,用來合併 category 中的方法:

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    uint32_t oldCount = array()->count;
    uint32_t newCount = oldCount + addedCount;
    setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
    array()->count = newCount;
    memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
    memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}複製代碼

這段代碼很簡單,其實就是先調用 realloc() 函數將原來的空間拓展,而後把原來的數組複製到後面,最後再把新數組複製到前面。

在實際代碼中,比上面略複雜一些。由於爲了提升性能,蘋果作了一些優化,好比當 List 處於第二種狀態(只有一個指針,指向一個元數據的集合)時,其實並不須要在原地擴容空間,而是隻要從新申請一塊內存,並將最後一個位置留給原來的集合便可。

這樣只多花費了不多的內存空間,也就是原來二維數組佔用的內存空間,可是 malloc() 的性能優點會更加明顯,這實際上是一個空間換時間的權衡問題。

須要注意的是,不管執行哪一種邏輯,參數列表中的方法都會被添加到二維數組的前面。而咱們簡單的看一下 runtime 在查找方法時的邏輯:

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists) {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
    for (auto& meth : *mlist) {
        if (meth.name == sel) return &meth;
    }
}複製代碼

可見搜索的過程是按照從前向後的順序進行的,一旦找到了就會中止循環。所以 category 中定義的同名方法不會替換類中原有的方法,可是對原方法的調用實際上會調用 category 中的方法。

總結

讀完本文後,你應該對如下內容有比較深入的理解,排名不分前後:

  1. 定義在 runtime.h 中的數據結構,若是有 OBJC2_UNAVAILABLE 標記則表示已經廢棄。
  2. Objective-C 2.0 中,類結構體的結構層次: objc_class -> class_data_bits_t -> class_rw_t -> method_array_t
  3. class_ro_t 結構體的做用,與 class_rw_t 的區別,以及和 ABI 穩定性的關係。
  4. category 解析過程的調用棧以及基本的流程。
  5. method_array_t 爲何要設計成一種相似於二維數組的數據結構,以及它的三種狀態之間的關係。

參考資料

  1. 深刻理解Objective-C:Category
  2. 從源代碼看 ObjC 中消息的發送
  3. 深刻解析 ObjC 中方法的結構
  4. Whats is methodLists attribute of the structure objc_class for?
  5. Objc與C(C++)之親緣關係(一) Class
  6. Objective-C Runtime
相關文章
相關標籤/搜索