Runtime源代碼解讀4(方法列表)

2019-10-15數組

類的方法包括實例方法(instance method)和類方法(class method),二者保存在徹底不一樣的地方,實例方法保存在類的方法列表中,類方法保存在元類的方法列表中。在Runtime源代碼解讀(實現面向對象初探)中已介紹過方法的基本數據結構objc_method,以及方法的基本響應鏈,本文介紹方法列表的具體實現原理。緩存

1、數據結構

Runtime源代碼解讀2(類和對象)中介紹過:類的class_ro_t包含method_list_t類型的baseMethodList成員;類的class_rw_t包含method_array_t類型的methods成員。二者一樣保存方法列表,卻使用不一樣的數據結構,那麼它們之間有什麼關係呢?因爲class_ro_t是保存的基本上是編譯時決議的數據,baseMethodList顯然是保存類定義時所定義的方法,這些方法是編譯時決議的。而class_rw_tmethods實際上纔是類的完整方法列表,並且class_rw_tmethods包含了class_ro_tbaseMethodListbash

類的class_rw_t保存方法列表保存形式並非簡單的一維數組結構,而是二維數組結構,這是爲了區分類構建階段定義的基本方法,以及不一樣分類之間的定義的方法。objc_class保存方法列表的數組結構是method_array_t類,method_array_t繼承list_array_tt模板類。數據結構

1.1 list_array_tt 二維數組容器

Runtime 定義list_array_tt類模板表示二維數組容器。list_array_tt保存的數據主體是一個聯合體,包含listarrayAndFlag成員,表示:容器要麼保存一維數組,此時聯合體直接保存一維數組的地址,地址的最低位必爲0;要麼保存二維數組,此時聯合體保存二維數組的首個列表元素的地址,且最低位置爲1。調用hasArray()方法能夠查詢容器是否保存的是二維數組,返回arrayAndFlag & 1,即經過最低位是否爲1進行判斷;調用list_array_ttarray()方法能夠獲取二維數組容器的地址,返回arrayAndFlag & ~1,即忽略最低位。函數

當聯合體保存二維數組時,聯合體的arrayAndFlag指向list_array_tt內嵌定義的array_t結構體。該結構體是簡單的一維數組容器,但其元素爲指向列表容器的指針,所以array_t的本質是二維數組。調用array_tbyteSize()能夠返回列表容器佔用的總字節數,爲array_t結構體自己的size與保存列表元素的連續內存區塊的size之和。post

// 二維數組容器模板
template <typename Element, typename List>
class list_array_tt {
      
    // 定義二維數組的外層一維數組容器
    struct array_t {
        uint32_t count;
        List* lists[0];

        static size_t byteSize(uint32_t count) {
            return sizeof(array_t) + count*sizeof(lists[0]);
        }

        // 計算容器佔用字節數
        size_t byteSize() {
            return byteSize(count);
        }
    };

 protected:
    // 內嵌迭代器的定義(後文 1.1.1 節介紹)
    ...

 private:
    union {
        List* list;  // 要麼指向一維數組容器
        uintptr_t arrayAndFlag;  // 要麼指向二維數組容器
    };

    // 容器是否保存的是二維數組
    bool hasArray() const {
        return arrayAndFlag & 1;
    }

    // 獲取容器保存的二維數組的地址
    array_t *array() {
        return (array_t *)(arrayAndFlag & ~1);
    }

    void setArray(array_t *array) {
        arrayAndFlag = (uintptr_t)array | 1;
    }

 public:
    // 計算方法列表二維數組容器中全部方法的數量
    uint32_t count() {
        uint32_t result = 0;
        for (auto lists = beginLists(), end = endLists(); 
             lists != end;
             ++lists)
        {
            result += (*lists)->count;
        }
        return result;
    }

    // 獲取方法列表二維數組容器的起始迭代器
    iterator begin() {
        return iterator(beginLists(), endLists());
    }

