iOS底层-消息的转发

前言

上篇文章介绍了方法调用的本质是消息发送。那如果经过查找后,没有找到方法,系统会怎么处理?这就是本文接下来介绍的方法的动态决议消息转发

动态决议

当方法查找一直查到父类为nil之后,有imp赋值为forward_imp这个操作

image-20220509213807681

这是方法开始就声明的

image-20220509213923958

通过源码无法找到实现,然后在汇编里找到了:

image-20220509214100090

TailCallFunctionPointer只是函数调用,没有什么研究价值;

// jop
.macro TailCallFunctionPointer
	// $0 = function pointer value
	braaz	$0
.endmacro

// not jop
.macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0
.endmacro

再看前面两行汇编代码提到的_objc_forward_handler:

// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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;

指针指向的方法objc_defaultForwardHandler就在上面,熟悉的报错信息:unrecognized selector sent to instance

这里还体现了类方法和实例方法的判断,仅仅是通过class_isMetaClass(是不是元类)来区分,再次证明底层没有类方法和实例方法的区别。

回到lookUpImpOrForward方法,这里还没有调用这个imp方法,只是赋值。也就是在报错前,会把for循环当前流程走完。

image-20220509215237260

下面一段逻辑,注释提到执行一次method resolver

image-20220509215439452

这个地方的判断相当于一个单例的效果。打个断点跑一下源码:

image-20220509215720780

这里behavior进来时初始值就是3,

image-20220509215806626

LOOKUP_RESOLVER = 2; 也就是说if判断是 3 & 2 = 2,第一次必定进入代码块内部,^ 是异或运算,二进制位相同为0,不同为1:

behavior ^= LOOKUP_RESOLVER // 3 ^ 2 = 011 ^ 010 = 001 = 1;

然后传入resolveMethod_locked方法,会调用一次动态决议方法,稍后再细说,这里先看一下方法的结尾,

image-20220509220541124

来到lookUpImpOrForwardTryCache方法,实际调用的是_lookUpImpTryCache方法;

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

进入_lookUpImpTryCache源码,可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImpcache里找一遍方法的sel

image-20220510155210696

如果还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward

这时候进lookUpImpOrForward方法,这里behavior传的是1了。执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时,就是 1 & 2 = 0,不会再进入里面的代码块,这就是为什么说相当于单例。

那么确定第一次执行会进入resolveMethod_locked(内部包含方法的动态决议),


/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

可以看到两个方法:resolveInstanceMethodresolveClassMethod。也称为方法的动态决议

实例方法的动态决议

我们可以在类里面重写这2个方法,为我们没有实现的方法,通过runtime的api进行动态添加方法实现。(对sel动态的添加imp)

image-20220509223237837

接收者cls,说明这是一个类方法,看到这里,总结一下:

当方法找不到的时候,在进行报错之前,还会通过@selector(resolveInstanceMethod:);调用一次类里的该方法,如果有实现的话,就能找到。尝试一下:


#import <Foundation/Foundation.h>


@interface Goods : NSObject

-(void)introduce;

@end

@implementation Goods

+(BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        Goods *goods = [[Goods alloc] init];
        [goods introduce];
    }
    return 0;
}

运行

image-20220511174221873

可以看到为什么会有2次执行呢?放到最后再讲。类方法也是如此。

既然是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtimeclass_addMethod,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

方法修改:

+(BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
    
    if (sel == @selector(introduce)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(addMethod));
        class_addMethod(self.class, sel, imp, "v@:");
    }
    
    return [super resolveInstanceMethod:sel];
}

-(void)addMethod {
    NSLog(@"%s", __func__);
}

可以看到运行正常了:

image-20220511203308000

回到决议方法:

image-20220509231159865

动态添加实现之后,还会从cache里找imp。试一下能不能找到:

执行实例方法前,正好扩容。(goods.class系统会自动添加2个方法 + init,达到 3/4 扩容条件)

然后LLDB调试打印出方法:

/*
 x/4gx goods.class
 p (cache_t *)0x100008930
 p *$1
 p $2.buckets()
 p *$3
 p $3+4
 p $6.sel()
 */

成功找到:

image-20220511195504295

确实添加进去了。注意,这里sel虽然是introduce,但是imp可是addMethod

类方法的动态决议

