Runtime原理探究(三)—— OC Class的方法緩存cache_t


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優(yōu)化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime

????本文篇幅比較長,創(chuàng)作的目的并不是為了在簡書上刷贊和閱讀量,而是為了自己日后溫習知識所用。如果有幸被你發(fā)現(xiàn)這篇文章,并且引起了你的閱讀興趣,請休息充分,靜下心來,精力充足地開始閱讀,希望這篇文章能對你有所幫助。如發(fā)現(xiàn)任何有誤之處,肯請留言糾正,謝謝。????

承接上一篇的內容,我們回過頭去看Class的定義

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable  方法緩存
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags  用于獲取具體的類信息

};

這里面還有一個cache_t cache沒有解讀過,一起來看一看這個東西。看名字很好理解,就是緩存的意思,緩存什么呢?——緩存方法。
它的底層是通過散列表(哈希表)的數(shù)據(jù)結構來實現(xiàn)的,用于緩存曾經調用過的方法,可以提高方法的查找速度。
首先,回顧一下正常情況下方法調用的流程。假設我們調用一個實例方法[bj XXXX];

  • obj -> isa -> objClass對象 -> method_array_t methods -> 對該表進行遍歷查找,找到就調用,沒找到繼續(xù)往下走
  • obj -> superclass -> obj的父類 -> isa -> method_array_t methods -> 對父類的方法列表進行遍歷查找,找到就調用,沒找到就重復本步驟
  • 找到就調用,沒找到重復流程
  • 找到就調用,沒找到重復流程
  • 找到就調用,沒找到重復流程
  • 直到NSObject -> isa -> NSObjectClass對象 -> method_array_t methods ......

如果XXXX方法在程序內會被頻繁的調用,那么這種逐層便利查找的方式肯定是效率低下的,因此蘋果設計了cache_t cache,當XXXX第一次被調用的時候,會按照常規(guī)流程查找,找到之后,就會被加入到cache_t cache中,當再次被調用的時候,系統(tǒng)就會直接現(xiàn)到cache_t cache來查找,找到就直接調用,這樣便大大提升了查找的效率。

剛才介紹了cache_t cache是通過散列表來實現(xiàn)的,下面就來著重分析一下,方法是如何被緩存的。散列/哈希表,想必大部分iOS開發(fā)這至少應該聽過,而我們常用的NSDictionary其實就是一種散列表數(shù)據(jù)結構。來看一下cache_t cache的定義

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
  • struct bucket_t *_buckets; —— 用來緩存方法的散列/哈希表
  • mask_t _mask; —— 這個值 = 散列表長度 - 1
  • mask_t _occupied; —— 表示已經緩存的方法的數(shù)量

上面介紹的_buckets散列表里面的存儲單元是bucket_t,來看看它包含了方法的什么信息

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}
  • cache_key_t _key; —— 這個key實際上就是方法的SEL,也就是方法名
  • IMP _imp; —— 這個就是方法對應的函數(shù)的內存地址

想一想我們平時是怎么使用NSDictionary的,通過一堆Key-Value鍵值對來進行存儲的,NSDictionary的底層就是散列表,這個剛才說過。方法緩存的時候,key就是上面的cache_key_t _key;,value就是上面的bucket_t結構體對象。

但是散列表的運作原理到底如何呢,這個屬于數(shù)據(jù)結構問題,這里簡要介紹一下。首先散列表本質上就是一個數(shù)組

在往散列表里面添加成員的時候,首先需要借助key計算出一個index,然后再將元素插入散列表的index位置
往散列表插值

那么從散列表里面取值就顯而易見了,根據(jù)一個key,計算出index,然后到散列表對應位置將值取出
根據(jù)key從散列表取值

這里的查詢方法的時候(也就是取值操作),時間復雜度為O(1), 對比我們一開始從方法列表的遍歷查詢,它的時間復雜度為O(n),因此通過緩存方法,可以極大的提高方法查詢的效率,從而提高了方法調用機制的效率。