    // 獲取方法列表二維數組容器的結束迭代器
    iterator end() {
        List **e = endLists();
        return iterator(e, e);
    }

    // 獲取方法列表二維數組容器包含的方法列表數量
    uint32_t countLists() {
        if (hasArray()) {
            return array()->count;
        } else if (list) {
            return 1;
        } else {
            return 0;
        }
    }

    // 獲取方法列表二維數組容器的起始地址
    List** beginLists() {
        if (hasArray()) {
            return array()->lists;
        } else {
            return &list;
        }
    }

    // 獲取方法列表二維數組容器的結束地址
    List** endLists() {
        if (hasArray()) {
            return array()->lists + array()->count;
        } else if (list) {
            return &list + 1;
        } else {
            return &list;
        }
    }

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        // 後文 1.1.2 節介紹
        ...
    }

    void tryFree() {
        // 後文 1.1.2 節介紹
        ...
    }

    template<typename Result> Result duplicate() {
        // 後文 1.1.2 節介紹
        ...
    }
};
複製代碼

1.1.1 list_array_tt 的內嵌迭代器

list_array_tt容器模板的內嵌迭代器iterator類的定義以下,包含三個成員:ui

  • lists:當前迭代到的數組的位置;
  • listsEnd:二維數組的外層容器的結尾;
  • m:當前迭代到的數組 中的元素的位置;
  • mEnd:當前迭代到的數組的結尾;

注意:構建list_array_tt的迭代器時,只能從方法列表到方法列表,不能從容器中的方法列表的某個元素的迭代器開始迭代。this

class iterator {
    List **lists;
    List **listsEnd;
    typename List::iterator m, mEnd;

 public:
    // 構建從begin指向的列表,到end指向的列表的迭代器
    iterator(List **begin, List **end) 
        : lists(begin), listsEnd(end)
    {
        if (begin != end) {
            m = (*begin)->begin();
            mEnd = (*begin)->end();
        }
    }

    const Element& operator * () const {
        return *m;
    }
    Element& operator * () {
        return *m;
    }

    bool operator != (const iterator& rhs) const {
        if (lists != rhs.lists) return true;
        if (lists == listsEnd) return false;  // m is undefined
        if (m != rhs.m) return true;
        return false;
    }

    //迭代時,若達到當前數組的結尾,則切換到下一個數組的開頭
    const iterator& operator ++ () {
        assert(m != mEnd);
        m++;
        if (m == mEnd) {
            assert(lists != listsEnd);
            lists++;
            if (lists != listsEnd) {
                m = (*lists)->begin();
                mEnd = (*lists)->end();
            }
        }
        return *this;
    }
};
複製代碼

1.1.2 list_array_tt 公開的操做方法

list_array_tt容器模板的三個操做類型的公開方法以下:編碼

  • attachLists(...):將列表元素添加到二維數組容器的開頭,注意到list_array_t沒有定義構造函數,這是由於構造邏輯均在attachLists(...)中,包含容器從空轉化爲保存一位數組,再轉化爲保存二維數組的處理邏輯;
  • tryFree():釋放二維數組容器佔用內存;
  • duplicate(...):複製二維數組容器;
// 向二維數組容器中添加列表
void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // 當容器中保存的是二維數組
        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]));  // 將須要新增的方法列表拷貝到新內存空間
    }
    else if (!list  &&  addedCount == 1) {
        // 當容器中無任何方法,直接將list成員指向addedLists
        list = addedLists[0];
    } 
    else {
        // 當容器中保存的是一維數組
        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]));
    }
}

// 釋放容器佔用的內存空間
void tryFree() {
    if (hasArray()) {
        for (uint32_t i = 0; i < array()->count; i++) {
            try_free(array()->lists[I]);
        }
        try_free(array());
    }
    else if (list) {
        try_free(list);
    }
}

