iOS objc_msgSend流程分析

找到objc_msgSend

調用方法多艇,打斷點

圖片一.png

通過匯編發(fā)現(xiàn)調用objc_msgSendstep into攒至,發(fā)現(xiàn)objc源碼里面實現(xiàn)
圖片二.png

通過objc源碼找到objc_msgSend 的實現(xiàn)入口,其中不同的架構有不同的實現(xiàn)躁劣,這里我們主要看arm64
圖片三.png

匯編分析

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend
  1. 先進入ENTRY _objc_msgSend方法
  2. cmp p0, #0檢查對象是否為空p0 上是對象本身迫吐, cmp比較
  3. 如果為空則跳轉到LReturnZerob.le小于等于并跳轉账忘,b.eq等于并跳轉
    4.ldr p13, [x0]志膀,讀取x0寄存器的值,賦值給p13鳖擒,這里的p13實際上是lisa
    5.GetClassFromIsa_p16 p13, 1, x0溉浙,通過isa 獲取到對象的class
    6.CacheLookup通過緩存查找

GetClassFromIsa_p16分析

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA
    // Indexed isa
...省略
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    mov p16, \src
.else
    // 64-bit packed isa
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov p16, \src
#endif
.endmacro

INDEXED_ISA表示直接是一個raw isa,PACKED_ISA,需要通過一個掩碼取獲取蒋荚,那么這段代碼實際上只是調用了 ExtractISA 方法戳稽,
進入ExtractISA


#if __has_feature(ptrauth_calls)
.macro ExtractISA
    and $0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
    xpacd   $0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
    mov x10, $2
    movk    x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
    autda   $0, x10
#endif
.endmacro
#else
.macro ExtractISA
    and    $0, $1, #ISA_MASK
.endmacro
  • __has_feature(ptrauth_calls): 是判斷編譯器是否支持指針身份驗證功能
  • ptrauth_calls 指針身份驗證,針對arm64e架構期升;使用Apple A12或更高版本A系列處理器的設備(如iPhone XS惊奇、iPhone XS Max和iPhone XR或更新的設備)支持-arm64e架構
    上面的機型的判斷并不影響閱讀源碼,最主要還是and $0, $1, #ISA_MASK吓妆,
    0 表示p16赊时,1表示src,也就是p13行拢,and是與運算祖秒,p16 = p13 & ISA_MASK
    ISA_MASK 是isa里面宏定義的掩碼,所以p16就是對象的class指針

CacheLookup分析

CacheLookup,這個方法是快速查找的核心
首先一進來會有架構的判斷竭缝,我們找到真機的方法

 CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE]  
  • x16class的指針房维,CACHE的大小為16個字節(jié),也就是p11位cache位置的指針
#if __has_feature(ptrauth_calls)
    tbnz    p11, #0, LLookupPreopt\Function
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
#else
    and p10, p11, #0x0000fffffffffffe   // p10 = buckets
    tbnz    p11, #0, LLookupPreopt\Function
#endif
    eor p12, p1, p1, LSR #7
    and p12, p12, p11, LSR #48  
  • 我們以 A12為準抬纸,tbnz p11, #0, LLookupPreopt\Function咙俩,tbnz:表示如果測試位不為0則跳轉到LLookupPreopt方法,LLookupPreopt這個表示從共享緩存去加載(UIKit湿故,F(xiàn)oundation)阿趁,在這里不做過多的分析,
  • and p10, p11, #0x0000ffffffffffff 表示p10 = p11 & 0x0000ffffffffffff, 那么p10表示bucket首地址
  • eor p12, p1, p1, LSR #7坛猪, and p12, p12, p11, LSR #48這兩句實際上通過 sel 拿到bucket index, p1表示sel脖阵,LSR右移,eor異或操作墅茉,p12 ,其實是bucket的索引
    add p13, p10, p12, LSL #(1+PTRSHIFT)
                        // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
                        // do {
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel != _cmd) {
    b.ne    3f              //         scan more
                        //     } else {
2:  CacheHit \Mode              // hit:    call or return imp
                        //     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
    b.hs    1b
  • add p13, p10, p12, LSL #(1+PTRSHIFT) ,p13 = p10 + p12 << 4,
    那么p13 實際上就是當前發(fā)送方法的 bucket命黔, bucket存儲的是{imp,sel}
  • 將imp,sel分別保存到p17 和p9就斤,然后遍歷buckets去對比悍募,如果發(fā)現(xiàn)有和調用的sel和buckets有相同的,就直接返回imp洋机,也就是CacheHit緩存命中坠宴,
  • 如果遍歷都沒有找到,即cbz p9位0,即sel為空槐秧,那么跳轉到MissLabelDynamic啄踊,即沒有找到。
    用一張圖總結bucket查找過程:
    bucket查找.png

緩存沒有找到

緩存沒有找到則會進入__objc_msgSend_uncached

CacheLookup NORMAL, _objc_msgSend,__objc_msgSend_uncached

繼續(xù)往下走:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached

發(fā)現(xiàn)會調用MethodTableLookup刁标,最后會調用lookUpImpOrForward方法颠通,發(fā)現(xiàn)
lookUpImpOrForward在匯編里面沒有找到實現(xiàn),發(fā)現(xiàn)實現(xiàn)在objc-runtime-new.mm里面膀懈,在此就近入方法的慢速查找流程顿锰。

lookUpImpOrForward分析

準備工作

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//準備工作
  if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }
   checkIsKnownClass(cls);
  cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

