欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
写在前面
上一篇文章讲了方法在底层是如何通过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_resolveClassMethod
非元类
的话调用了实例方法,走_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消息机制的理解和防崩溃的运用