根據(jù)key計算出index值的這個算法稱作散列算法,這個算法可以由你自己設計,總之目的就是盡可能減少不同的key得出相同index的情況出現(xiàn),這種情況被稱作哈希碰撞,同時還要保證得出的index值在合理的范圍。index越大,意味著對應的散列表的長度越長,這是需要占用實際物理空間的,而我們的內存是有限的。散列表是一種通過犧牲一定空間,來換取時間效率的設計思想。

我們通過key計算出的index大小是隨機的,無順序的,因此在方法緩存的過程中,插入的順序也是無順序的

而且可以預見的是,散列表里面再實際使用中會有很多位置是空著的,比如散列表長度為16,最終值存儲了10個方法,散列表長度為64,最終可能只會存儲40個方法,有一部分空間終究是要被浪費的。但是卻提高查找的效率。這既是所謂的空間換時間。

再介紹一下蘋果這里所采用的散列算法,其實很簡單,如下
index = @selector(XXXX) & mask 根據(jù)&運算的特點,可以得知最終index <= mask,而mask = 散列表長度 - 1,也就是說 0 <= index <= 散列表長度 - 1,這實際上覆蓋了散列表的索引范圍。而剛剛我們還提到過一個問題——哈希碰撞,也就是不同的key得到相同的index,該怎么處理呢?我們看一下源碼,在objc源碼里面搜索cache_t,可以發(fā)現(xiàn)一個跟查找相關的方法

bucket_t * cache_t::find(cache_key_t k, id receiver)  //根據(jù)key值 k 進行查找
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);   //通過cache_hash函數(shù)【begin  = k & m】計算出key值 k 對應的 index值 begin,用來記錄查詢起始索引
    mask_t i = begin; // begin 賦值給 i,用于切換索引
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) { 
              //用這個i從散列表取值,如果取出來的bucket_t的 key = k,則查詢成功,返回該bucket_t,
              //如果key = 0,說明在索引i的位置上還沒有緩存過方法,同樣需要返回該bucket_t,用于中止緩存查詢。
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
// 這一步其實相當于 i = i-1,回到上面do循環(huán)里面,相當于查找散列表上一個單元格里面的元素,再次進行key值 k的比較,
//當i=0時,也就i指向散列表最首個元素索引的時候重新將mask賦值給i,使其指向散列表最后一個元素,重新開始反向遍歷散列表,
//其實就相當于繞圈,把散列表頭尾連起來,不就是一個圈嘛,從begin值開始,遞減索引值,當走過一圈之后,必然會重新回到begin值,
//如果此時還沒有找到key對應的bucket_t,或者是空的bucket_t,則循環(huán)結束,說明查找失敗,調用bad_cache方法。

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

*********************************** cache_hash(k, m);
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

*********************************** cache_next(i, m)
static inline mask_t cache_next(mask_t i, mask_t mask) {
   // return (i-1) & mask;  // 非arm64
    return i ? i-1 : mask; // arm64
}

cache_t::find函數(shù)還被源碼里面的另一個函數(shù)調用過——cache_fill_nolock,緩存填充(插入)操作,源碼如下

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

上面源碼的最后一段以及它的注釋說明可以明白,當通過cache->find返回的bucket->key() == 0,就說明該位置上是空的,沒有緩存過方法,是一個unused slot(未使用的槽口),因此可以進行插入操作bucket->set(key, imp);,也就是將方法緩存到這個位置上。

根據(jù)上面的分析,下面用圖示來總結一下方法存入cache_t中,以及從cache_t中取方法的整體流程

向cache_t存入方法
(1)緩存bucket_t(key_A,IMP_A)
(2)緩存bucket_t(key_B,IMP_B)
(3)緩存bucket_t(key_C,IMP_C)
(4)緩存bucket_t(key_D,IMP_D)
從cache_t查詢方法
(1)查詢 SEL = key_A
(1)查詢 SEL = key_C
(1)查詢 SEL = key_D
(1)查詢 SEL = key_E

你可能還會有一個疑問,如果不斷的往緩存里添加方法,緩存滿了怎么辦?我們回到剛才看過的一段代碼cache_fill_nolock函數(shù),直接用截圖解讀一下

通過上面的解讀,可以知道,其實蘋果的做法是,在已緩存的方法數(shù)量達到當前緩存容量的3/4時候,就會出發(fā)擴容操作expand(),源碼如下

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

上面代碼里面uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;說的很明白,擴容就是將當前緩存容量* 2,如果是首次調用這個函數(shù),會使用一個初始容量值INIT_CACHE_SIZE來設定緩存容量

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

INIT_CACHE_SIZE的定義顯示它的值是4,也就是說蘋果給cache_t設定的初始容量是4。

你可能還會問,重置緩存之后,原來老緩存里面的內容還要不要呢,expand()函數(shù)里面調用的最后一個函數(shù)是reallocate(oldCapacity, newCapacity);,我們在進入它的源碼看看

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

很明顯,在最對舊的緩存空間進行了釋放,但是條件是freeOld = true,函數(shù)開頭給出了freeOld的由來,通過canBeFreed()函數(shù)獲得

bool cache_t::isConstantEmptyCache()
{
    return 
        occupied() == 0  &&  
        buckets() == emptyBucketsForCapacity(capacity(), false);
}


bool cache_t::canBeFreed()
{
    return !isConstantEmptyCache();
}

canBeFreed()函數(shù)其實很簡單,就是判斷一下緩存是不是空的,如果空的,舊沒必要釋放空間了,如果原來的緩存不是空的,就直接釋放掉,并且我們發(fā)現(xiàn),擴容的操作里面,并沒有對舊的緩存空間里面的內容進行復制保留,就是很粗暴的直接分配一塊新的緩存空間,然后直接釋放掉舊的緩存空間,這意味著,每次進行擴容操作之后,原來緩存過的方法就會全部丟失,而上面的cache_fill_nolock函數(shù)里面,在進行完expand()擴容操作之后,也僅僅是把當前處理的方法放到緩存空間里面,因此,擴容之前曾經被緩存過的方法,如果下次再次調用的話,有需要被重新緩存了。這里好好體會一下。

父類的方法被調用的時候,會如何緩存?

現(xiàn)在,我們知道,當對一個對象發(fā)送消息后,會通過對象的isa找到它的Class對象,在Class對象里面先從方法緩存cache_t查找該方法,沒有的話再對Class對象的方法列表進行遍歷查找,如果找到了方法,就進行緩存并且調用,那么這里肯定是將方法緩存到了該對象的Class對象的cache_t里面。

如果在當前Class對象里面沒有找到該方法,那么會通過Class對象的superclass進入其父類的Class對象里面,同樣,會先查找它的cache_t,如果沒有找到方法,會對其方法列表進行遍歷查找,問題就在這里,如果此時在方法列表里面找到了方法,進行緩存操作的時候,是會將方法存入當前父類的Class對象的cache_t里面呢,還是會存到接收消息的對象的Class對象的cache_t里面呢?

要搞清楚這個問題,首先可以看一下哪些地方調用了static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)函數(shù),因為它的參數(shù)里面?zhèn)魅肓艘粋€Class cls我們只需要搞清楚這個Class cls到底是誰。