// 拷貝容器
template<typename Result>
Result duplicate() {
    Result result;

    if (hasArray()) {
        array_t *a = array();
        result.setArray((array_t *)memdup(a, a->byteSize()));
        for (uint32_t i = 0; i < a->count; i++) {
            result.array()->lists[i] = a->lists[i]->duplicate();
        }
    } else if (list) {
        result.list = list->duplicate();
    } else {
        result.list = nil;
    }

    return result;
}
複製代碼

1.2 method_array_t 方法列表二維數組容器

類的方法列表信息保存在class_rw_t結構體的methods成員中,類型爲method_array_tmethod_array_t是按list_array_tt模板構建,以method_t(方法)爲元素,以method_list_t(方法列表)爲列表容器的二維數組容器,用於存儲類的方法列表。method_list_t繼承自entsize_list_tt順序表模板,entsize_list_ttRuntime源代碼解讀2(類和對象) 介紹過,是具備固定類型元素的順序表容器,注意到FlagMask指定爲0x03,所以method_list_tentsizeAndFlags成員最低兩位預留有特殊功能:若最低兩位均爲1,表示該方法列表已排序。spa

method_array_t中擴展list_array_tt的方法的方法名可見,method_array_t是爲 category 量身定作的,顯然 category 中定義的全部方法都存儲在該容器中,每一個 category 定義的方法對應method_array_t二維數組容器中的一個元素,也就是一個方法列表method_list_t結構體的指針。擴展的方法以下:

  • beginCategoryMethodLists():指向容器的第一個數組;
  • endCategoryMethodLists(Class cls):當類的class_rw_t中不存在baseMethodList時,直接返回容器最後一個數組,當存在baseMethodList時,返回容器的倒數第二個數組。
static uint32_t fixed_up_method_list = 3;

// 方法列表
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    // 查詢方法列表是否已排序
    bool isFixedUp() const {
        return flags() == fixed_up_method_list;
    }
    
    // 標記方法列表已排序
    void setFixedUp() {
        runtimeLock.assertLocked();
        assert(!isFixedUp());
        entsizeAndFlags = entsize() | fixed_up_method_list;
    }

    // 新增返回方法在順序表中的索引值的方法
    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t I = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        assert(i < count);
        return I;
    }
};

// 方法列表二維數組容器
class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls) {
        method_list_t **mlists = beginLists();
        method_list_t **mlistsEnd = endLists();
    
        if (mlists == mlistsEnd  ||  !cls->data()->ro->baseMethods()) 
        {
            return mlistsEnd;
        }
    
        return mlistsEnd - 1;
    };

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

複製代碼

Runtime源代碼解讀2(類和對象) 中介紹過類的加載過程。從 class realizing 時調用的methodizeClass(...)函數的處理邏輯能夠看出:class_rw_t中的method_array_t容器保存了類的完整方法列表,包括靜態編譯的類的基本方法、運行時決議的 category 中的方法以及運行時動態添加的方法。並且class_rw_tmethod_array_t容器的最後一個數組實際上就是class_ro_tbaseMethodList

再結合 1.1.2 介紹的list_array_ttattachLists(...)方法邏輯,能夠基本瞭解方法列表容器的工做機制。當使用class_addMethod(...)動態添加類,或者應用加載階段加載 category 時,均調用了該方法。因爲attachLists(...)添加方法時,將方法添加到容器的開頭,將原有的method_list_t集體後移,所以類的同名方法的IMP的優先級從高到低排序以下:

  • 經過class_addMethod(...)動態添加的方法;
  • 後編譯的類的 category 中的方法;
  • 先編譯的類的 category 中的方法;
  • 類實現的方法;
  • 類的父類實現的方法;

類的方法列表的結構可總結以下圖所示。其中綠色表示method_list_t容器,藍色表示保存method_t結構體,黃色表示指向method_list_t結構體的指針。上圖爲method_array_t保存一維數組容器(method_list_t)時的內存結構,此時method_array_t的聯合體list成員有效;下圖爲method_array_t保存二維數組容器時的內存結構,此時method_array_t的聯合體arrayAndFlag成員有效。

