歡迎閱讀iOS探索系列(按序閱讀食用效果更加)c++
上一篇文章講了方法在底層是如何經過sel
找到imp
的,本文就將經過源碼來研究「沒有實現的方法在底層要經過多少關卡才能發出unrecognized selector sent to instance
並Crash
」,看完本文後你會明白程序崩潰也是一個很複雜的過程git
在動態方法決議源碼中,FXSon
中有兩個只聲明未實現的方法,分別調用它們:github
- (void)doInstanceNoImplementation;
+ (void)doClassNoImplementation;
消息查找流程
部分再也不展開講解,未實現方法
查找主要通過如下流程:緩存
isa
平移獲得class
,內存偏移獲得cache->buckets
查找緩存因爲慢速流程調用的是lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/)
,遍歷父類無果後來到動態方法解析
bash
只有resolver
和triedResolver
知足條件下才會進入動態方法解析
post
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;
}
複製代碼
動態方法解析按調用方法走不一樣分支:ui
元類
的話說明調用類方法,走_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);
}
}
}
複製代碼
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,因此只會在本類和父類中查找,並不會動態方法解析
spa
但cls沒有這個方法,其實根類NSObject
已經實現了這個方法(NSProxy
沒有實現)3d
// 具體搜索 NSObject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
複製代碼
②向本類發送SEL_resolveInstanceMethod
消息,即調用這個方法
③lookUpImpOrNil
再次查找當前實例方法imp,找到就填充緩存,找不到就返回
④結束動態方法解析
,回到lookUpImpOrForward
方法將triedResolver
置否並goto retry
從新查找緩存和方法列表
相較於實例方法,類方法就複雜多了
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
從新查找緩存和方法列表
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
複製代碼
實例方法
能夠重寫resolveInstanceMethod
添加imp
類方法
能夠在本類重寫resolveClassMethod
往元類添加imp
,或者在NSObject分類
重寫resolveInstanceMethod
添加imp
動態方法解析
只要在任意一步lookUpImpOrNil
查找到imp
就不會查找下去——即本類
作了動態方法決議,不會走到NSObjct分類
的動態方法決議NSObject分類
重寫resolveInstanceMethod
添加imp
解決崩潰那麼把全部崩潰都在NSObjct分類
中處理,加之前綴區分業務邏輯,豈不是美滋滋?錯!
NSObjct分類
動態方法決議以前已經作了處理這也不行,那也不行,那該怎麼辦?放心,蘋果爸爸已經給咱們準備好走路了!
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
forwardingTargetForSelector
在源碼中只有一個聲明,並無其它描述,好在幫助文檔中提到了關於它的解釋:
forwardInvocation:
方法進行處理objc_msgSend(forwardingTarget, sel, ...);
來實現消息的發送以下代碼就是是經過快速轉發解決崩潰——即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
複製代碼
在快速流程找不到轉發的對象後,會來到慢速流程methodSignatureForSelector
依葫蘆畫瓢,在幫助文檔中找到methodSignatureForSelector
點擊查看forwardInvocation
forwardInvocation
和methodSignatureForSelector
必須是同時存在的,底層會經過方法簽名,生成一個NSInvocation
,將其做爲參數傳遞調用NSInvocation
中編碼的消息的對象(對於全部消息,此對象沒必要相同)anInvocation
將消息發送到該對象。anInvocation
將保存結果,運行時系統將提取結果並將其傳遞給原始發送者
慢速流程
流程就是先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消息機制的理解和防崩潰的運用