根據(jù)下圖的操作進入上層調用函數(shù)cache_fill


用同樣方法查看一下cache_fill的上層調用函數(shù),如下圖

首先來看一下這個log_and_cache()
image.png
可以看到它實際上是被lockUpImpOrForward()函數(shù)調用的。

接下來我們在先看一下lookUpMethodInClassAndLoadCache()函數(shù)

很顯然,這個函數(shù)沒有處理superclass的問題,不是我們要找的。

最后在來看一下剩下的那個 lookUpImpOrForward函數(shù),下面代碼請看??????處標記的中文注解即可

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.-------------------------------->??????標準的IMP查找流程
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {//------------------------------>??????查詢當前Class對象的緩存,如果找到方法,就返回該方法
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.read();

    if (!cls->isRealized()) {//------------------------------>??????當前Class如果沒有被realized,就進行realize操作
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {//-------------->??????當前Class如果沒有初始化,就進行初始化操作
        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
    }

    
 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.//------------------------------>??????嘗試從該Class對象的緩存中查找,如果找到,就跳到done處返回該方法

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.//---------------->??????嘗試從該Class對象的方法列表中查找,找到的話,就緩存到該Class的cache_t里面,并跳到done處返回該方法
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.------>??????進入當前Class對象的superclass對象
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;//------>??????該for循環(huán)每循環(huán)一次,就會進入上一層的superclass對象,進行循環(huán)內部方法查詢流程
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.------>??????在當前superclass對象的緩存進行查找
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;//------>??????如果在當前superclass的緩存里找到了方法,就調用log_and_fill_cache進行方法緩存,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對象所對應的Class對象的cache_t中,然后跳到done處返回該方法
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;//---->??????如果緩存里找到的方法是_objc_msgForward_impcache,就跳出該輪循環(huán),進入上一層的superclass,再次進行查找
                }
            }
            
            // Superclass method list.---->??????如過畫緩存里面沒有找到方法,則對當前superclass的方法列表進行查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
            //------>??????如果在當前superclass的方法列表里找到了方法,就調用log_and_fill_cache進行方法緩存,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對象所對應的Class對象的cache_t中,然后跳到done處返回該方法
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.//------>??????如果到基類還沒有找到方法,就嘗試進行方法解析

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. //------>??????如果方法解析不成功,就進行消息轉發(fā)
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

