歡迎閱讀iOS探索系列(按序閱讀食用效果更加)
寫在前面
上一篇文章講了方法在底層是如何通過sel
找到imp
的,本文就將通過原始碼來研究“沒有實現的方法在底層要通過多少關卡才能發出unrecognized selector sent to instance
並Crash
”,看完本文後你會明白程式崩潰也是一個很複雜的過程
在動態方法決議原始碼中,FXSon
中有兩個只宣告未實現的方法,分別呼叫它們:
- (void)doInstanceNoImplementation;
+ (void)doClassNoImplementation;
一、訊息查詢流程
訊息查詢流程
部分不再展開講解,未實現方法
查詢主要經過以下流程:
- 彙編中通過
isa
平移得到class
,記憶體偏移得到cache->buckets
查詢快取 - c++中
- 先查詢本類快取,再找本類方法列表
- 遍歷父類:查詢父類快取,再找父類方法列表
由於慢速流程呼叫的是lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/)
,遍歷父類無果後來到動態方法解析
二、動態方法解析
只有resolver
和triedResolver
滿足條件下才會進入動態方法解析
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;
}
複製程式碼
動態方法解析按呼叫方法走不同分支:
- cls是
元類
的話說明呼叫類方法,走_class_resolveInstanceMethod
非元類
的話呼叫了例項方法,走_class_resolveInstanceMethod
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);
}
}
}
複製程式碼
1.例項方法
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*/);
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));
}
}
}
複製程式碼
①檢查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)
方法
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;
}
複製程式碼
注意這裡的lookUpImpOrForward
中的resolver
為NO,所以只會在本類和父類中查詢,並不會動態方法解析
但cls沒有這個方法,其實根類NSObject
已經實現了這個方法(NSProxy
沒有實現)
// 具體搜尋 NSObject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
複製程式碼
②向本類傳送SEL_resolveInstanceMethod
訊息,即呼叫這個方法
③lookUpImpOrNil
再次查詢當前例項方法imp,找到就填充快取,找不到就返回
④結束動態方法解析
,回到lookUpImpOrForward
方法將triedResolver
置否並goto retry
重新查詢快取和方法列表
2.例項方法流程圖
3.類方法
相較於例項方法,類方法就複雜多了
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, 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, 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));
}
}
}
複製程式碼
①_class_resolveClassMethod
進入
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);
}
}
複製程式碼
②lookUpImpOrNil
查詢SEL_resolveClassMethod(resolveClassMethod)
是否實現
③向非元類傳送SEL_resolveClassMethod
訊息(由於cls是元類,_class_getNonMetaClass(cls, inst)
得到inst
)
④lookUpImpOrNil
再次查詢當前例項方法imp,找到就填充快取,找不到就返回
⑤結束_class_resolveClassMethod
,lookUpImpOrNil
查詢sel
的imp
,若有imp
則退出動態方法決議,若無則進入_class_resolveInstanceMethod
⑥檢查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)
方法
⑦向本類傳送SEL_resolveInstanceMethod
訊息
⑧lookUpImpOrNil
再次查詢當前例項方法imp,找到就填充快取,找不到就返回
⑨結束動態方法解析
,回到lookUpImpOrForward
方法將triedResolver
置否並goto retry
重新查詢快取和方法列表
4.類方法流程圖
5.動態方法決議
Objective-C提供了一種名為動態方法決議
的手段,使得我們可以在執行時動態地為一個selector
提供實現,並在其中為指定的selector
提供實現即可——子類重寫+resolveInstanceMethod:
或+resolveClassMethod:
- 對於例項方法
從例項方法流程圖
中可以看出,解決崩潰的方法就是resolveInstanceMethod
階段新增一個備用實現
#import "FXSon.h"
#import <objc/message.h>
@implementation FXSon
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(doInstanceNoImplementation)) {
NSLog(@"——————————找不到%@-%@方法,崩潰了——————————", self, NSStringFromSelector(sel));
IMP insteadIMP = class_getMethodImplementation(self, @selector(doInstead));
Method insteadMethod = class_getInstanceMethod(self, @selector(doInstead));
const char *instead = method_getTypeEncoding(insteadMethod);
return class_addMethod(self, sel, insteadIMP, instead);
}
return NO;
}
- (void)doInstead {
NSLog(@"——————————解決崩潰——————————");
}
@end
複製程式碼
- 對於類方法——
resolveClassMethod
階段
效仿解決例項方法崩潰,類方法
也可以往元類
中塞一個imp
(例項方法
存在類物件
中,類方法
存在元類物件
中)
#import "FXSon.h"
#import <objc/message.h>
@implementation FXSon
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(doClassNoImplementation)) {
NSLog(@"——————————找不到%@+%@方法,崩潰了——————————", self, NSStringFromSelector(sel));
IMP classIMP = class_getMethodImplementation(objc_getMetaClass("FXSon"), @selector(doClassNoInstead));
Method classMethod = class_getInstanceMethod(objc_getMetaClass("FXSon"), @selector(doClassNoInstead));
const char *cls = method_getTypeEncoding(classMethod);
return class_addMethod(objc_getMetaClass("FXSon"), sel, classIMP, cls);
}
return NO;
}
+ (void)doClassNoInstead {
NSLog(@"——————————解決崩潰——————————");
}
@end
複製程式碼
- 對於類方法——
resolveInstanceMethod
階段
因為元類
的方法以例項方法
儲存在根元類
中,由於元類
和根源類
由系統建立無法修改,所以只能在根元類
的父類NSObject
中,重寫對應的例項方法resolveInstanceMethod
進行動態解析(isa走點陣圖完美說明一切)
#import "NSObject+FX.h"
#import <objc/message.h>
@implementation NSObject (FX)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"doClassNoImplementation"]) {
NSLog(@"——————————找不到%@-%@方法,崩潰了——————————", self, NSStringFromSelector(sel));
IMP instanceIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead));
Method instanceMethod = class_getInstanceMethod(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead));
const char *instance = method_getTypeEncoding(instanceMethod);
return class_addMethod(objc_getMetaClass("NSObject"), sel, instanceIMP, instance);
}
return NO;
}
- (void)doInstanceNoInstead {
NSLog(@"——————————解決崩潰——————————");
}
@end
複製程式碼
6.動態方法決議總結
例項方法
可以重寫resolveInstanceMethod
新增imp
類方法
可以在本類重寫resolveClassMethod
往元類新增imp
,或者在NSObject分類
重寫resolveInstanceMethod
新增imp
動態方法解析
只要在任意一步lookUpImpOrNil
查詢到imp
就不會查詢下去——即本類
做了動態方法決議,不會走到NSObjct分類
的動態方法決議- 所有方法都可以通過在
NSObject分類
重寫resolveInstanceMethod
新增imp
解決崩潰
那麼把所有崩潰都在NSObjct分類
中處理,加以字首區分業務邏輯,豈不是美滋滋?錯!
- 統一處理起來耦合度高
- 邏輯判斷多
- 可能在
NSObjct分類
動態方法決議之前已經做了處理 - SDK封裝的時候需要給一個容錯空間
這也不行,那也不行,那該怎麼辦?放心,蘋果爸爸已經給我們準備好後路了!
三、訊息轉發機制
lookUpImpOrForward
方法在查詢類、父類快取和方法列表以及動態方法解析後,如果還沒有找到imp
那麼將進入訊息處理的最後一步——訊息轉發流程
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製程式碼
在彙編中發現了_objc_msgForward_impcache
,如下是arm64的彙編程式碼
最後會來到c++中_objc_forward_handler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
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);
}
複製程式碼
再來看看崩潰資訊,崩潰之前底層還呼叫了___forwarding___
和_CF_forwarding_prep_0
等方法,但是CoreFoundation庫
不開源
在無從下手之際,只能根據前輩們的經驗開始著手——然後在logMessageSend
找到了探索方向(lookUpImpOrForward
->log_and_fill_cache
->logMessageSend
)
通過方法我們可以看到,日誌會記錄在/tmp/msgSends
目錄下,並且通過objcMsgLogEnabled
變數來控制是否儲存日誌
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// 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;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
複製程式碼
instrumentObjcMessageSends
可以改變objcMsgLogEnabled
的值
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;
}
複製程式碼
所以我們可以根據以下程式碼來記錄並檢視日誌(彷彿不能在原始碼工程中操作)
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXSon *son = [[FXSon alloc] init];
instrumentObjcMessageSends(true);
[son doInstanceNoImplementation];
instrumentObjcMessageSends(false);
}
}
複製程式碼
訪達
中shift+command+G
訪問/tmp/msgSends
動態方法解析
和doesNotRecognizeSelector崩潰
之間,就是訊息轉發流程
——分為快速流程forwardingTargetForSelector
和慢速流程methodSignatureForSelector
1.快速流程
forwardingTargetForSelector
在原始碼中只有一個宣告,並沒有其它描述,好在幫助文件中提到了關於它的解釋:
- 該方法的返回物件是執行sel的新物件,也就是自己處理不了會將訊息轉發給別人的物件進行相關方法的處理,但是不能返回self,否則會一直找不到
- 該方法的效率較高,如果不實現,會走到
forwardInvocation:
方法進行處理 - 底層會呼叫
objc_msgSend(forwardingTarget, sel, ...);
來實現訊息的傳送 - 被轉發訊息的接受者引數、返回值等應和原方法相同
2.快速流程解決崩潰
如下程式碼就是是通過快速轉發解決崩潰——即FXSon
實現不了的方法,轉發給FXTeacher
去實現(轉發給已經實現該方法的物件)
#import "FXTeacher.h"
@implementation FXSon
// FXTeacher已實現了doInstanceNoImplementation
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(doInstanceNoImplementation)) {
return [FXTeacher alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
複製程式碼
3.慢速流程
在快速流程找不到轉發的物件後,會來到慢速流程methodSignatureForSelector
依葫蘆畫瓢,在幫助文件中找到methodSignatureForSelector
點選檢視forwardInvocation
forwardInvocation
和methodSignatureForSelector
必須是同時存在的,底層會通過方法簽名,生成一個NSInvocation
,將其作為引數傳遞呼叫- 查詢可以響應
NSInvocation
中編碼的訊息的物件(對於所有訊息,此物件不必相同) - 使用
anInvocation
將訊息傳送到該物件。anInvocation
將儲存結果,執行時系統將提取結果並將其傳遞給原始傳送者
4.慢速流程解決崩潰
慢速流程
流程就是先methodSignatureForSelector
提供一個方法簽名,然後forwardInvocation
通過對NSInvocation
來實現訊息的轉發
#import "FXTeacher.h"
@implementation FXSon
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(doInstanceNoImplementation)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s ",__func__);
SEL aSelector = [anInvocation selector];
if ([[FXTeacher alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[FXTeacher alloc]];
else
[super forwardInvocation:anInvocation];
}
@end
複製程式碼
四、訊息轉發機制流程圖
寫在後面
有興趣的小夥伴們可以看看Demo,加深對OC訊息機制的理解和防崩潰的運用