iOS的OC的方法的決議與訊息轉發原理
前言
筆者整理了一系列有關OC的底層文章,希望可以幫助到你。
OC的方法的查詢是通過訊息的傳送
來查詢函式的IMP
,首先通過objc_msgSend
來進行慢速查詢(cache_t
),如果慢速找不到,就需要進行方法的快速查詢,具體可以瞭解iOS的OC的方法的查詢原理這篇文章。但是,如果通過慢速和快速的查詢都找不到的話,就會直接報錯。是不是說如果找不到就沒有辦法走其他的操作了呢?並不是的,接下來這邊文章就是介紹方法的決議和訊息轉發原理
TestObject
類。
1.方法的決議
實現下面的程式碼,其中testErrorMthod
方法是沒有在TestObject
類宣告和實現的,直接執行是報錯的。
TestObject *testObject = [[TestObject alloc] init];
[testObject performSelector:@selector(testErrorMthod)];
==========執行結果=================
LGTest[1639:40195] -[TestObject testErrorMthod]: unrecognized selector sent to instance 0x1010c3600
LGTest[1639:40195] *** Terminating app due to uncaught exception 'NSInvalidArgumentException' ,reason: '-[TestObject testErrorMthod]: unrecognized selector sent to instance 0x1010c3600'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff3c7438ab __exceptionPreprocess + 250
1 libobjc.A.dylib 0x000000010038dfea objc_exception_throw + 42
2 CoreFoundation 0x00007fff3c7c2b61 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff3c6a7adf ___forwarding___ + 1427
4 CoreFoundation 0x00007fff3c6a74b8 _CF_forwarding_prep_0 + 120
5 libobjc.A.dylib 0x00000001003ccf26 -[NSObject performSelector:] + 70
6 LGTest 0x0000000100001afd main + 93
7 libdyld.dylib 0x00007fff73d6b7fd start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
複製程式碼
在之前介紹方法的快速查詢流程中的lookUpImpOrForward
函式中,有這段原始碼,在方法查詢不到的時候,會執行到裡面去。
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls,sel,inst);
runtimeLock.lock();
// 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;
}
複製程式碼
1.1 _class_resolveMethod
下面是_class_resolveMethod
的原始碼,其中cls
是類,sel
方法的編號,inst
是例項物件。
void _class_resolveMethod(Class cls,SEL sel,id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls,inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls,inst);
if (!lookUpImpOrNil(cls,inst,NO/*initialize*/,YES/*cache*/,NO/*resolver*/))
{
_class_resolveInstanceMethod(cls,inst);
}
}
}
複製程式碼
這段原始碼對傳進來的cls
判斷是否是元類
,通過之前的文章可以知道類方法
是存在元類
中的。所以如果傳進來的cls
是類,就直接執行_class_resolveInstanceMethod
函式,如果是元類
執行_class_resolveClassMethod
函式。
1.2 _class_resolveInstanceMethod
static void _class_resolveInstanceMethod(Class cls,id inst)
{
if (! lookUpImpOrNil(cls->ISA(),SEL_resolveInstanceMethod,cls,NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class,SEL,SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls,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,NO/*resolver*/);
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->isMetaClass() ? '+' : '-',sel_getName(sel));
}
}
}
複製程式碼
其中lookUpImpOrNil
函式對類方法SEL_resolveInstanceMethod
查詢是否有存在,發現SEL_resolveInstanceMethod
的實現是resolveInstanceMethod
方法,這個是在NSObject
類裡面有實現的,所以這個是返回true
的。
objc_msgSend
查詢當前的類是否執行resolveInstanceMethod
方法。
BOOL (*msg)(Class,SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls,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,NO/*resolver*/);
複製程式碼
通過前面的文章,可以知道如果當前的類沒有實現resolveInstanceMethod
類方法就會查詢父類是否實現,因為在NSObject
類中是預設實現返回NO
的,如果當前的類有實現resolveInstanceMethod
類方法就會執行方法裡的內容。並且在實現的resolveInstanceMethod
類方法中對沒有找到的sel
重新賦值一個IMP
,下面是在TestObject
類的實現程式碼。
-(void)testOk{
NSLog(@"%p===testOk",__func__);
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"===執行resolveInstanceMethod===%s===%@",__func__,NSStringFromSelector(sel));
if(sel == @selector(testErrorMthod)){
Method okMethod = class_getInstanceMethod(self,@selector(testOk));
IMP okImp = method_getImplementation(okMethod);
const char *type = method_getTypeEncoding(okMethod);
return class_addMethod(self,okImp,type);
}
return [super resolveInstanceMethod:sel];
}
======執行的結果=======
LGTest[2769:92089] ===執行resolveInstanceMethod===+[TestObject resolveInstanceMethod:]===testErrorMthod
LGTest[2769:92089] 0x100001f2a===testOk
Program ended with exit code: 0
複製程式碼
此時還執行一次lookUpImpOrNil
函式在快取中查詢。所以在TestObject
類中定義了這個方法並且為testErrorMthod
賦值了新的IMP
。
// 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,NO/*resolver*/);
複製程式碼
上面的是對例項方法的動態決議,其實對類方法的也是差不多的,如果是類方法此時會執行如下原始碼
_class_resolveClassMethod(cls,inst);
}
複製程式碼
1.3 _class_resolveClassMethod
static void _class_resolveClassMethod(Class cls,id inst)
{
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls,SEL_resolveClassMethod,SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls,inst),sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(cls,imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
",sel_getName(sel));
}
}
}
複製程式碼
通過原始碼可以知道是與_class_resolveInstanceMethod
原始碼是差不多的,主要區別是,這個原始碼的SEL_resolveClassMethod
是要實現resolveClassMethod
這個類方法,需要在TestObject
類中實現類方法resolveClassMethod
並在這個方法裡面實現邏輯。但是為什麼執行完_class_resolveClassMethod
函式之後還會再做一次lookUpImpOrNil
函式的判斷呢?因為如果在_class_resolveClassMethod
是沒有做處理的,由於元類
的查詢方法查詢流程是會往根元類
查詢最終會找到NSObject
這個類,所以如果在根元類
都找不到的情況下會找到NSObject
類的方法裡面。而NSObject
類是有預設實現了這兩個類方法的並且預設返回NO
。
2. 訊息轉發
如果對不存在的方法的查詢,沒有實現上面的方法決議
,此時會在lookUpImpOrForward
函式中執行
// No implementation found,and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls,imp,inst);
複製程式碼
其中_objc_msgForward_impcache
就會執行到彙編中的內容,然後執行__objc_msgForward
,最終會執行到_objc_forward_handler
函式中,最終是會報錯的。
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
// 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),self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製程式碼
這些流程就是在之前文章的方法的查詢原理有介紹。是不是就是說訊息的轉發流程就跟宗不了呢?並不是的。在lookUpImpOrForward
函式中有一個可以列印log
的方法log_and_fill_cache
中的logMessageSend
方法裡面有介紹可以根據objcMsgLogEnabled
屬性來控制列印log
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling,flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
複製程式碼
為了可以看到訊息轉發的過程中實現了那些方法,可以在mac的專案中實現
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc,const char * argv[]) {
@autoreleasepool {
TestObject *test = [TestObject alloc] ;
instrumentObjcMessageSends(true);
[test performSelector:@selector(testErrorMthod)];
instrumentObjcMessageSends(false);
}
return 0;
}
複製程式碼
就可以在路徑:/tmp/msgSends-
找到生成的msgSends
檔案
msgSends-7265
檔案的內容
這個列印log是要在mac的專案下才可以,如果在其他的專案下是會報objc[6984]: lock 0x100cbf0c0 (runtimeLock) acquired before 0x100cbf040 (objcMsgLogLock) with no defined lock order這種錯誤。
從打印出來的方法可以知道,在訊息的轉發的過程中執行的過程是resolveInstanceMethod
-->forwardingTargetForSelector
-->methodSignatureForSelector
-->resolveInstanceMethod
-->doesNotRecognizeSelector
。所以如果不執行resolveInstanceMethod
方法決議,會執行forwardingTargetForSelector
方法。
2.1 訊息快速轉發
在objc
的原始碼中可以找到在NSObject.mm
檔案中有定義和實現forwardingTargetForSelector
的例項方法和類方法。
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
複製程式碼
在官方的檔案中有對forwardingTargetForSelector
方法的介紹
forwardingTargetForSelector
主要是返回一個不是自身(如果是self會進入死循壞)的物件去處理sel
這個當前類無法處理的訊息,其他的情況可以呼叫super
方法。如果處理不了,會轉到效率低下的forwardInvocation
。在效率方面,forwardingTargetForSelector
領先forwardInvocation
一個數量級,因此,如果可以的話最好避免使用後者來做訊息轉發。下面在TestObject
類中新增多一個TestForwardObject
類,並且在TestObject
類中實現forwardingTargetForSelector
方法。
@interface TestForwardObject : NSObject
@end
@implementation TestForwardObject
-(void)testErrorMthod{
NSLog(@"TestForwardObject的testErrorMthod方法%p",__func__);
}
@end
//在TestObject類中實現的方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"方法名字:%@",NSStringFromSelector(aSelector));
if(aSelector == @selector(testErrorMthod)){
return [TestForwardObject alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
//=========執行的結果========
LGTest[1308:29082] 方法名字:testErrorMthod
LGTest[1308:29082] TestForwardObject的testErrorMthod方法0x100001eb4
複製程式碼
從中可以看到訊息的轉發到TestForwardObject
的testErrorMthod
方法執行了。但是需要注意的是轉發到其他的類執行的方法必須要和被呼叫的方法相同方法簽名的方法(方法名、引數列表、返回值型別都必須一致)。否則的話,還是報錯的。
2.2訊息慢速轉發
如果在訊息轉發的慢速流程中不做處理,此時會執行到訊息轉發的慢速流程中,需要分別執行兩個方法分別是methodSignatureForSelector
和forwardInvocation
。
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"方法名字:%@",NSStringFromSelector(aSelector));
if(aSelector == @selector(testErrorMthod)){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"執行forwardInvocation:%s",__func__);
}
//======執行結果==========
LGTest[1222:21266] 方法名字:testErrorMthod
LGTest[1222:21266] 執行forwardInvocation:-[TestObject forwardInvocation:]
複製程式碼
在這個流程中methodSignatureForSelector
是返回的方法的簽名,可以參考
蘋果官方型別編碼。可以發現在forwardInvocation
方法中就算不做處理也不會奔潰,因為每個方法其實就是一個事務,不做處理就會失效,在forwardInvocation
中做處理的話,可以如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"執行forwardInvocation:%s",__func__);
SEL inVocationSeletor = [anInvocation selector];
if([[TestForwardObject alloc] respondsToSelector:inVocationSeletor]){
[anInvocation invokeWithTarget:[TestForwardObject alloc]];
}else{
[super forwardInvocation:anInvocation];
}
}
//=====執行結果===========
LGTest[1465:30634] 方法名字:testErrorMthod
LGTest[1465:30634] 執行forwardInvocation:-[TestObject forwardInvocation:]
LGTest[1465:30634] TestForwardObject的testErrorMthod方法-[TestForwardObject testErrorMthod]
複製程式碼
3.最後
OC方法呼叫是通過objc_msgSend
先通過cache_t
的快速查詢,如果找不到就要進行慢速查詢
。如果都查詢不到方法,就會進入方法的決議
和訊息轉發流程
。如果查詢的類有實現resolveInstanceMethod
或resolveClassMethod
方法對需要查詢的方法做處理就完成,否則就進入訊息轉發
流程。訊息轉發的流程中先進入訊息快速轉發流程
,需要實現forwardingTargetForSelector
方法。否則進入訊息慢速轉發流程
,需要實現methodSignatureForSelector
和forwardInvocation
方法。如果都沒有,此時程式只能報錯了。最後附上訊息轉發的流程圖