當method_array_t的保存一位數組.jpg

當method_array_t保存二維數組.jpg

2、 動態添加方法的實現原理

運行時調用class_addMethod (...)函數能夠給類動態添加方法,調用class_replaceMethod(...)能夠替換方法的實現。實際上二者都調用了addMethod(...)函數,原理是先根據傳入的方法名、方法IMP、類型編碼構建method_t,而後新建method_list_t方法列表容器將method_t添加到其中,最後調用attachList(...)將方法列表添加到類的class_rw_tmethods方法列表二維數組容器中。

class_addMethod (...)的源代碼以下:

// 添加方法
BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    rwlock_writer_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

// 替換方法實現
IMP 
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return nil;

    rwlock_writer_t lock(runtimeLock);
    return addMethod(cls, name, imp, types ?: "", YES);
}

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertWriting();

    assert(types);
    assert(cls->isRealized());

    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // 若方法已經存在
        if (!replace) {
            result = m->imp;
        } else {
            result = _method_setImplementation(cls, m, imp);  // 設置方法IMP
        }
    } else {
        // 若方法不存在,則構建方法列表容器
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdup(types);
        if (!ignoreSelector(name)) {
            newlist->first.imp = imp;
        } else {
            newlist->first.imp = (IMP)&_objc_ignored_method;
        }

        prepareMethodLists(cls, &newlist, 1, NO, NO);  // 後文 2.1 中詳細介紹

        // 添加方法到類的class_rw_t的methods中
        cls->data()->methods.attachLists(&newlist, 1);  

        flushCaches(cls);  // 後文 2.2中詳細介紹

        result = nil;
    }

    return result;
}

// 設置方法IMP
static IMP 
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
    runtimeLock.assertWriting();

    if (!m) return nil;
    if (!imp) return nil;

    // 對於打開ARC的編譯環境,忽略retain/release等MRC方法
    if (ignoreSelector(m->name)) {
        return m->imp;
    }

    IMP old = m->imp;
    m->imp = imp;

    flushCaches(cls);  // 清空cls類的方法緩衝

    updateCustomRR_AWZ(cls, m);  // 對retain/release等MRC方法,以及allocWithZone方法的特殊處理

    return old;
}

複製代碼

向類動態添加方法,類的方法列表結構先後對比圖示以下。其中紅色標記部分爲新增結構。處理過程爲:將添加方法封裝到method_list_t容器,而後插入到method_array_t的外層一位數組開頭,其餘元素總體後移,外層一位數組長度增長1

類動態插入方法方法先後列表結構圖示.jpg

2.1 method_list_t 排序

addMethod(...)函數中還調用了prepareMethodLists(...)函數,該方法主要調用了fixupMethodList(...)函數。fixupMethodList(...)作了三件事情:

  • 調用sel_registerNameNoLock函數將方法列表中全部方法的SEL註冊到內存,註冊的內存實際是以 方法名字符串 爲鍵值,將SEL插入到系統維護的namedSelectors哈希表中;
  • 對方法列表內的全部方法進行排序;
  • 將方法列表狀態標記爲isFixedUp,表示方法列表已排序;

之因此對方法列表排序,是爲了使方法列表支持二分查找,從而下降經過SEL在方法列表中搜索IMP的時間複雜度。prepareMethodLists(...)函數相關代碼以下:

static void 
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount, 
                   bool baseMethods, bool methodsFromBundle)
{
    if (addedCount == 0) return;

    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[I];
        assert(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }
}

static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    for (auto& meth : *mlist) {
        const char *name = sel_cname(meth.name);
        
        // 註冊方法列表中的方法名
        SEL sel = sel_registerNameNoLock(name, bundleCopy); 
        meth.name = sel;
        
        if (ignoreSelector(sel)) {
            meth.imp = (IMP)&_objc_ignored_method;
        }
    }
    
    // 按照SEL對方法列表排序
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
    
    // 標記方法列表已經排好序並固定。
    // method_list_t的entsizeAndFlags最低兩位均爲1表示方法列表已經排好序並固定
    mlist->setFixedUp();
}
複製代碼

