iOS底層探索之Runtime(三): lookUpImpOrForward慢速查找分析

1. 回顧

iOS底層探索之Runtime(一):運行時&方法的本質 算法

iOS底層探索之Runtime(二): objc_msgSend&彙編快速查找分析編程

在前面的文章中介紹了消息發送(objc_msgSend)流程,主要是彙編快速查找cache的過程,並對彙編源碼進行了分析,本章內容主要分析慢速查找_lookUpImpOrForward流程。緩存

2. _lookUpImpOrForward

在彙編的快速查找過程當中若是沒有找到緩存,就會進入__objc_msgSend_uncached裏面,在__objc_msgSend_uncached 最主要是對MethodTableLookup的處理。markdown

2.1 MethodTableLookup

MethodTableLookup

  • x0寄存器裏面存的是imp,並賦值給x17x0是第一個寄存器也是返回值的存儲位置,若是impx0裏面,必將作一件事情,就是返回,那麼結果必定是在bl _lookUpImpOrForward執行後的返回值裏面,也就是咱們要找的imp存儲的地方,因此接下來的重點就是_lookUpImpOrForward
  • blb是跳轉,l是連接寄存器,將下一條指令的地址保存到lr寄存器中,也就是把(mov x17, x0)的指令地址保存在lr中,當_lookUpImpOrForwar執行完之後,執行lr寄存器中的地址。
  • _lookUpImpOrForward找到imp賦值給x17寄存器

_lookUpImpOrForward在源碼裏面沒有找到彙編的實現,由於_lookUpImpOrForward不是彙編寫的,是C++寫的,因此去掉下劃線就能夠搜索🔍找到了less

lookUpImpOrForward

lookUpImpOrForward的函數實現裏面,確實發現了lookUpImpOrForward返回的是imp,也就有驗證了上面👆的彙編分析ide

lookUpImpOrForward返回imp

緩存找不到了,就會進入慢速查找流程,遍歷method_list方法列表,遍歷是個耗時間的流程,因此就放入了C++中實現,下面重點分析lookUpImpOrForward👇函數

2.2 isKnownClass

lookUpImpOrForward -> checkIsKnownClass(cls) -> checkIsKnownClass -> isKnownClassoop

  • 查詢當前的類是否註冊到緩存列表中
isKnownClass(Class cls)
{
    if (fastpath(objc::dataSegmentsRanges.contains(cls->data()->witness, (uintptr_t)cls))) {
        return true;
    }
    auto &set = objc::allocatedClasses.get();
    return set.find(cls) != set.end() || dataSegmentsContain(cls);
}
複製代碼

lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked -> realizeClassMaybeSwiftAndLeaveLocked -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift學習

  • rwro 進行處理
auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }

    cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
複製代碼
  • 類,元類是否初始化註冊

realizeClassWithoutSwift

  • 爲何要對類,元類進行初始化呢?

我在iOS底層探索之類的結構(上):ISA文章中已經介紹了isa的走位,和元類的繼承關係。當對象調用方法的時候,判斷當前類是否初始化,父類、元類是否初始化。目的是,若是當前類中沒有實現方法,就去父類查找。若是元類中沒有實現類方法,就去根元類查找。遞歸操做,遍地開花。ui

究竟是怎麼遞歸,怎麼循環找方法的呢?請耐心往下看

在這裏插入圖片描述

  • lookUpImpOrForward -> for循環流程

for循環

個人天哪!開什麼玩笑啊?這是循環嗎?不要蒙我,我但是學過編程的人啊,循環有三個條件語句的啊!這就一個,後面兩個都沒有啊!

靚仔,你沒有看錯,這確實是循環,死循環!

圖不重要

這真的是for循環,只是循環體裏面,有gotobreak等語句打破死循環

3. 慢速查找分析

3.1 慢速查找流程大綱

  1. 查找本身方法列表Method_list -> sel-imp
  2. 父類中查找 -> NSObject -> nil -> 跳出循環

大概就是這麼個流程,那麼咱們下面去驗證下

