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

1. 回顧

在上篇博客iOS底層探索之Runtime:01—運行時&方法的本質(zhì)中介紹了 運行時編譯時的概念。同時也知道了OC方法的調(diào)用,本質(zhì)上是發(fā)送消息,在底層通過objc_msgSend方法來實現(xiàn)。那么底層是如何實現(xiàn)的呢?

在這里插入圖片描述

2. 消息發(fā)送底層如何實現(xiàn)

補充1

Runtime有兩個版本 ?個是Legacy版本(早期版本) ,另一個是Modern版本(現(xiàn)?版本)

  • 早期版本對應(yīng)的編程接?:Objective-C 1.0
  • 現(xiàn)?版本對應(yīng)的編程接?:Objective-C 2.0
  • 早期版本?于Objective-C 1.0, 32位Mac OS X的平臺上
  • 現(xiàn)?版本:iPhone程序和Mac OS X v10.5及以后的系統(tǒng)中的64 位程序

Objective-C Runtime Programming Guide

下面的代碼已經(jīng)不陌生了吧!調(diào)用效果都是一樣的,一個是上層OC的對象調(diào)用方法,一個是下層消息的發(fā)送。

JPStudent *stu = [[JPStudent alloc]init];
[stu test];
objc_msgSend(stu, sel_registerName("test"));

sel_registerName是一個C語言的方法,傳入一個C語言的字符串(其實就是我們的方法名稱)

objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)
sel_registerName(<#const char * _Nonnull str#>)
//C語言的函數(shù),闖入一字符串

sel_registerName("test") 等價于 @selector(test),我們可以打印下它們的地址。

NSLog(@"%p---%p",sel_registerName("test"),@selector(test));

//打印輸出
2021-06-29 12:58:50.610720+0800 方法的本質(zhì)探索[42704:741799] 0x7fff7b9f5ddc---0x7fff7b9f5ddc

從打印結(jié)果來看,是一模摸一樣樣??

666

補充2

OC中方法的調(diào)用,底層都是轉(zhuǎn)換為消息發(fā)送objc_msgSend函數(shù)的調(diào)用,執(zhí)行流程大概可以分為三大階段。

  1. 消息發(fā)送流程
  2. 動態(tài)方法解析流程
  3. 消息轉(zhuǎn)發(fā)流程

2.1 查找源碼

既然要看objc_msgSend的底層,就得去蘋果的源碼里面去看看,必須要深入底層去探索。

源碼工程查找objc_msgSend

源碼工程查找

我的天哪!什么鬼?????這么多文件,有匯編的,有C/C++的該看哪一個呢?而且架構(gòu)還不一樣。
我的天那

我們肯定是要找arm架構(gòu)的,不要問為什么,問就是找它就對了,哈哈!因為我們手機的真機是arm架構(gòu)的,加上OC的底層都是C、C++和匯編實現(xiàn)的,所以我們基本可以定位到objc-msg-arm64.s這個文件。

objc-msg-arm64.s

2.2 查看源碼

既然找到了,就不要在外面停留了,進去看看。

查看源碼.png

偶買噶,我的天那!這是熟悉又陌生(大學(xué)學(xué)過)的匯編??!惡魔??,噩夢??!大學(xué)學(xué)的時候就很懵!

苦澀

靚仔,穩(wěn)住,挺住!
匯編確實是比較難啃,但也不是啃不動,一口吃不下,就慢慢啃!干,就完了!

加油

3. 分析匯編

匯編源碼是從ENTRY _objc_msgSend開始,到END_ENTRY _objc_msgSend結(jié)束。

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

3.1 _objc_msgSend

  1. p0 和空對比,即判斷接收者是否存在,其中p0objc_msgSend的第一個參數(shù)-消息接收者receiver
  2. if else 判斷,如果支持tagged pointer,跳轉(zhuǎn)至LNilOrTagged,如果小對象為空,則直接返回空,即LReturnZero。如果小對象不為空,則處理小對象的isa,走到 CacheLookup NORMAL
  3. GetClassFromIsa_p16是定義的一個宏,通過isa找到對應(yīng)的類,ExtractISA也是個宏定義,將傳入的isa&isaMask,得到class,并將class賦給p16
  • GetClassFromIsa_p1 的宏定義
// p13(isa), 1, x0(isa)
//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
    mov p16, \src           // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#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
  • ExtractISA宏定義
.macro ExtractISA
and    $0, $1, #ISA_MASK
.endmacro

3.2 CacheLookUp

  • CacheLookUp核心代碼

// NORMAL, _objc_msgSend, __objc_msgSend_uncached ,  MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart\Function label we may have
    //   loaded an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd\Function,
    //   then our PC will be reset to LLookupRecover\Function which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
    
    mov x15, x16            // stash the original isa
LLookupStart\Function:
    // p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    ldr p10, [x16, #CACHE]              // p10 = mask|buckets
    lsr p11, p10, #48           // p11 = mask
    and p10, p10, #0xffffffffffff   // p10 = buckets
    and w12, w1, w11            // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE]          // p11 = mask|buckets
    #if CONFIG_USE_PREOPT_CACHES
        #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      // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else

//  p11 cache -> p10 = buckets
//  p11, LSR #48 -> mask
//  p1(_cmd) & mask = index -> p12
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask

#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11           // p11 = mask = 0xffff >> p11
    and p12, p1, p11            // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

// objc - 源碼調(diào)試 + 匯編
//  p11 cache -> p10 = buckets
//  p1(_cmd) & mask = index -> p12
//  (_cmd & mask) << 4  -> int 1 2 3 4 5   地址->int
//  buckets +  內(nèi)存平移 (1 2 3 4)
//  b[i] -> b + i
//  p13 當(dāng)前查找bucket
    add p13, p10, p12, LSL #(1+PTRSHIFT)
                        // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

                        // do {
//  *bucket--  p17, p9
//  bucket 里面的東西 imp (p17) sel (p9)
//  查到的 sel (p9) 和我們 say1
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

    // wrap-around:
    //   p10 = first bucket
    //   p11 = mask (and maybe other bits on LP64)
    //   p12 = _cmd & mask
    //
    // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
    // So stop when we circle back to the first probed bucket
    // rather than when hitting the first bucket again.
    //
    // Note that we might probe the initial bucket twice
    // when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                        // p13 = buckets + (mask << 1+PTRSHIFT)
                        // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p13, p10, p11, LSL #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                        // p12 = first probed bucket

                        // do {
4:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel == _cmd)
    b.eq    2b              //         goto hit
    cmp p9, #0              // } while (sel != 0 &&
    ccmp    p13, p12, #0, ne        //     bucket > first_probed)
    b.hi    4b