2.2 方法緩衝

所謂方法緩衝(cache),是使用哈希表保存類的最近被調用的方法,利用哈希表的 O(1) 的查詢時間複雜度提升方法調用的效率。objc_classcache_t cache成員就是類的方法緩衝。cache_t結構體代碼以下。其實是指向一個能夠動態擴容的 Key-Value 形式的哈希表,以方法名SEL爲Key,以方法IMP爲Value,分別對應bucket_t結構體的_sel_imp成員。cache_t結構體包含如下成員:

_buckets:保存哈希表全部元素的數組,元素類型爲bucket_t結構體,不是指向bucket_t的指針; _mask:間接表示哈希表的容量,爲所有位爲1的二進制數。哈希表容量最小爲4,知足公式:哈希表容量 = _mask + 1。擴容時設置_mask =(_mask + 1) & _mask_occupied:哈希表實際緩存的方法個數;

前文addMethod(...)的代碼中,調用flushCache(...)時用於清空指定類的方法緩衝。之因此清空是由於addMethod(...)可能會觸發方法IMP變動,必須保證方法緩衝中保存的全部方法SELIMP映射的正確性。關於方法緩衝的具體細節見 Runtime 源代碼objc-cache.mm不深刻介紹。

// 方法緩衝哈希表的元素,_sel爲key,_imp爲value
struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif

    ...

public:

    ...

};

// 方法緩衝的數據結構
struct cache_t {
    struct bucket_t *_buckets;  //保存哈希表全部元素的數組
    mask_t _mask;  // 間接表示哈希表的容量
    mask_t _occupied;  // 

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
複製代碼

3、方法的完整響應流程

調用方法經常使用的方式包括:

  • @interface定義的公開接口;
  • NSObjectperformSelector系列方法;
  • NSInvocation類觸發;
  • objc_msgSend(...)消息發送函數;

注意:<objc/message.h>公開的接口形式是無參的,能夠嘗試用相似BOOL (*msg)(Class, SEL, id) = (typeof(msg))objc_msgSend處理,作強制轉換後再調用msg函數指針以實現正常傳參,第一個參數是接收消息的對象(實例方法)/(類方法),第二個參數是SEL方法選擇器,後續可變長參數列表爲方法的參數列表

形式各有不一樣,可是本質上是作一樣的事情:首先轉化爲objc_msgSend(...)消息發送的形式,而後經過 targetselector 定位到方法的IMP,最後調用IMP指向的函數指針。因而關鍵點來到了方法IMP定位上。從class_getMethodImplementation(Class cls, SEL sel)源代碼能夠知道 Runtime 如何經過方法名SEL定位 方法的實現IMP。代碼以下:

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

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

