想要成爲一名
iOS開發高手
,免不了閱讀源碼。如下是筆者在OC源碼探索
中梳理的一個小系列——類與對象篇,歡迎你們閱讀指正,同時也但願對你們有所幫助。html
OC
中方法的調用是經過objc_msgSend
(或objc_msgSendSuper
,或objc_msgSend_stret
,或objc_msgSendSuper_stret
)函數,向調用者發送名爲SEL
的消息,找到具體的函數地址IMP
,進而執行該函數。若是找不到IMP
,會進行方法的解析,這至關於提供一次容錯處理;方法解析以後,若是依然找不到IMP
,還有最後一次機會,那就是消息的轉發。git
方法的查找流程盡在 OC源碼分析之方法的查找原理 一文中,文接此文,本文將深刻剖析方法的解析與轉發。github
下面進入正題。緩存
須要注意的是,筆者用的源碼是 objc4-756.2。架構
方法的解析,即method resolver
(又名消息的解析,也叫方法決議),其創建在方法的查找的失敗結果上,入口源碼以下:app
// 在【類...根類】的【緩存+方法列表】中都沒找到IMP,進行方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
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;
}
複製代碼
它主要是調用了resolveMethod
函數。resolveMethod
函數處理完畢以後,還要從新執行一次retry
(再走一遍方法的查找流程)。其中,triedResolver
這個變量使得消息的解析只進行一次。ide
resolveMethod
且看resolveMethod
函數源碼:函數
static void resolveMethod(Class cls, SEL sel, id inst) {
runtimeLock.assertUnlocked();
assert(cls->isRealized());
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
}
}
複製代碼
這裏有兩個分支,主要是對cls
作個是否元類的判斷:源碼分析
resolveInstanceMethod
函數resolveClassMethod
函數,以後若是依然沒找到IMP
,則再去執行resolveInstanceMethod
函數;先看實例方法的狀況post
resolveInstanceMethod
源碼以下:
static void resolveInstanceMethod(Class cls, SEL sel, id inst) {
runtimeLock.assertUnlocked();
assert(cls->isRealized());
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 若是你沒有實現類方法 +(BOOL)resolveInstanceMethod:(SEL)sel
// NSObject也有實現,因此通常不會走這裏
// 注意這裏傳入的第一個參數是:cls->ISA(),也就是元類
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 調用類方法: +(BOOL)resolveInstanceMethod:(SEL)sel
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// 再找一次imp(此次是sel,而不是resolveInstanceMethod)
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, 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->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
複製代碼
resolveInstanceMethod
函數前後調用了兩次lookUpImpOrNil
:
+(BOOL)resolveInstanceMethod:(SEL)sel
類方法
SEL_resolveInstanceMethod
至關於@selector(resolveInstanceMethod:)
,NSObject
類中有實現這個類方法(返回的是NO
,會影響是否打印),因此通常會接着往下走。sel
對應的IMP
。假如你在+(BOOL)resolveInstanceMethod:(SEL)sel
中添加了sel
的函數地址IMP
,此時再次去查找這個IMP
就能找到。注意到這兩次調用中,resolver
都是NO
,所以在其調用lookUpImpOrForward
時不會觸發 消息的解析,僅僅是從「類、父類、...、根類」的緩存中和方法列表中找IMP
,沒找到會觸發 消息轉發。
lookUpImpOrNil
函數源碼:
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) {
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
複製代碼
這裏會判斷IMP
是不是消息轉發而來的,若是是,就不返回。
類方法的解析首先是調用resolveClassMethod
函數,其源碼以下:
// 這裏的cls是元類,由於類方法存儲在元類
static void resolveClassMethod(Class cls, SEL sel, id inst) {
runtimeLock.assertUnlocked();
assert(cls->isRealized());
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 若是你沒有實現類方法 +(BOOL)resolveClassMethod:(SEL)sel
// NSObject也有實現,因此通常不會走這裏
// 注意這裏的第一個參數是cls,是元類
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
// 獲取 元類的對象,即類。換句話說,nonmeta 也就是 inst
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 調用類方法: +(BOOL)resolveClassMethod:(SEL)sel
bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);
// 再找一次imp
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, 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 resolveClassMethod:%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));
}
}
}
複製代碼
你會發現,這個函數與resolveInstanceMethod
函數大致相同,須要留意的是,此次判斷類(包括其父類,直至根類)是否實現的是+(BOOL)resolveClassMethod:(SEL)sel
類方法。
讓咱們回顧一下resolveMethod
函數對類方法的解析
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 此時的cls爲元類,也就是 NSObject 調用 resolveInstanceMethod:
resolveInstanceMethod(cls, sel, inst);
}
複製代碼
在通過resolveClassMethod
的處理以後,若是依然沒有找到類方法的IMP
,就會再次執行resolveInstanceMethod
函數!不一樣於實例方法的是,此時的cls
是元類,所以msg(cls, SEL_resolveInstanceMethod, sel);
便是向元類內部發送resolveInstanceMethod:
消息,也就意味着是根類調用resolveInstanceMethod:
方法(此次只能在根類的分類中補救了),同時緩存查找類方法的IMP
僅發生在根元類和根類中,而方法列表中查找類方法的IMP
則分別在「元類、元類的父類、...、根元類、根類」中進行。
簡而言之,當咱們調用一個類方法時,若是在類中沒有實現,同時在resolveClassMethod
中也沒有處理,那麼最終會調用根類(NSObject
)的同名實例方法。
經過上述的分析,相信你們對方法的解析有了必定的認知,下面咱們來整個簡單的例子消化一下。
@interface Person : NSObject
+ (void)personClassMethod1;
- (void)personInstanceMethod1;
@end
@implementation Person
@end
複製代碼
一個簡單的Person
類,裏面分別有一個類方法和一個實例方法,可是都沒有實現。
接着添加對這兩個方法的解析:
- (void)unimplementedMethod:(SEL)sel {
NSLog(@"沒實現?不要緊,毫不崩潰");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"動態實例方法解析:%@", NSStringFromSelector(sel));
if (sel == @selector(personInstanceMethod1)) {
IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
const char *methodType = method_getTypeEncoding(method);
return class_addMethod(Person.class, sel, methodIMP, methodType);
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"動態類方法解析:%@", NSStringFromSelector(sel));
if (sel == @selector(personClassMethod1)) {
IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
const char *methodType = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("Person"), sel, methodIMP, methodType);
}
return [super resolveClassMethod:sel];
}
複製代碼
看看打印:
經過對類方法解析的源碼分析,咱們知道,也能夠把對Person
類方法的處理放在NSObject
分類的resolveClassMethod:
或resolveInstanceMethod:
中,都能達到相同的效果(記得把Person
類中的resolveClassMethod:
處理去掉)。這裏略過不提。
方法的調用通過了查找、解析,若是仍是沒有找到IMP
,就會來到消息轉發流程。它的入口在lookUpImpOrForward
函數靠後的位置
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製代碼
_objc_msgForward_impcache
是彙編函數,以arm64
架構爲例,其源碼以下:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
複製代碼
__objc_msgForward_impcache
內部調用了__objc_msgForward
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
複製代碼
這個函數主要作的事情是,經過頁地址與頁地址偏移的方式,拿到_objc_forward_handler
函數的地址並調用。
說明:
adrp
是以頁爲單位的大範圍的地址讀取指令,這裏的p
就是page
的意思ldr
相似與mov
和mvn
,噹噹即數(__objc_msgForward
中是[x17, __objc_forward_handler@PAGEOFF]
,PAGEOFF
是頁地址偏移值)大於mov
和mvn
能操做的最大數時,就使用ldr
。
在OBJC2
中,_objc_forward_handler
實際上就是objc_defaultForwardHandler
函數,其源碼以下:
// 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;
複製代碼
是否是很熟悉?當咱們調用一個沒實現的方法時,報的錯就是「unrecognized selector sent to ...」
可是問題來了,說好的消息轉發流程呢?這纔剛開始怎麼就結束了?不急,憋慌,且看下去。
回顧方法解析時舉的例子,不妨把解析的內容去掉,Let it crash!
發如今崩潰以前與消息轉發相關的內容是,調用了_CF_forwarding_prep_0
和___forwarding___
這兩個函數。遺憾的是這兩個函數並未開源。
既然崩潰信息不能提供幫助,只好打印具體的調用信息了。
在方法的查找流程中,log_and_fill_cache
函數就跟打印有關,跟蹤其源碼:
static void 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
cache_fill(cls, sel, imp, receiver);
}
bool objcMsgLogEnabled = false;
// Define SUPPORT_MESSAGE_LOGGING to enable NSObjCMessageLoggingEnabled
#if !TARGET_OS_OSX
# define SUPPORT_MESSAGE_LOGGING 0
#else
# define SUPPORT_MESSAGE_LOGGING 1
#endif
複製代碼
打印的關鍵函數就是logMessageSend
,可是它受SUPPORT_MESSAGE_LOGGING
和objcMsgLogEnabled
控制。
繼續跟進SUPPORT_MESSAGE_LOGGING
#if !DYNAMIC_TARGETS_ENABLED
#define TARGET_OS_OSX 1
...
#endif
#ifndef DYNAMIC_TARGETS_ENABLED
#define DYNAMIC_TARGETS_ENABLED 0
#endif
複製代碼
從源碼不難看出TARGET_OS_OSX
的值是1,所以,SUPPORT_MESSAGE_LOGGING
也爲1!
若是能把objcMsgLogEnabled
改爲true
,顯然就能夠打印調用信息了。經過全局搜索objcMsgLogEnabled
,咱們找到了instrumentObjcMessageSends
這個關鍵函數
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;
}
複製代碼
接下來就好辦了!來到main.m
,添加如下代碼
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(true);
[Person personClassMethod1];
instrumentObjcMessageSends(false);
}
return 0;
}
複製代碼
運行工程,直到再次崩潰。此時已打印函數調用棧,日誌文件位置在logMessageSend
函數中有標註
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
複製代碼
打開Finder
(訪達),cmd
+ shift
+ G
快捷鍵,輸入/tmp/msgSends
,找到最新的一份日誌文件(數字最大)
打印結果以下:
從這份日誌能夠看出,與轉發相關的方法是forwardingTargetForSelector
和methodSignatureForSelector
,分別對應了消息的快速轉發流程和慢速轉發流程,接下來開始分析這兩個方法。
forwardingTargetForSelector:
對應的就是消息的快速轉發流程,它在源碼中只是簡單的返回nil
(可在子類或分類中重寫)
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
複製代碼
不過咱們能夠在開發文檔中找到說明(cmd
+ shift
+ 0
快捷鍵)
歸納地說,forwardingTargetForSelector:
主要是返回一個新的receiver
,去處理sel
這個當前類沒法處理的消息,若是處理不了,會轉到效率低下的forwardInvocation:
。在效率方面,forwardingTargetForSelector:
領先forwardInvocation:
一個數量級,所以,最好不要用後者的方式處理消息的轉發邏輯。
關於forwardingTargetForSelector:
返回的新的receiver
,須要注意一下幾點:
self
,不然會陷入無限循環;nil
,或者[super forwardingTargetForSelector:sel]
(非根類的狀況),此時會走methodSignatureForSelector:
慢速轉發流程;receiver
,此時至關於執行objc_msgSend(newReceiver, sel, ...)
,那麼它必須擁有和被調用的方法相同方法簽名的方法(方法名、參數列表、返回值類型都必須一致)。咱們能夠實驗一下,準備工做以下
@interface ForwardObject : NSObject
@end
@implementation ForwardObject
+ (void)personClassMethod1 {
NSLog(@"類方法轉發給%@,執行%s", [self className], __FUNCTION__);
}
- (void)personInstanceMethod1 {
NSLog(@"實例方法轉發給%@,執行%s", [self className], __FUNCTION__);
}
@end
@interface Person : NSObject
+ (void)personClassMethod1;
- (void)personInstanceMethod1;
@end
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"實例方法開始轉發");
return [ForwardObject alloc];
}
+ (id)forwardingTargetForSelector:(SEL)sel {
NSLog(@"類方法開始轉發");
return [ForwardObject class];
}
@end
複製代碼
顯然,ForwardObject
做爲消息轉發後的處理類,擁有Person
類的同名類方法和實例方法。如今開始驗證,結果以下:
事實證實確實有效!接下來看消息的慢速轉發流程。
若是forwardingTargetForSelector:
沒有處理消息(如返回nil
),就會啓動慢速轉發流程
,也就是methodSignatureForSelector:
方法,一樣須要在子類或分類中重寫
// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
複製代碼
經過閱讀官方文檔,咱們得出如下結論:
methodSignatureForSelector:
方法是跟forwardInvocation:
方法搭配使用的,前者須要咱們根據sel
返回一個方法簽名,後者會把這個方法簽名封裝成一個NSInvocation
對象,並將其做爲形參。Invocation
中的sel
,Invocation
能夠指派這個對象處理;不然不處理。
Invocation
能夠指派多個對象處理注意:消息的慢速轉發流程性能較低,若是能夠的話,你應該儘量早地處理掉消息(如在方法解析時,或在消息的快速轉發流程時)。
針對慢速流程,一樣能夠驗證。這裏把快速轉發例子中的Person
類修改一下:
@implementation Person
// MARK: 慢速轉發--類方法
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"類方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
if (aSelector == @selector(personClassMethod1)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL aSelector = [anInvocation selector];
NSLog(@"類方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
id target = [ForwardObject class];
if ([target respondsToSelector:aSelector]) [anInvocation invokeWithTarget:target];
else [super forwardInvocation:anInvocation];
}
// MARK: 慢速轉發--實例方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"實例方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
if (aSelector == @selector(personInstanceMethod1)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL aSelector = [anInvocation selector];
NSLog(@"實例方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
ForwardObject *obj = [ForwardObject alloc];
if ([obj respondsToSelector:aSelector]) [anInvocation invokeWithTarget:obj];
else [super forwardInvocation:anInvocation];
}
@end
複製代碼
其結果以下圖所示,顯然也沒有崩潰。
對方法簽名類型編碼不熟悉的能夠查看 蘋果官方的類型編碼介紹
綜上所述,當咱們調用方法時,首先進行方法的查找,若是查找失敗,會進行方法的解析,此時OC
會給咱們一次對sel
的處理機會,你能夠在resolveInstanceMethod:
(類方法對應resolveClassMethod:
)中添加一個IMP
;若是你沒把握住此次機會,也就是解析失敗時,會來到消息轉發階段,這個階段有兩個機會去處理sel
,分別是快速轉發的forwardingTargetForSelector:
,以及慢速轉發的methodSignatureForSelector:
。固然,若是這些機會你都放棄了,那OC
只好讓程序崩潰。
下面用一副圖總結方法的解析和轉發流程
在一個方法被調用以前,咱們是沒辦法肯定它的實現地址的,直到運行時,這個方法被調用的時候,咱們才能真正知道它是否有實現,以及其具體的實現地址。這也就是所謂的「動態綁定」。
在編譯期,若是編譯器發現方法不存在,會直接報錯;一樣,在運行時,也有doesNotRecognizeSelector
的處理。
在拋出doesNotRecognizeSelector
這個異常信息以前,OC
利用其動態綁定的特性,引入了消息轉發機制,給予了咱們額外的機會處理消息(解析 or 轉發),這樣的作法顯然更加周全合理。
github
上,請戳 objc4-756.2源碼