LLookupEnd\Function:
LLookupRecover\Function:
    b   \MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
    and p10, p11, #0x007ffffffffffffe   // p10 = buckets
    autdb   x10, x16            // auth as early as possible
#endif

    // x12 = (_cmd - first_shared_cache_sel)
    adrp    x9, _MagicSelRef@PAGE
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    sub p12, p1, p9

    // w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    lsr x17, x11, #55           // w17 = (hash_shift, ...)
    lsr w9, w12, w17            // >>= shift

    lsr x17, x11, #60           // w17 = mask_bits
    mov x11, #0x7fff
    lsr x11, x11, x17           // p11 = mask (0x7fff >> mask_bits)
    and x9, x9, x11         // &= mask
#else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr x17, x11, #48           // w17 = (hash_shift, hash_mask)
    lsr w9, w12, w17            // >>= shift
    and x9, x9, x11, LSR #53        // &=  mask
#endif

    ldr x17, [x10, x9, LSL #3]      // x17 == sel_offs | (imp_offs << 32)
    cmp x12, w17, uxtw

.if \Mode == GETIMP
    b.ne    \MissLabelConstant      // cache miss
    sub x0, x16, x17, LSR #32       // imp = isa - imp_offs
    SignAsImp x0
    ret
.else
    b.ne    5f              // cache miss
    sub x17, x16, x17, LSR #32      // imp = isa - imp_offs
.if \Mode == NORMAL
    br  x17
.elseif \Mode == LOOKUP
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    SignAsImp x17
    ret
.else
.abort  unhandled mode \Mode
.endif

5:  ldursw  x9, [x10, #-8]          // offset -8 is the fallback offset
    add x16, x16, x9            // compute the fallback isa
    b   LLookupStart\Function       // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro
  1. 通過cache首地址平移16字節(jié)(因為在objc_class中,首地址距離cache正好16字節(jié),即isa8字節(jié),superClass8字節(jié)),獲取cahce,cache中高16位存mask,低48位存buckets,即p11 = mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        ldr p11, [x16, #CACHE]  // p11 = mask|buckets

  1. cache中分別取出bucketsmask,并由mask根據(jù)哈希算法計算出哈希下標(biāo)。在arm64環(huán)境下,maskbuckets放在一起共占用8個字節(jié),64位;其中mask在高16位,buckets在低48位。通過掩碼(0x0000fffffffffffe)與運算(&)將高16位抹零獲取buckets;將buckets賦值給p10。將cache右移48位,得到mask,即p10 = buckets。
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz    p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff   // p10 = buckets
#else 
// 走該流程獲取buckets
and p10, p11, #0x0000fffffffffffe   // p10 = buckets
tbnz    p11, #0, LLookupPreopt\Function
#endif 
// 此部分就位cache_hash算法
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff   // p10 = buckets
and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
  1. objc_msgSend的參數(shù)p1(即第二個參數(shù)_cmd& msak,通過哈希算法,得到需要查找存儲sel-impbucket下標(biāo)index,即p12 = index = _cmd & mask。這是因為系統(tǒng)在存儲sel-imp時,就是通過哈希計算得到下標(biāo),再去存儲,所以讀取也需要通過同樣的方式。
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

通過首地址 + 實際偏移量,獲取哈希下標(biāo)index對應(yīng)的bucket

  1. 知道了下標(biāo),buckets的首地址也有了,那么怎么找到_cmd的位置呢?我們都知道可以通過內(nèi)存地址平移,在bucket_t中存放的是impsel,8+8=16個字節(jié)。
add p13, p10, p12, LSL #(1+PTRSHIFT)
   // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

根據(jù)buckets首地址偏移下標(biāo) 乘以16個單位,其中PTRSHIFT = 3,相當(dāng)于下標(biāo)左移4 就是以16的倍數(shù)進行平移,然后加上buckets首地址的話,就獲得了當(dāng)前_cmd對應(yīng)的bucket地址。根據(jù)獲取的bucket,取出其中的sel存入p17,即p17 = sel,取出imp存入p9,即p9 = imp。

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
  1. cmp p9, p1,如果當(dāng)前獲取的sel與要查找的sel相同,則緩存命中,CacheHit。
  2. 如果不相等,則進入3流程中,判斷當(dāng)前獲取的sel,p9是否為空,如果為空,則Miss,緩存沒有命中。
  3. 如果獲取的sel不為空,說明存在下標(biāo)沖突,則以當(dāng)前獲取的bucket的地址與首個bucket的地址進行比較如果獲取地址,大于等于首地址,繼續(xù)比較流程,向前查找,循環(huán)下去!直到查詢到首地址位置。
  4. 如果上面的循環(huán)結(jié)束依然沒有找到,則會進入下面的流程,CACHE_MASK_STORAGE_HIGH_16環(huán)境下,同樣p11右移48位獲取mask,而mask等于開辟的總空間容量減1,所以獲取最后一個存儲空間所在的位置,也即是首地址的基礎(chǔ)上,添加mask*16的位置,所以這里p13就是當(dāng)前最大的那個存儲空間,也就是最后一個存儲空間。
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
        add p13, p10, w11, UXTW #(1+PTRSHIFT)
                                                // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                                                // p13 = buckets + (mask << 1+PTRSHIFT)
                                                // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        add p13, p10, p11, LSL #(1+PTRSHIFT)
                                                // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
     

上面已經(jīng)知道p12是要查找方法_cmd的存儲下標(biāo),只要把首地址加上偏移地址index*16,就可以知道_cmd對應(yīng)bucket地址,并賦值給p12。

add p12, p10, p12, LSL #(1+PTRSHIFT)
  // p12 = first probed bucket

此次循環(huán)是從最后一個位置,查找的_cmd對應(yīng)位置,進行向前查找

#endif
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                        // p12 = first probed bucket

                        // do {
4:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel == _cmd)
    b.eq    2b              //         goto hit
    cmp p9, #0              // } while (sel != 0 &&
    ccmp    p13, p12, #0, ne        //     bucket > first_probed)
    b.hi    4b

LLookupEnd\Function:
LLookupRecover\Function:
    b   \MissLabelDynamic

cmp p9, p1,如果當(dāng)前獲取的sel與要查找的sel相同,跳轉(zhuǎn)至流程2,即緩存命中,CacheHit

如果不相等,判斷sel是否為空,如果不為空,并且循環(huán)獲取的地址大于p12的位置,繼續(xù)循環(huán)流程。

如果以上流程均未能命中緩存,則進入MissLabelDynamic流程

3.3 CacheHit

下面是緩存命中(CacheHit)的分析

 // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
 .macro CacheHit
 .if $0 == NORMAL
         TailCallCachedImp x17, x10, x1, x16    // authenticate and call imp
 .elseif $0 == GETIMP
         mov    p0, p17
         cbz    p0, 9f          // don't ptrauth a nil imp
         AuthAndResignAsIMP x0, x10, x1, x16    // authenticate imp and re-sign as IMP
 9: ret             // return IMP
 .elseif $0 == LOOKUP
         // No nil check for ptrauth: the caller would crash anyway when they
         // jump to a nil IMP. We don't care if that jump also fails ptrauth.
         AuthAndResignAsIMP x17, x10, x1, x16   // authenticate imp and re-sign as IMP
         cmp    x16, x15
         cinc   x16, x16, ne            // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
         ret                // return imp via x17
 .else
 .abort oops
 .endif
 .endmacro

 // 調(diào)用imp
 .macro TailCallCachedImp
         // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
         eor    $0, $0, $3
         br $0
 .endmacro

CacheLookup中,Mode傳入的為NORMAL,會執(zhí)行TailCallCachedImp,在TailCallCachedImp實現(xiàn)中進行了位異或運算,獲取imp。因為在存儲imp時,對imp進行了編碼處理,取出執(zhí)行調(diào)用時,需要進行解碼操作。

如果緩存沒有命中,則會進入MissLabelDynamic流程。全局搜索MissLabelDynamic,發(fā)現(xiàn)MissLabelDynamic即為CacheLookUp的第三個參數(shù)

// NORMAL, _objc_msgSend, __objc_msgSend_uncached ,  MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

就是_objc_msgSend中傳入的__objc_msgSend_uncached

_objc_msgSend

源碼工程里面全局搜索__objc_msgSend_uncached
__objc_msgSend_uncached

分析:
在該函數(shù)中執(zhí)行宏MethodTableLookup,繼續(xù)跟蹤MethodTableLookup,在MethodTableLookup的匯編實現(xiàn)中,我們可以看到最重要的_lookUpImpOrForward的方法,然后全局搜索_lookUpImpOrForward發(fā)現(xiàn)搜不到實現(xiàn)方法, 說明該方法并不是匯編實現(xiàn)的,需要去C/C++源碼中查找。

到此,消息發(fā)送流程中匯編快速查找的分析就結(jié)束了,因為lookUpImpOrForward不是匯編實現(xiàn)的,是C/C++實現(xiàn)的,所以屬于(慢速查找)。lookUpImpOrForward慢速查找下次再分析。

4. 總結(jié)

  1. 為什么底層不用C或者C++用匯編?
  • 匯編更接近機器語言,直接操作寄存器,查找效率高
  • 因為一些方法的參數(shù)未知,匯編可以處理未知的參數(shù),更加動態(tài)化一點
  1. objc_msgSend函數(shù)的調(diào)用,執(zhí)行流程大概可以分為三大階段。
  • 消息發(fā)送流程(1.匯編快速查找,2.慢速查找)
  • 動態(tài)方法解析流程
  • 消息轉(zhuǎn)發(fā)流程
  1. 消息發(fā)送流程流程圖:


    發(fā)送消息流程圖

更多內(nèi)容持續(xù)更新

?? 喜歡就點個贊吧????

?? 覺得學(xué)習(xí)到了的,可以來一波,收藏+關(guān)注,評論 + 轉(zhuǎn)發(fā),以免你下次找不到我????

??歡迎大家留言交流,批評指正,互相學(xué)習(xí)??,提升自我??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容