再看这里,当判断是元类的时候,也就是类方法找不到,会调用resolveClassMethod

image-20220510161115415

增加代码验证一下:

+(BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
    
    if (sel == @selector(introduce)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(classMethod));
        class_addMethod(objc_getMetaClass("Goods"), sel, imp, "v@:");
    }
    
    return [super resolveInstanceMethod:sel];
}

-(void)classMethod {
    NSLog(@"%s", __func__);
}

运行:

image-20220511200955203

扩展1:如果添加的方法没有实现,并且实例的动态决议也不添加方法。

resolveClassMethod也是调用了2次,其中第二次进入的sel是_forwardStackInvocation,这就是文章后面会涉及到的消息转发。

image-20220512101545796

扩展2:如果把if判断都去掉

打印结果竟然去执行了addMethod实例方法;

image-20220511202601054

注意看源码这里:如果cache里没有(因为类方法没有找到,就不会添加到cache里),会调用实例方法:

image-20220510161333199

由于去掉了方法名判断,所以最终找到实例方法addMethod去了;

iOS为什么这么做呢?首先,类方法是去元类找到的,那这个类方法的动态决议,正常应该放到元类里的。

但是我们无法在元类里写代码,如果系统没提供resolveClassMethod,如何进行动态决议呢?

结合消息发送的流程,以及类的继承链,可以想到,把resolveInstanceMethod方法写到NSObject分类里面。因为子类没有就会从父类找,直到找到NSObject分类里,所以也是能够解析到的。

不过这个消息的查找流程比较长,影响效率。所以才有了resolveClassMethod,来给类方法提供动态实现,目的是简化类方法的查找流程,直接在当前类里实现。

进一步理解动态决议

如果方法实现改成从元类里获取?结果死循环。

image-20220511203835729

梳理一下流程:

  • 调用类方法allGoods,因为没有实现,所以调用resolveClassMethod;
  • 从元类里查找classMethod,元类里自然没有实例方法的实现,所以找不到。进行动态决议;
  • 又回到resolveClassMethod; 如此循环;

上一个例子能找到实例方法的实现,因为传的不是元类。

image-20220511204021745

这些机制有什么应用场景呢?

AOP埋点的思路

将代码粘贴到NSObject分类里。

再也不会出现方法找不到的崩溃了,resolveClassMethod方法也不用了。因为最终会找到NSObject这个分类里。有点AOP(面向切面编程)的意思,常用于埋点。应用场景:在该分类提示/上报没有实现的imp。

对效率有什么影响?这是系统提供的防止崩溃的手段,主要为了保证系统的稳定性,非必要不使用。

写一个demo,记录页面停留时间。记录单个页面:

image-20220513194111636

如果页面非常多呢?给父类ViewController加一个分类

image-20220513194547843

通过方法交换,所有的控制器就添加了埋点。注意保证方法只被交换一次,还需要借助dispatch_once

能走到这个分类的原因是什么?原方法里的super:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
}

本质是还是通过消息发送,从父类里找方法实现,才能找到分类里交换的方法。所以重载这个方法的时候,一定记得调用父类的方法 。

消息转发

如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。

消息的快速转发

方法找到后会执行done代码块

image-20220511163017074

进入log_and_fill_cache方法,插入cache前还有个判断

/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled. 
* implementer is the class that owns the implementation in question.
**********************************************************************/
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
    cls->cache.insert(sel, imp, receiver);
}

是往文件里写入信息

image-20220511164750343

前面的判断if (slowpath(objcMsgLogEnabled && implementer)), 入参implementer是上个方法的curClass,所以必定有值;那么看看objcMsgLogEnabled

image-20220510221430448

默认值是false,接着搜索一下哪里使用到;发现在instrumentObjcMessageSends,方法里进行赋值

image-20220510221547202

搞个demo试一下,通过extern关键字导出这个方法

#import <Foundation/Foundation.h>

@interface Goods : NSObject

-(void)introduce;

@end

@implementation Goods

-(void)introduce {
}

@end

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        
        Goods *goods = [Goods alloc];
        instrumentObjcMessageSends(YES);
        [goods introduce];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

运行后,来到logMessageSend方法提到的目录tmp

image-20220511212042476

打开文件

image-20220511212117210

如果把方法实现注释掉,在运行看看log文件里多出了什么

image-20220511211831408

