Objc 的方法調(diào)用是運(yùn)行時(shí)決定的,系統(tǒng)會(huì)根據(jù) selector 動(dòng)態(tài)地查找 IMP,那么這一過(guò)程究竟是怎樣實(shí)現(xiàn)的?selector 是如何與 IMP 對(duì)應(yīng)起來(lái)的?面對(duì)應(yīng)用內(nèi)部成千上萬(wàn)次的函數(shù)調(diào)用,它會(huì)不會(huì)造成性能瓶頸?基于這些疑問(wèn),本文將會(huì)從源碼的角度來(lái)探究 Objc 的消息派發(fā)流程。
一切都源于 objc_msgSend
在 Objc 中我們這樣去調(diào)用函數(shù):
[obj func1]
但是編譯器會(huì)將其翻譯成如下代碼:
objc_msgSend(obj, @selector(func1));
objc_msgSend 是處理方法調(diào)用的核心,Objc 中的函數(shù)調(diào)用都交由 objc_msgSend 執(zhí)行,它會(huì)啟動(dòng)整個(gè)消息派發(fā)流程,尋找與 selector 相對(duì)應(yīng)的函數(shù)實(shí)現(xiàn),所以我們?cè)创a探究的切入點(diǎn)就是 objc_msgSend。從這里可以下載到 objc 的源碼,本文用的是 objc4-680 版本。
objc_msgSend 是匯編語(yǔ)言寫(xiě)的,為什么是這樣呢?一方面是性能上的考慮,動(dòng)態(tài)查找 IMP 是一項(xiàng)耗時(shí)的過(guò)程,況且應(yīng)用運(yùn)行時(shí)會(huì)無(wú)數(shù)次的調(diào)用函數(shù),因此需要 objc_msgSend 在執(zhí)行速度上達(dá)到最優(yōu);另一方面是編程語(yǔ)言上的問(wèn)題,因?yàn)槌绦騿T會(huì)設(shè)計(jì)出各式各樣的函數(shù),它們的參數(shù)也是數(shù)量不一、類型多變的,使用 C 語(yǔ)言很難寫(xiě)出函數(shù)原型來(lái)處理所有的情景,所以需要使用匯編來(lái)解決。
另一個(gè)需要注意的地方是函數(shù)的返回值,使用匯編可以解決可變參數(shù)的問(wèn)題,但是函數(shù)返回值還是要區(qū)別對(duì)待。如果函數(shù)的返回值是 struct 或是 float,那么會(huì)有 objc_msgSend_stret 以及 objc_msgSend_fpret 版本與其對(duì)應(yīng),詳情可見(jiàn) message.h 文件。
以下是 ARM64 架構(gòu)上的 objc_msgSend 代碼(對(duì)應(yīng) objc-msg-arm64.s 文件):
ENTRY _objc_msgSend
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x9, x13, #ISA_MASK // x9 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x9, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
objc_msgSend 首先會(huì)去檢測(cè)對(duì)象指針是否為 nil 或是 tagged pointer,如果是 nil,則返回的 IMP 也是 nil;如果是 tagged pointer,則對(duì)其進(jìn)行特殊處理得到 isa 指針。接下來(lái)便是根據(jù) isa 拿到 class 指針,然后進(jìn)入最關(guān)鍵的一步:CacheLookup。
方法緩存
為了提升 IMP 的查詢速度,系統(tǒng)為其設(shè)計(jì)了緩存。objc_msgSend 進(jìn)行消息派發(fā)的第一步便是去緩存中查找 IMP,具體的查詢流程在 CacheLookup 中。在閱讀 CacheLookup 代碼前,我們需要弄清楚 Objc 的方法緩存究竟是什么。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
......
}
方法緩存存在于類對(duì)象中,而且每個(gè)類都有一份。類的結(jié)構(gòu)推薦大家看這篇文章:從 NSObject 的初始化了解 isa,本文就不再贅述。在上面代碼中,我們可以看到方法緩存的類型是 cache_t,它的定義如下:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
...
}
struct bucket_t {
cache_key_t _key;
IMP _imp;
...
}
其中的 mask_t 以及 cache_key_t 定義如下:
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
在 Objc 中,方法緩存被設(shè)計(jì)成一張 hash 表,對(duì)應(yīng)于 cache_t 結(jié)構(gòu)體中的 _buckets 屬性。cache_t 中的 _mask 屬性代表 _buckets 數(shù)組的最大索引(size = _mask + 1),_occupied 代表當(dāng)前已存放的緩存數(shù)量,當(dāng) _occupied / _buckets.size > 3 / 4 時(shí),系統(tǒng)會(huì)對(duì) _buckets 數(shù)組擴(kuò)容。以下是 Objc 存放緩存的算法,我們來(lái)看下緩存是怎樣被填充的:
cache_t *getCache(Class cls)
{
assert(cls);
return &cls->cache;
}
cache_key_t getKey(SEL sel)
{
assert(sel);
return (cache_key_t)sel;
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
// 1. 合法性檢查
......
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// 2. 判斷是否需要擴(kuò)容
......
// 3. 插入緩存
// 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);
}
系統(tǒng)首先會(huì)進(jìn)行一些合法性檢查以及判斷是否需要對(duì)緩存進(jìn)行擴(kuò)容,接下來(lái)便是調(diào)用 cache->find(key, receiver) 函數(shù)尋找存放緩存的位置,最后調(diào)用 bucket->set(key, imp) 設(shè)置緩存。這樣看來(lái)核心代碼就在 cache->find 中了,我們來(lái)看它的實(shí)現(xiàn):
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
系統(tǒng)通過(guò) cache_hash 函數(shù)對(duì) selector 進(jìn)行簡(jiǎn)單的運(yùn)算,得到 hash 表的索引,然后去 _buckets 中取出緩存,如果為空或是發(fā)現(xiàn)之前已經(jīng)記錄過(guò),那么就返回緩存,否則就依次遍歷 hash 表來(lái)尋找合適的位置。以上便是 Objc 用于存放緩存的核心算法,只要把相關(guān)的數(shù)據(jù)結(jié)構(gòu)弄清楚了就很容易看懂,那么接下來(lái)就來(lái)看 CacheLookup 到底做了些什么事情:
.macro CacheLookup
// x1 = SEL, x9 = isa
ldp x10, x11, [x9, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x16, x17, [x12] // {x16, x17} = *bucket
1: cmp x16, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->cls == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x16, x17, [x12] // {x16, x17} = *bucket
1: cmp x16, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->cls == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
雖然 CacheLookup 都是用匯編寫(xiě)的,但是有了上面的基礎(chǔ)再配合代碼注釋,理解起來(lái)并不困難。大體上來(lái)說(shuō),還是先通過(guò) selector 計(jì)算得到 hash 表的索引,然后去 buckets 中尋找緩存,若是有沖突就
順位向下尋找,如果最后找到了,就執(zhí)行相應(yīng)的 IMP,否則就交由 JumpMiss 處理:
.macro JumpMiss
.if $0 == NORMAL
b __objc_msgSend_uncached_impcache
.else
b LGetImpMiss
.endif
由于最初我們是這樣調(diào)用 CacheLookup 的:CacheLookup NORMAL ,所以當(dāng)緩存沒(méi)有命中時(shí),系統(tǒng)會(huì)跳轉(zhuǎn)到 __objc_msgSend_uncached_impcache 進(jìn)行下一步的處理。
STATIC_ENTRY __objc_msgSend_uncached_impcache
// 保存寄存器中的值
......
// receiver and selector already in x0 and x1
mov x2, x9
bl __class_lookupMethodAndLoadCache3
// imp in x0
mov x17, x0
// 恢復(fù)寄存器
......
br x17
END_ENTRY __objc_msgSend_uncached_impcache
可以看到 __objc_msgSend_uncached_impcache 最終調(diào)用 __class_lookupMethodAndLoadCache3 函數(shù)來(lái)處理緩存失效的情況,同時(shí)它也是我們接下來(lái)要講的消息派發(fā)的第二階段:消息發(fā)送。
消息發(fā)送
消息發(fā)送是在緩存失效后啟動(dòng)的,主要的作用就是在類自身以及繼承體系中尋找 IMP,與此外還給了程序員動(dòng)態(tài)添加 IMP 的機(jī)會(huì)。前面我們提到系統(tǒng)在緩存失效后會(huì)調(diào)用 __class_lookupMethodAndLoadCache3 函數(shù),但是 __class_lookupMethodAndLoadCache3 僅僅調(diào)用了 lookUpImpOrForward 函數(shù)(參見(jiàn) objc-runtime-new.mm 文件):
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
看來(lái) lookUpImpOrForward 函數(shù)是消息發(fā)送的核心,我們來(lái)看它到底做了些什么:
- 在緩存中尋找 IMP。由于之前對(duì)緩存的查詢以失敗告終,所以 _class_lookupMethodAndLoadCache3 調(diào)用 lookUpImpOrForward 函數(shù)時(shí)傳入的 cache 參數(shù)為 NO,因此這里并不會(huì)觸發(fā)緩存的查詢操作。
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
-
進(jìn)行一些準(zhǔn)備工作,realizeClass 用來(lái)申請(qǐng) class_rw_t 的可讀寫(xiě)空間,_class_initialize 是對(duì)類進(jìn)行初始化。
if (!cls->isRealized()) { rwlock_writer_t lock(runtimeLock); realizeClass(cls); } if (initialize && !cls->isInitialized()) { _class_initialize (_class_getNonMetaClass(cls, inst)); } 準(zhǔn)備工作結(jié)束后,開(kāi)始消息發(fā)送的流程。
// The lock is held 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.
retry:
runtimeLock.read();
// Ignore GC selectors
if (ignoreSelector(sel)) {
imp = _objc_ignored_method;
cache_fill(cls, sel, imp, inst);
goto done;
}
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
系統(tǒng)先是調(diào)用 runtimeLock.read() 加鎖,然后判斷 sel 是否與垃圾回收相關(guān),是的話就返回 _objc_ignored_method,否則就去緩存中查找,如果緩存未命中,就進(jìn)入下一步。
- 調(diào)用 getMethodNoSuper_nolock 在類自身的方法列表中查找,如果找到,則調(diào)用 log_and_fill_cache 填充緩存并結(jié)束查詢流程,否則進(jìn)入下一步。
// Try this class's method lists.
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
log_and_fill_cache 最終會(huì)調(diào)用到 cache_fill_nolock 函數(shù),也就是我們之前講的存放緩存的那一套流程。
- 按照繼承體系依次在父類的緩存以及方法列表中尋找 IMP。
// Try superclass caches and method lists.
curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
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;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
如果在父類中找到了 IMP,系統(tǒng)會(huì)將其填充到子類的方法緩存中(不會(huì)填充到父類的緩存),然后結(jié)束流程。如果在父類中沒(méi)有找到 IMP 或者找到的 IMP 是 _objc_msgForward_impcache,那么系統(tǒng)會(huì)結(jié)束對(duì)父類的查詢,然后進(jìn)入下一步。
- 動(dòng)態(tài)方法解析。
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
// 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;
}
系統(tǒng)使用 runtimeLock.unlockRead() 解鎖,然后調(diào)用 _class_resolveMethod 函數(shù)給程序員一個(gè)動(dòng)態(tài)添加 IMP 的機(jī)會(huì):
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
_class_resolveMethod 首先會(huì)判斷 cls 是否是元類,如果不是元類,就去調(diào)用 _class_resolveInstanceMethod 來(lái)動(dòng)態(tài)解析實(shí)例方法,否則就調(diào)用 _class_resolveClassMethod 動(dòng)態(tài)解析類方法,如果 _class_resolveClassMethod 沒(méi)有起到作用,就調(diào)用 _class_resolveInstanceMethod 再次解析。
這里以 _class_resolveInstanceMethod 為例:
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 打印日志
......
}
實(shí)例方法的動(dòng)態(tài)解析需要程序員實(shí)現(xiàn) + (BOOL)resolveInstanceMethod:(SEL)sel; 方法,在其中為類添加 IMP, _class_resolveInstanceMethod 函數(shù)運(yùn)行時(shí)會(huì)去調(diào)用這個(gè)方法,然后利用 lookUpImpOrNil 函數(shù)緩存結(jié)果。如果需要?jiǎng)討B(tài)添加類方法,你需要實(shí)現(xiàn) + (BOOL)resolveClassMethod:(SEL)sel;。
動(dòng)態(tài)解析結(jié)束后,goto retry; 會(huì)重復(fù)之前的查找流程,這會(huì)是消息發(fā)送流程的最后一次嘗試。
- retry 結(jié)束后,如果 IMP 還是沒(méi)有找到,那么系統(tǒng)就返回 _objc_msgForward_impcache,進(jìn)入消息轉(zhuǎn)發(fā)階段。
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
// paranoia: look for ignored selectors with non-ignored implementations
assert(!(ignoreSelector(sel) && imp != (IMP)&_objc_ignored_method));
// paranoia: never let uncached leak out
assert(imp != _objc_msgSend_uncached_impcache);
return imp;
消息轉(zhuǎn)發(fā)
消息轉(zhuǎn)發(fā)是 Objc 消息派發(fā)的第三步,也是最后一步,不過(guò) _objc_msgForward_impcache 只是內(nèi)部使用的函數(shù)指針,它需要被轉(zhuǎn)為 _objc_msgForward 才可以被外部調(diào)用。
/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward is the externally-callable
* function returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
* method caches.
*
********************************************************************/
STATIC_ENTRY __objc_msgForward_impcache
MESSENGER_START
nop
MESSENGER_END_SLOW
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
而 _objc_msgForward 也不是終點(diǎn),它僅僅調(diào)用 __objc_forward_handler:
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr x17, [x17, __objc_forward_handler@PAGEOFF]
br x17
END_ENTRY __objc_msgForward
那么 __objc_forward_handler 究竟是什么,我們可以在 objc-runtime.mm 文件中找到它的默認(rèn)實(shí)現(xiàn):
// Default forward handler halts the process.
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
用來(lái)設(shè)置 __objc_forward_handler 的函數(shù)是 objc_setForwardHandler,但是在蘋(píng)果開(kāi)源的代碼中并沒(méi)有看到它在哪里被調(diào)用,那么就在實(shí)際項(xiàng)目中打個(gè)斷點(diǎn)看一下:

它是在
__CFInitialize 函數(shù)中被調(diào)用,調(diào)用時(shí)傳入 __forwarding_prep_0___ 以及 __forwarding_prep_1___ 參數(shù):

結(jié)合
objc_setForwardHandler 的代碼,可以看出 __forwarding_prep_0___ 對(duì)應(yīng) _objc_forward_handler,而 __forwarding_prep_1___ 對(duì)應(yīng) _objc_forward_stret_handler。然而在源碼中依舊沒(méi)有看到 __forwarding_prep_0___,那么就反匯編看一下 __forwarding_prep_0___ 都做了些什么:

__forwarding_prep_0___ 內(nèi)部調(diào)用 ___forwarding___ 函數(shù),如果 ___forwarding___ 的結(jié)果不為空,則將結(jié)果返回,否則調(diào)用 objc_msgSend,下一步應(yīng)該就是拋出 doesNotRecognizeSelector 錯(cuò)誤了。使用 dis -n ___forwarding___ 命令可以查看 ___forwarding___ 的內(nèi)部邏輯,這里就不截圖了(圖片大概有兩個(gè)外接顯示器那么高??),下面簡(jiǎn)單總結(jié)一下 ___forwarding___ 內(nèi)部的流程:
如果用戶實(shí)現(xiàn)了 forwardingTargetForSelector 函數(shù),就用它拿到用于消息轉(zhuǎn)發(fā)的新的 target,如果 target 不為空并且不是當(dāng)前對(duì)象,那么就調(diào)用 objc_msgSend(target, sel, ...) 函數(shù),否則進(jìn)入下一步。
判斷當(dāng)前對(duì)象是否為僵尸對(duì)象,是的話就會(huì)拋出異常:

如果用戶實(shí)現(xiàn)了 methodSignatureForSelector 函數(shù)并且生成的簽名不為空,系統(tǒng)就會(huì)根據(jù)它創(chuàng)建 NSInvocation 對(duì)象,并將其作為參數(shù)傳遞給 forwardInvocation 函數(shù),否則進(jìn)入下一步。
判斷 selector 是否在 runtime 注冊(cè)過(guò),沒(méi)有的話會(huì)出現(xiàn)這樣的提錯(cuò)誤信息:

否則就是大家喜聞樂(lè)見(jiàn)的的 doesNotRecognizeSelector 錯(cuò)誤,如果連 doesNotRecognizeSelector 也沒(méi)有實(shí)現(xiàn),錯(cuò)誤信息會(huì)是這樣:

以上就是消息轉(zhuǎn)發(fā)的流程。
總結(jié)
上面的內(nèi)容涵蓋了 Objc 消息派發(fā)的三部曲:1. 緩存查找;2. 消息發(fā)送;3. 消息轉(zhuǎn)發(fā),由于內(nèi)容十分龐雜,所以這里總結(jié)下消息派發(fā)的流程來(lái)幫助大家理解:

enmmmmmmm,終于寫(xiě)完了,斷斷續(xù)續(xù)花了好多時(shí)間,中途還差點(diǎn)棄坑,不過(guò)最終還是完成了,真是可喜可賀,晚上給自己加個(gè)雞腿??