3.2 分析源碼

進入for循環首先就是一個if判斷,是否有共享緩存。

if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
複製代碼

爲何又要去緩存裏面查找啊?以前不是已經彙編查找過了啊?由於在操做ro/rw的時候有可能寫入了新的方法,因此這時候再去查看一遍,以防萬一。

那麼若是沒有寫入呢?沒有就沒有唄!那就繼續往下執行代碼。

// curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
複製代碼
  • getMethodNoSuper_nolock
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

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

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
複製代碼

3.3 二分查找分析

在分析以前咱們玩個小遊戲,《猜猜猜》

在一次戶外活動中,有編號1到100的盒子,其中有一個裏面有獎品,猜是幾號盒子有獎品,一共有五次機會。是你,你會怎麼猜呢?下面是活動中勝出的猜想方法。

第一次猜: RENO:50 KC:小了

第二次猜: RENO:75 KC:大了

第三次猜: RENO:60 KC:大了

第四次猜: RENO:55 KC:對了

KC: 一共五次機會,第四次就猜中了,厲害啊! 厲害

這就是著名的二分查找法(Binary Search),也叫折半查找。下面源碼裏面findMethodInSortedMethodList方法就是經過這種算法實現的,可能剛剛那個遊戲,你還無感知二分查找的魅力,看完下面👇的分析,你就能感知了。

search_method_list_inline - > findMethodInSortedMethodList - > 非M1電腦的找big的

findMethodInSortedMethodList

  • findMethodInSortedMethodList
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) 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)getName(probe);
        
        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)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
複製代碼
  • 假如方法count的個數爲8count >>= 1就是右移1位,至關於二進制1000,變成0100count =4

  • probe = base + (count >> 1): 就是首地址base加上偏移

  • if (keyValue == probeValue): 兩個值相等的時候,if 判斷裏面的while循環是對分類方法進行處理,類和分類有可能同時實現了相同的方法,probe--就是取分類的方法,由於排好序了,分類方法是排在前面一個位置,最後return &*probe 返回方法的地址。

  • if (keyValue > probeValue): 大於中間值的狀況,base = probe + 1,就是4 + 1base等於5count--以後count變爲7,進入下一次循環

  • 7右移變爲3,完美的避開了4,由於4已經比較過了,這是巧合嗎?這就是算法的魅力,我只能說蘋果牛逼

蘋果牛逼

  • 上面已經執行過一次了,4不符合,那麼範圍縮小到58區間,那麼只能取6或者7進行比較了
  • 循環的話又執行probe = base + (count >> 1)base上面算過了等於5count等於3,再count >> 1以後count = 1probe就等於6。到這裏我直呼,好傢伙,好牛逼啊!完美的卡在了區間內。count >> 1兩次卡的這麼完美,我只能再一次說蘋果牛逼,佩服!佩服啊!

蘋果然牛逼 蘋果工程師把二分查找,用到了極致啊!不愧是世界第一市值的牛逼公司!

方法緩存查找到了就執行,goto done

// curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
複製代碼

既然找到方法了,不可能再去執行二分查找了,就會調用log_and_fill_cache方法,把它寫入緩存中,提升下次查找速度。

  • log_and_fill_cache
done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
複製代碼
  • insert插入緩存
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}
複製代碼

4.總結

  • 彙編的快速查找若是找不到緩存,就會進入__objc_msgSend_uncached,再到_lookUpImpOrForward
  • _lookUpImpOrForward爲何不用匯編實現呢?

遍歷method_list方法列表,是個耗時間的流程,因此就放入了C++中實現。

  • findMethodInSortedMethodList使用二分查找算法,提升查找效率

更多內容持續更新

🌹 喜歡就點個贊吧👍🌹

🌹 以爲學習到了的,能夠來一波,收藏+關注,評論 + 轉發,以避免你下次找不到我😁🌹

🌹歡迎你們留言交流,批評指正,互相學習😁,提高自我🌹

相關文章
相關標籤/搜索