在方法崩溃前调用的方法栈记录。可以看到,如果没有实现方法,以及没有重写动态决议,系统会进行了上面两个方法的调用,这就是消息快速转发

示例:


#import <Foundation/Foundation.h>

@interface FFAnimal : NSObject

- (void)func1;
+ (void)func2;

@end

@interface FFTiger : NSObject

- (void)func1;
+ (void)func2;

@end

@implementation FFAnimal

-(id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));

    if (aSelector == @selector(func1)) {
        return [FFTiger alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

@implementation FFTiger

- (void)func1 {
    NSLog(@"%s",__func__);
}

+ (void)func2 {
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FFAnimal *animal = [FFAnimal alloc];
        [animal func1];
    }
    return 0;
}

运行:

image-20220511213714037

转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。

这时候方法缓存在哪?接收转发消息的对象

应用场景:专门搞一个类,来处理这些无法响应的消息。方法找不到时的crash收集。

演示的是实例方法,如果是类方法,只需要将 - 改成 + ;修改完运行:

image-20220511213945968

消息的慢速转发

如果消息的快速转发也没有找到方法;回看日志,后面还有个methodSignatureForSelector方法,作用是方法有效性签名。

image-20220511212331401

修改代码再运行看看

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

直接崩溃了。

image-20220511214840157

因为方法签名需要搭配另一个方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation;

再运行,就不奔溃了;

image-20220511215348127

在调用func1时,虽然没有提供方法实现,但是在了方法的慢速转发里提供了有效签名(只要格式正确,和实际返回类型不同也行),代码就不崩溃了。

防止系统崩溃的三个救命稻草:动态解析、快速转发、慢速转发。

forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了targetselector用于指定目标里查找方法实现。

NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available") // swift不能用
@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

补充一些代码:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    FFTiger *t = [FFTiger alloc];
    // 如果自己能响应
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self];
    }
    // 实例能响应
    else if ([t respondsToSelector:anInvocation.selector] ) {
        [anInvocation invokeWithTarget:t];
    }
    // 都无法响应
    else {
        NSLog(@"功能开发中,敬请期待");
    }
}

应用场景:统一处理没实现的方法,进行提示。你也可以不做任何处理,这样消息找不到的崩溃就不会出现了。

不过救命稻草不能解决实际问题,只是为了app稳定性的一种手段。

流程图:

image-20220511220946406

如果每个流程走到最后,就是日志里的doesNotRecognizeSelector方法:

image-20220511221259764

触发后面打印的崩溃信息。

这个救命稻草一般写在哪?NSObject的分类里,这样只要写一次。

两次动态决议的原因

还是前面的demo,然后注释方法实现。断点看一下:(方法最好不要放在NSObject分类里,放到本类里比较方便)

image-20220512200233774

第一次因为uncache进入;第二次是消息转发:

image-20220512200421243

lldb输入指令bt可以看到打印的信息,里面调用了___forwarding___符号。

image-20220512200532099

上一行是熟悉的慢速转发methodSignatureForSelector方法;在这些CoreFoundation框架的方法之后,第一个调用的方法是class_getInstanceMethod,源码里找一下实现:

image-20220512201152287

梳理一下:在消息的第一次动态决议和快速转发都没找到方法后,进入到慢速转发。过程中,runtime还会调用一次lookUpImpOrForward,这个方法里包含了动态决议,这才造成了二次动态决议。

总结

动态决议

通过消息发送机制也找不到方法,系统在进入消息转发前,还会进行动态决议。

实例方法的动态决议

+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 系统通过该方法调用上面OC类里的实现
static void resolveInstanceMethod(id inst, SEL sel, Class cls) 

类方法的动态决议

+ (BOOL)resolveClassMethod:(SEL)sel;

消息转发

动态决议也找不到方法,才真正进入消息转发环节。

动态决议、快速转发、慢速转发合称为三个救命稻草,用于防止方法查找导致的系统崩溃。

消息快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector;

消息慢速转发

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;

AOP与埋点

面向切面编程(AOP)在不修改源代码的情况下,通过运行时给程序添加统一功能的技术。 埋点就是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况,然后精准分析用户数据。 比如⻚面停留时间、点击按钮、浏览内容等等。

动态决议二次调用

慢速转发过程中,通过runtime又调用了一次lookUpImpOrForward方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值