    imp = lookUpImpOrNil(cls, sel, nil, 
                         YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

    // 繼承鏈無該SEL對應的IMP,則從消息轉發流程中搜索IMP
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

複製代碼

最後定位到關鍵代碼lookUpImpOrForward(...),顧名思義是用於在類中查找IMP,或者進入轉發。根據代碼邏輯,總結方法根據方法名SEL定位方法的實現IMP的主要處理流程以下:

  1. 在類的方法緩衝中搜索方法,搜索到則馬上返回;
  2. 判斷類是否已完成 class realizing,若未完成,則先進行 class realizing,由於在class realizing 完成以前,類的class_rw_t中的方法列表都有多是不完整的;
  3. 若類未初始化,則先執行類的initialize()方法,由於其中可能包含動態添加方法邏輯;
  4. 再次在類的方法緩衝中搜索方法,搜索到則馬上返回,由於均可能觸發方法緩衝更新;
  5. 遍歷類的method_array_t方法列表二維數組容器中的全部method_list_t,在method_list_t中搜索方法。若method_list_t已排序,則使用二分法搜索,若method_list_t未排序,則用簡單線性搜索。搜索到則馬上返回;
  6. 沿着繼承鏈在父類中遞歸地搜索方法。搜索到則馬上返回;
  7. 嘗試方法動態解析(在 3.1 中詳細介紹),若返回true,則回到第4步再次嘗試搜索;
  8. 若方法動態解析返回false,則觸發消息轉發流程。返回_objc_msgForward_impcache,返回該IMP表示須要進入消息轉發流程;

注意:initialize()方法和load()方法的做用有點類似,都是用於類的初始化。但二者在行爲上有很大區別:一、load()方法不具備繼承特性,繼承鏈上的全部類定義的全部load()方法均會被調用,load()方法中禁止使用super關鍵字;intialize()具備繼承特性,可以使用super關鍵字,其行爲至關於普通的類方法。二、load()方法在類加載時觸發,且 runtime 嚴格控制一個類的load()方法只會被執行一次;initialize()方法 在第一次調用類的方法時,在lookUpImpOrForward(...)中檢查類是否初始化時觸發,所以若父類實現了initialize()方法,可是其全部子類均未實現,則不管是第一次調用子類仍是父類的方法,都會觸發父類的initialize()方法。

lookUpImpOrForward(...)的源代碼看起來很長,可是基本上每一步都包含很關鍵的信息,對理解消息的響應流程很是重要。所以只刪除代碼中讀寫鎖、調試相關代碼,在代碼中作了詳細的註釋。

// Runtime預約義的統一的觸發方法動態解析的SEL,注意實際值應該並非NULL
SEL SEL_resolveInstanceMethod = NULL;

// Runtime預約義的標記進入消息轉發流程的IMP
extern void _objc_msgForward_impcache(void);

// 從cls類的`cache_t`方法緩存哈希表中查詢SEL對應的IMP
extern IMP cache_getImp(Class cls, SEL sel);

// 在cls類查詢SEL對應的IMP
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO;

    // 在方法緩衝中搜索方法
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // 方法固定相關操做
    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    // 若類的initialize()方法未調用,則先進行類的initialize初始化過程
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));  //調用類的initialize()方法
    }

 retry:
    // 若ARC編譯選項打開則須要忽略retain、release等方法,忽略
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }

    // 在類的cache_t方法緩存哈希表中查詢SEL對應的IMP
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 在類的method_array_t方法列表容器中查詢SEL對應的IMP
    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }

    // 沿着類的繼承鏈循環。在各級類及父類的方法緩衝、方法列表容器中查詢SEL對應的IMP
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // 在類的方法緩存中查詢
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // 若搜索到IMP,則將IMP寫入父類的方法緩存
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                break;
            }
        }

        // 在類的方法列表中查詢
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }

    // 嘗試動態解析方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);

        triedResolver = YES;
        goto retry;
    }

    // 觸發消息轉發流程
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    return imp;
}

// 當前類的方法列表中搜索方法
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;
}

// 簡單線性遍歷method_list_t搜索方法名爲sel的方法,查到馬上返回
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        //對於isFixedUp的方法列表用二分法搜索,這也是fixedUp時須要對方法列表排序的緣由
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 線性搜索,簡單遍歷
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}

// 用二分查找從已排序的method_list_t中搜索方法名爲key的方法
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
複製代碼

3.1 方法動態解析及消息轉發

當類及其繼承鏈的方法列表不存在SEL對應的方法時,會進入方法動態解析 以及 消息轉發流程,Runtime 只公佈了觸發邏輯,實現細節則徹底隱藏在objc_msgSend的實現中。觸發方法動態解析經過objc_msgSend向類發送SEL_resolveInstanceMethod消息,推測objc_msgSend內部只是直接調用類的resolveInstanceMethodresolveClassMethod方法查詢SEL是否有動態解析,有則返回true反之返回false