//循環(huán)查找
 for (unsigned attempts = unreasonableClassCount();;){
}
 return imp;
}
  • cls->isInitialized() 檢測類是否初始化
  • checkIsKnownClass 檢測類是否已經注冊
  • realizeAndInitializeIfNeeded_locked檢測類是否已經實現(xiàn),沒有實現(xiàn)會遞歸去實現(xiàn)父類和元類

二分查找流程(重點)

循環(huán)查找

for (unsigned attempts = unreasonableClassCount();;) {
        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
        } else {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
  • cache.isConstantOptimizedCache先從緩存中判斷是否存在启搂,如果存在直接從緩存中取
  • getMethodNoSuper_nolock硼控,如果沒有找到從類的methods里面取找

getMethodNoSuper_nolock分析

 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;
    }
  • 可以看出methods是一個二維數(shù)字,methods-> mlists
  • 真正的查找是在mlists里面
    繼續(xù)進入search_method_list_inline
    在這個里面有兩個方法胳赌,
  1. findMethodInSortedMethodList已經拍好序的
  2. findMethodInUnsortedMethodList沒有排好序的
    我們知道如果要進行二分查找牢撼,數(shù)組必須有序绰上,如果是沒有排序的直接遍歷數(shù)組去找桶错。
    沒有排序,循環(huán)遍歷
findMethodInUnsortedMethodList(SEL sel, const method_list_t *list, const getNameFunc &getName)
{
    for (auto& meth : *list) {
        if (getName(meth) == sel) return &meth;
    }
    return nil;
}

排序的,二分查找:

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;
}
  • 蘋果的工程是用的很巧妙课锌,通過右移來實現(xiàn)折半查找
  • 這里有一個主意的點赏淌,就是我們發(fā)現(xiàn)即使找到了還有一個循環(huán)筛严,probe--腮考,這里實際上是因為如果有分類的方法和類的方法一樣的話,會優(yōu)先使用分類的方法曲横,This is required for correct category overrides.注釋里面也給出了解釋

如果自身的類沒有找到喂柒,就會查找父類,一值到跟類禾嫉。

 if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }

如果都沒有找到就會走消息轉發(fā)forward_imp

總結

上面就是方法的二分查找灾杰,其實消息的發(fā)送還沒有結束,如果自身類熙参,父類吭露,根類(NSObject)都沒有找到就會走轉發(fā)流程也就是forward_imp

補充(initialize)

還有一個關于initialize方法的調用,我們再調用其他方法是也可能會走initialize方法,這個方法在我們平時的開發(fā)中也是比較熟悉的
objc源碼中可以看到如下的調用順序
lookupimporforward-> realizeAndInitializeIfNeeded_locked-> initializeAndLeaveLocked-> initializeAndMaybeRelock-> initializeNonMetaClass-> callInitialize-> initialize

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末尊惰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泥兰,更是在濱河造成了極大的恐慌弄屡,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞋诗,死亡現(xiàn)場離奇詭異膀捷,居然都是意外死亡,警方通過查閱死者的電腦和手機削彬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門全庸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人融痛,你說我怎么就攤上這事壶笼。” “怎么了雁刷?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵覆劈,是天一觀的道長。 經常有香客問我沛励,道長责语,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任目派,我火速辦了婚禮坤候,結果婚禮上,老公的妹妹穿的比我還像新娘企蹭。我一直安慰自己白筹,他們只是感情好智末,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著遍蟋,像睡著了一般吹害。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上虚青,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天它呀,我揣著相機與錄音,去河邊找鬼棒厘。 笑死纵穿,一個胖子當著我的面吹牛,可吹牛的內容都是我干的奢人。 我是一名探鬼主播谓媒,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼何乎!你這毒婦竟也來了句惯?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤支救,失蹤者是張志新(化名)和其女友劉穎抢野,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體各墨,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡指孤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了贬堵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恃轩。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖黎做,靈堂內的尸體忽然破棺而出叉跛,到底是詐尸還是另有隱情,我是刑警寧澤引几,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布昧互,位于F島的核電站,受9級特大地震影響伟桅,放射性物質發(fā)生泄漏敞掘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一楣铁、第九天 我趴在偏房一處隱蔽的房頂上張望玖雁。 院中可真熱鬧,春花似錦盖腕、人聲如沸赫冬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽劲厌。三九已至膛薛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間补鼻,已是汗流浹背哄啄。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留风范,地道東北人咨跌。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像硼婿,于是被迫代替她去往敵國和親锌半。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容