快速查找
runtime將方法調(diào)用轉(zhuǎn)換為objc_msgSend函數(shù),盡管每個方法的返回值,參數(shù)可能不一樣,
但是objc_msgSend可以做類型轉(zhuǎn)換.
這個函數(shù)沒有C++實現(xiàn),直接是匯編代碼實現(xiàn).位于.s文件,不同的架構(gòu)有不同的文件以及不同的匯編代碼.
除了objc_msgSend還有幾個相關(guān)的方法objc_msgSendSuper等.
以ENTRY為入口.
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
NilTest NORMAL
GetIsaFast NORMAL // r10 = self->isa
// calls IMP on success
CacheLookup NORMAL, CALL, _objc_msgSend
NilTestReturnZero NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss_objc_msgSend:
// isa still in r10
jmp __objc_msgSend_uncached
//...
這是x86_64的objc_msgSend,
NilTest,GetIsaFast,CacheLookup等等這些都是定義在當前文件中的其他代碼段.
NilTest用于判斷receive是否為空.
GetIsaFast獲取isa
接下來CacheLookup就是快速查找IMP.
如果沒找到,進入__objc_msgSend_uncached
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
上面這段是arm64的_objc_msgSend.
x86_64沒有SUPPORT_TAGGED_POINTERS的情況,arm64有.
首先也是查看receiver是否為空,如果是空,分成兩種情況,一是tagged pointer,走LNilOrTagged,二是不支持tagged pointer,走LReturnZero.
LNilOrTagged和LReturnZero就在上面代碼的最后部分.
ldr p13, [x0]
GetClassFromIsa_p16 p13, 1, x0
//p13拿到isa 通過isa拿到class放入p16寄存器
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
//接下來走CacheLookup
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
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
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
#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
這里有一個CACHE_MASK_STORAGE,
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
所以arm64真機是CACHE_MASK_STORAGE_HIGH_16
p1的類型是SEL,也就是objc_msgSend傳進來的參數(shù)_cmd, p16是剛才的isa.
ldr p11, [x16, #CACHE] // 這里p11 = mask|buckets,也就是bucket的首地址,
這里CONFIG_USE_PREOPT_CACHES是1,并且我們看帶簽名的部分,也就是__has_feature(ptrauth_calls)是true.
tbnz p11, #0, LLookupPreopt\Function //判斷cache_t存在
and p10, p11, #0x0000fffffffffff //取出后48位,這部分是buckets,從前面的內(nèi)容我們知道bucket是連續(xù)存儲的.
eor p12, p1, p1, LSR #7 //eor異或,LSR #7 是右移7位,這就是前面說到的cache_hash函數(shù)在CONFIG_USE_PREOPT_CACHES為1的時候的算法,是通過這個hash算法獲得SEL的hash值,也就是在buckets中的順位.
上面說到p1是SEL,所以這個就是p12 = SEL ^ (SEL >> 7),在cache_hash函數(shù)里是這么寫的value ^= value >> 7.
and p12, p12, p11, LSR #48 // 不過這里最后還要&上mask,最終得到下標.
add p13, p10, p12, LSL #(1+PTRSHIFT)// 剛才p12是下標,p10是bucket首地址,PTRSHIFT是3,這里是首地址加上下標左移4位.指向了一個bucket內(nèi)存.
1: ldp p17, p9, [x13], #-BUCKET_SIZE
cmp p9, p1
b.ne 3f
2: CacheHit \Mode
3: cbz p9, \MissLabelDynamic
cmp p13, p10
b.hs 1b
這一段是do while,
ldp p17, p9, [x13], #-BUCKET_SIZE //p13是剛才指向的bucket,-BUCKET_SIZE是一個bucket的大小,意思是指向前面一個bucket.放在p9,
這一步對標cache_next函數(shù).
cmp p9, p1 //如果p9不是_cmd.
b.ne 3f //那么去執(zhí)行3
CacheHit \Mode //p9就是_cmd,就執(zhí)行CacheHit
cbz p9, \MissLabelDynamic //如果p9是空的,執(zhí)行MissLabelDynamic函數(shù).
cmp p13, p10 //如果p13大于p10,也就是說bucket不是第0個.
b.hs 1b //就執(zhí)行1,向前移動.
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) //如果向前移動到了首地址,還沒匹配到,那就把p13移動到最后一個bucket
add p12, p10, p12, LSL #(1+PTRSHIFT) //剛才cmp p13, p10是從中間到首地址,現(xiàn)在修改p10,到剛才的p13后面一個,意思就是已經(jīng)查看過的部分就不再查看了
4: ldp p17, p9, [x13], #-BUCKET_SIZE // 取出sel
cmp p9, p1
b.eq 2b
cmp p9, #0
ccmp p13, p12, #0, ne
b.hi 4b
還是do while,如果相等,就執(zhí)行2, 否則就繼續(xù)移動,直到走到新的首地址.
上面使用了和cache_hash和cache_next相同的算法來查找,也就是存的取的方法是一樣的,只要存了,那一定取的到,
如果沒存,那么在ldp p17, p9, [x13], #-BUCKET_SIZE這一步,p9就是空的,
然后cbz p9, \MissLabelDynamic就會跳轉(zhuǎn)到MissLabelDynamic.
這個MissLabelDynamic是什么呢.
/*
* CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
*/
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
CacheLookup是這么定義的,MissLabelConstant是參數(shù),是一個函數(shù)
回到前面看objc_msgSend調(diào)用cacheLookup的地方
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
MissLabelConstant傳的是objc_msgSend_uncached,所以我們?nèi)タ催@個函數(shù)
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached
里面執(zhí)行的是MethodTableLookup
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
MethodTableLookup里面執(zhí)行的是lookUpImpOrForward函數(shù),
傳參是lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER).
這個函數(shù)就是在快速查找失敗的時候會執(zhí)行的,它是C++實現(xiàn).位于objc-runtime-new.mm
還是CacheLookup的定義
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.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
mode有三種,NORMAL,GETIMP,和LOOKUP
objc_msgSend里面?zhèn)鞯木褪荖ORMAL.是驗證并調(diào)用imp
當傳入GETIMP時,就是_cache_getImp這個函數(shù)的實現(xiàn),
IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil);
這個函數(shù)也是頻繁的被使用.用于獲取imp.
LOOKUP模式是驗證并重簽名imp,不會調(diào)用.
慢速查找
上面講到,快速查找失敗會執(zhí)行l(wèi)ookUpImpOrForward函數(shù),這個函數(shù)位于objc-runtime-new.mm
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
這個方法是在匯編中調(diào)用的時候,傳參是這樣的
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
obj是x0,sel是x1,cls是x2,最后的參數(shù)behavior是x3,傳的是3.
首先判斷類是否初始化isInitialized,這個條件一般不會進來,如果類沒有初始化,behavior = 0011 | 1000 = 1011.
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
判斷類是否被添加到類表中,也就是存不存在這個類,如果不存在,就會觸發(fā)斷言.
然后調(diào)用realizeAndInitializeIfNeeded_locked初始化類,behavior & LOOKUP_INITIALIZE是0011 & 0001 或者1011 & 0001總之一定是true.
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 {
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
imp = forward_imp;
break;
}
}
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
這是一個最多執(zhí)行attempts次的for循環(huán),或者說是無限循環(huán),當(--attempts == 0)時會直接觸發(fā)斷言.
attempts是一個相當大的數(shù),它是這么實現(xiàn)的
static unsigned unreasonableClassCount()
{
runtimeLock.assertLocked();
int base = NXCountMapTable(gdb_objc_realized_classes) +
getPreoptimizedClassUnreasonableCount();
return (base + 1) * 16;
}
它是以靜態(tài)類表的大小加上dyld的類的個數(shù)為基礎(chǔ),乘上16得到的一個不真實的數(shù)字,因為那個for循環(huán)不需要真實的循環(huán)次數(shù),只要足夠就行.
isConstantOptimizedCache用于判斷是否緩存優(yōu)化.
CONFIG_USE_PREOPT_CACHES在arm64并且iOS時是1.
cache_getImp前面說到了是cahcelookup的getImp mode,用來查找imp.
如果滿足這些條件,就會走快速查找,不過cahcelookup的getImp mode失敗了不走lookUpImpOrForward,所以不會循環(huán).
另一種情況則是去rw中找method_t, getMethodNoSuper_nolock函數(shù)是從類本身查找.
前面curClass首先指向傳進來的cls.
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
從class獲取methods(),這是一個list_array_tt,可能會有兩層結(jié)構(gòu),內(nèi)層結(jié)構(gòu)用search_method_list_inline來迭代.
search_method_list_inline這個函數(shù)在上一篇有說明.
如果從類自己身上沒找到的話,curClass = curClass->getSuperclass(),
curClass會指向父類,順便判斷如果沒有父類了,就賦值imp為forward_imp,這個等下再說.
接下來是前面有提到的for循環(huán)最大執(zhí)行次數(shù).
然后就是從父類查找,如果找到了就跳到done,沒有就繼續(xù)循環(huán).
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
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);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
如果for循環(huán)中找到了imp,就會跳轉(zhuǎn)到done或者done_unlock,
如果沒有找到imp,但是從for循環(huán)break了,就會走resolveMethod_locked.
但是它是有條件的,為什么要設(shè)置條件呢,因為resolveMethod_locked的流程中也會調(diào)用lookUpImpOrForward,
這行限制改變了behavior,第二次進來的時候就不滿足條件了.
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
if (! cls->isMetaClass()) {
resolveInstanceMethod(inst, sel, cls);
}
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
這個過程叫做動態(tài)方法決議.
做了兩件事,
1.如果是元類就調(diào)用resolveClassMethod,如果是類對象就調(diào)用resolveInstanceMethod
2.調(diào)用lookUpImpOrForwardTryCache
lookUpImpOrForwardTryCache就是_lookUpImpTryCache,它里面干了兩件事,
一是快速查找,二是慢速查找.
為什么要再來次,這是因為步驟1,也就是動態(tài)決議,給了程序一個臨時添加方法的機會,添加之后,再走一次查找流程.
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
首先獲取了一個SEL,resolveInstanceMethod,并且判斷class是否實現(xiàn)了這個sel.
如果實現(xiàn)了,就給class發(fā)消息,調(diào)用resolveInstanceMethod這個方法.
然后調(diào)用lookUpImpOrNilTryCache查詢一次sel是否存在了.
void newFunc(){
NSLog(@"新添加的method6");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"未知的方法 %@",NSStringFromSelector(sel));
if([NSStringFromSelector(sel) isEqualToString:@"myMethod6"]){
class_addMethod(self, sel, (IMP)newFunc, "");
return true;
}
return [super resolveInstanceMethod:sel];
}
//main.m
id myObj = [MyClass alloc];
MyClass *my = (MyClass *)[myObj init];
[my performSelector:NSSelectorFromString(@"myMethod6")];
寫一個demo,可以通過在bool resolved = msg(cls, resolve_sel, sel);斷點,
看到消息發(fā)送之后,輸出了"未知的方法 myMethod6".
放開斷點輸出"新添加的method6".
在done的位置還調(diào)用了log_and_fill_cache函數(shù)
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);
}
這個函數(shù)的作用是給cache_t添加數(shù)據(jù).這部分的內(nèi)容在上一篇.
上面的logMessageSend是輸出方法調(diào)用的信息.
void instrumentObjcMessageSends(BOOL flag)
通過這個函數(shù)可以設(shè)置是否輸出.
前面提到了一個forward_imp.
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
_objc_msgForward_impcache沒有c++的實現(xiàn),但是可以在.s里找到匯編實現(xiàn)
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
``
里面就一句,執(zhí)行__objc_msgForward,然后__objc_msgForward的實現(xiàn)緊接著在下面,
調(diào)用__objc_forward_handler,最終返回一個x17.
__objc_forward_handler又回到了c++代碼
// Default forward handler halts the process.
attribute((noreturn, cold)) 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;
這就是轉(zhuǎn)發(fā)的默認實現(xiàn),會直接終止程序,錯誤信息unrecognized selector sent to instance ...
老朋友了屬于是.
這個函數(shù)是可以修改的.
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
endif
}