所謂動態方法解析實際上就是對不經常使用到的方法,在正式調用到該方法時纔將方法添加到類的方法列表。所以,resolveInstanceMethodresolveClassMethod中的代碼通常是對須要動態解析的SEL,調用class_addMethod(...)動態添加該SEL對應的方法,並返回YES。若是隻是返回YES的話,而不添加方法極可能形成的後果是responseToSelector雖然能夠返回YES,可是performSelector時仍然拋出unrecognized selector異常。

方法動態解析相關代碼以下。

// 動態解析方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // 動態解析實例方法
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // 動態解析類方法
        _class_resolveClassMethod(cls, sel, inst);
        
        // 這裏爲何要給元類再動態解析實例方法
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

// 動態解析類方法
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    // 若類未實現resolveClassMethod方法,則不走方法動態解析流程
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 觸發方法動態解析流程
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // lookUpImpOrNil觸發將動態解析的SEL與IMP的映射添加到類的方法緩存
    // 這樣下次調用SEL時可直接從cache_t中查詢到IMP
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
    }
}

// 動態解析實例方法
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 若類未實現resolveInstanceMethod方法,則不走方法動態解析流程
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 觸發方法動態解析流程
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // lookUpImpOrNil觸發將動態解析的SEL與IMP的映射添加到類的方法緩存
    // 這樣下次調用SEL時可直接從cache_t中查詢到IMP
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
}
複製代碼

注意:完成動態解析後,又當即調用了lookUpImpOrNil(...),其目的是將resolveInstanceMethodresolveClassMethod方法實現代碼中爲響應SEL而動態添加的IMP加入到類的方法緩衝,則下一次調用可直接從方法緩衝中獲取。

關於方法動態解析和消息轉發流程的內部實現代碼,Runtime 公開的源代碼並很少。尤爲是消息轉發流程的實現,基本隱藏在objc_msgSend函數的內部實現中。可是網上有不少關於 Runtime 消息轉發機制的博文,這裏只貼出方法動態解析及消息轉發流程的大體流程再也不贅述。圖中從左到右第一列是指,在類的繼承鏈上存在響應方法;第二列是進入方法動態解析流程(method resolution);第三列是進入消息快速轉發流程(fast forwarding);第四列是進入消息完整轉發流程(normal forwarding)。

消息動態解析及轉發流程.jpg

4、總結

  • class_ro_tbaseMethodList只保存了類的基本方法列表,只包含類定義時定義的方法,這些方法都是編譯時決議的; 類的class_rw_tmethods保存了類的完整方法列表,除了包含基本方法列表外,還包含運行時決議的,類的分類中定義的方法以及動態添加的方法;

  • class_rw_tmethods使用method_array_t二維數組容器保存,包含一個或多個method_list_t結構體,其中method_array_t的外層一位數組容器的最後一個元素爲指向class_ro_tbaseMethodList的指針;

  • 類的 class realizing 階段載入方法列表時,須要對全部方法列表根據SEL進行排序,這是爲了在搜索方法時能根據SEL進行二分法搜索提升方法搜索效率;

  • 調用class_addMethod(...)添加方法時,將方法封裝成method_list_t,並添加到class_rw_tmethods的開頭,其餘元素總體後移;

  • 類的方法緩衝緩存最近調用方法的SELIMP的映射,是可動態擴容的 Key-Value 形式的哈希表,可經過方法SEL查找IMP,引入方法緩衝可提升方法搜索的效率;

  • 調用方法的本質是經過objc_msgSend向 target 發送 selector 消息,target 響應消息時,前後在類的方法緩衝、類的方法列表、繼承鏈上全部類的方法緩衝和方法列表中搜索方法IMP,若方法無響應,則觸發方法動態解析過程,執行resolveInstanceMethodresolveClassMethod中的方法動態解析邏輯,若方法動態解析過程無響應,則進入消息轉發流程;

  • 下一篇介紹屬性列表的實現。

相關文章
相關標籤/搜索