iOS底层-消息的转发

语言: CN / TW / HK

前言

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

动态决议

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

image-20220509213807681

这是方法开始就声明的

image-20220509213923958

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

image-20220509214100090

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

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

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

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

c++ // 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:

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

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

image-20220509220541124

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

c++ 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(内部包含方法的动态决议),

```c++

/********** * 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:);调用一次类里的该方法,如果有实现的话,就能找到。尝试一下:

```objc

import

@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用字符串描述:"[email protected]:"

```objc 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);

} ```

方法修改:

```objc +(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, "[email protected]:");
}

return [super resolveInstanceMethod:sel];

}

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

可以看到运行正常了:

image-20220511203308000

回到决议方法:

image-20220509231159865

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

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

然后LLDB调试打印出方法:

```c / 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

增加代码验证一下:

```objc +(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, "[email protected]:");
}

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:

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

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

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

消息转发

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

消息的快速转发

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

image-20220511163017074

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

```c++ /********** * 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关键字导出这个方法

```objc

import

@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

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

示例:

```objc

import

@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

修改代码再运行看看

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

直接崩溃了。

image-20220511214840157

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

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

再运行,就不奔溃了;

image-20220511215348127

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

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

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

```objc 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 ```

补充一些代码:

objc - (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,这个方法里包含了动态决议,这才造成了二次动态决议。

总结

动态决议

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

实例方法的动态决议

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

类方法的动态决议

+ (BOOL)resolveClassMethod:(SEL)sel;

消息转发

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

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

消息快速转发

objc - (id)forwardingTargetForSelector:(SEL)aSelector;

消息慢速转发

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

AOP与埋点

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

动态决议二次调用

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