關于上面再方法列表查找的函數(shù)Method meth = getMethodNoSuper_nolock(cls, sel);還需要說明一下,進入它的實現(xiàn)

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);//---??????核心函數(shù)
        if (m) return m;
    }

    return nil;
}

再進入其核心函數(shù)search_method_list(*mlists, 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)) {
        
        //---??????如果方法列表是經過排序的,則進行二分查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        //---??????如果方法列表沒有進行排序,則進行線性遍歷查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif
    return nil;
}
}

根據(jù)代碼中的邏輯,如果方法列表是經過排序的,會使用findMethodInSortedMethodList進行查找,而這里面實際上是用二分法進行查找的,具體代碼如下

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;
    //---??????count >>= 1相當于 count/=2,說明是從數(shù)組中間開始查找,也就是二分查找發(fā)
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

經過上述解讀,我們已經基本了解方法查詢和方法緩存所涉及到的細節(jié),現(xiàn)在可以把方法查找和方法緩存流程結合起來描述一下 Runtime消息機制 當中的 消息發(fā)送 流程

  • (1) 當一個對象接收到消息時[obj message];,首先根據(jù)objisa指針進入它的類對象cls里面。
  • (2) 在objcls里面,首先到緩存cache_t里面查詢方法message的函數(shù)實現(xiàn),如果找到,就直接調用該函數(shù)。
  • (3) 如果上一步沒有找到對應函數(shù),在對該cls的方法列表進行二分/遍歷查找,如果找到了對應函數(shù),首先會將該方法緩存到obj的類對象clscache_t里面,然后對函數(shù)進行調用。
  • (4) 在每次進行緩存操作之前,首先需要檢查緩存容量,如果緩存內的方法數(shù)量超過規(guī)定的臨界值(設定容量的3/4),需要先對緩存進行2倍擴容,原先緩存過的方法全部丟棄,然后將當前方法存入擴容后的新緩存內。
  • (5) 如果在objcls對象里面,發(fā)現(xiàn)緩存和方法列表都找不到mssage方法,則通過clssuperclass指針進入它的父類對象f_cls里面
  • (6) 進入f_cls后,首先在它的cache_t里面查找mssage,如果找到了該方法,那么會首先將方法緩存到消息接受者obj的類對象clscache_t里面,然后調用方法對應的函數(shù)。
  • (7) 如果上一步沒有找到方法,將會對f_cls的方法列表進行遍歷二分/遍歷查找,如果找到了mssage方法,那么同樣,會首先將方法緩存到消息接受者obj的類對象clscache_t里面,然后調用方法對應的函數(shù)。需要注意的是,這里并不會將方法緩存到當前父類對象f_cls的cache_t里面。
  • (8) 如果還沒找到方法,則會通過f_clssuperclass進入更上層的父類對象里面,按照(6)->(7)->(8)步驟流程重復。如果此時已經到了基類對象NSObject,仍沒有找到mssage,則進入步驟(9)
  • (9) 接下來將會轉到消息機制的 動態(tài)方法解析 階段
    消息發(fā)送流程

至此,OC Runtime里面的消息發(fā)送流程方法緩存策略就分析完畢。


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優(yōu)化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime

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