草庐IT

探秘Runtime - Runtime Message Forward

刘小壮 2023-09-17 原文

该文章属于刘小壮原创,转载请注明:刘小壮


当一个对象的方法被调用时,首先在对象所属的类中查找方法列表,如果当前类中没有则向父类查找,一直找到根类NSObject。如果始终没有找到方法实现,则进入消息转发步骤中。

动态消息解析

当一个方法没有实现时,也就是在cache lsit和其继承关系的method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime会给一次机会动态添加方法实现。

可以通过重写resolveInstanceMethod:resolveClassMethod:方法,动态添加未实现的方法。其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个BOOL返回值,返回NO则进入消息转发机制。

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:sel];
}

在通过class_addMethod函数动态添加实现时,后面有一个"v@:"来描述SEL对应的函数实现,具体的描述可以参考官方文档

Message Forwarding

Message Forward

在进行消息转发之前,还可以在forwardingTargetForSelector:方法中将未实现的消息,转发给其他对象。可以在下面方法中,返回响应未实现方法的其他对象。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorName = NSStringFromSelector(aSelector);
    if ([selectorName isEqualToString:@"selector"]) {
        return object;
    }
    return [super forwardingTargetForSelector:aSelector];
}

forwardingTargetForSelector:方法未做出任何响应的话,会来到消息转发流程。消息转发时会首先调用methodSignatureForSelector:方法,在方法内部生成NSMethodSignature类型的方法签名对象。在生成签名对象时,可以指定targetSEL,可以将这两个参数换成其他参数,将消息转发给其他对象。

[otherObject methodSignatureForSelector:otherSelector];

生成NSMethodSignature签名对象后,就会调用forwardInvocation:方法,这是消息转发中最后一步了,如果在这步还没有对消息进行处理,则会导致崩溃。

这个方法中会传入一个NSInvocation对象,这个对象就是通过刚才生成的签名对象创建的,可以通过invocation调用其他对象的方法,调用其invokeWithTarget:即可。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([object respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:object];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

消息转发

将一条消息发送给一个不能处理的对象会引起崩溃,但是在崩溃之前,系统给响应对象一次处理异常的机会。

当发送一条对象不能处理的消息,产生Crash之前,系统会调用响应者的forwardInvocation:方法,并传入一个NSInvocation对象,NSInvocation对象中包含原始消息及参数。这个方法只有方法未实现的时候才会调用。

你可以实现forwardInvocation:方法,将消息转发给另一个对象。forwardInvocation:方法是一个动态方法,在响应者无法响应方法时,会调用forwardInvocation:方法,可以重写这个方法实现消息转发。

消息转发中forwardInvocation:需要做的是,确认消息将发送到哪里,以及用原始参数发送消息。可以通过invokeWithTarget:方法,发送被转发的消息。调用invoke方法后,原方法的返回值将被返回给调用方。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:someOtherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

forwardInvocation:方法不只可以处理一个方法,可以通过Selector进行判断,来处理多个需要转发的方法。

动态方法解析

OC中有时候可以动态的提供方法实现,例如属性可以通过@dynamic propertyName;的形式,表示将在运行过程中动态的提供属性方法。

如果想实现动态方法解析,需要实现resolveInstanceMethod:resolveClassMethod:方法,在这两个方法中动态的添加方法实现。通过class_addMethod方法可以动态添加方法,添加方法时需要关联对应的函数指针,函数指针需要声明两个隐藏参数self_cmd

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL {
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSel];
}

消息转发和动态方法解析大部分是相同的,在消息转发之前一个类有机会动态解析这个方法。如果已经调用respondsToSelector:instancesRespondToSelector:方法,动态方法解析有机会优先为Selector添加IMP。如果你实现了resolveInstanceMethod:方法,但想要特定的Selector走消息转发流程,则将此方法返回NO即可。

转发和多继承

可以通过消息转发机制来模拟多继承,例如下面这张图中,两个类中虽然不存在继承关系,但是却由另一个类处理了Warrior的事件。

多继承

由上面的例子可以看出,分属两个继承分支的类,通过消息转发机制实现了继承的关系。Warriornegotiate消息由其“父类”Diplomat来实现。

通过消息转发实现的多重继承相对于普通继承来说更有优势,消息转发可以将消息转发给多个对象,这样就可以将代码按不同职责封装为不同对象,并通过消息转发给不同对象处理。

需要注意的是,Warrior虽然通过消息转发机制可以响应negotiate消息,但如果通过respondsToSelector:isKindOfClass:方法进行判断的话,依然是返回NO的。如果想让这两个方法可以在判断negotiate方法时返回YES,需要重写这两个方法并在其中加入判断逻辑。

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([super respondsToSelector:aSelector]) {
        return YES;
    } else {
        // 
    }
    return NO;
}

在执行forwardInvocation:之前,需要通过methodSignatureForSelector:方法返回方法签名,如果不实现则用默认的方法签名。

在方法签名的过程中,可以将未实现的方法转发给其代理。

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

runtime类型编码

使用技巧

在项目中经常会出现因为调用未实现的方法,导致程序崩溃的情况。在学习消息转发后,就可以通过消息转发来解决这个问题。

所有的类的基类都是NSObject类(NSProxy除外),可以将NSObject类的消息转发流程拦截,然后做一些统一的处理,这样就可以解决方法未实现导致的崩溃。根据Category可以将原类方法“覆盖”的特点,可以在Category中实现相应的拦截方法。

// 自定义类
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
- (void)testMethod;
@end

// 接收消息的IMP
void dynamicResolveMethod(id self, SEL _cmd) {
    NSLog(@"method forward");
}

// 对NSObject创建的Category
@implementation NSObject (ExceptionForward)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    const char *types = sel_getName(sel);
    class_addMethod([self class], sel, (IMP)dynamicResolveMethod, types);
    return YES;
}

#pragma clang diagnostic pop

@end

我们的拦截方案是在resolveInstanceMethod方法中,动态创建未实现的方法,并将IMP统一设置为dynamicResolveMethod函数进行处理。这样所有未实现的方法都会执行dynamicResolveMethod函数,而不崩溃,在dynamicResolveMethod函数中可以做崩溃统计等操作。

多继承

OC中是不支持多继承的,但是可以通过消息转发模拟多继承。在子类中实例化多个父类,当消息发送过来的时候,在消息转发的方法中,将调用重定向到父类的实例对象中,以实现多继承的效果。

下面是多继承的例子,创建两个父类CatDog,并将需要子类继承的方法都定义到Protocol中,在CatDog中实现Protocol中的方法。

@protocol CatProtocol <NSObject>
- (void)eatFish;
@end

@interface Cat : NSObject <CatProtocol>
@end

@implementation Cat
- (void)eatFish {
    NSLog(@"Cat Eat Fish");
}
@end

@protocol DogProtocol <NSObject>
- (void)eatBone;
@end

@interface Dog : NSObject
@end

@implementation Dog
- (void)eatBone {
    NSLog(@"Dog Eat Bone");
}
@end

子类直接通过遵守父类的协议,来表示自己“继承”哪些类,并在内部实例化对应的父类对象。在外界调用协议方法时,子类其实是没有实现这些父类的方法的,所以通过转发方法将消息转发给响应的父类。

@interface TestObject : NSObject <CatProtocol, DogProtocol>
@end

@interface TestObject()
@property (nonatomic, strong) Cat *cat;
@property (nonatomic, strong) Dog *dog;
@end

@implementation TestObject

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.cat respondsToSelector:aSelector]) {
        return self.cat;
    } else if ([self.dog respondsToSelector:aSelector]) {
        return self.dog;
    } else {
        return self;
    }
}

// 忽略Cat和Dog的初始化过程
@end

简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我Github上,下载Runtime PDF合集。把所有Runtime文章总计九篇,都写在这个PDF中,而且左侧有目录,方便阅读。

Runtime PDF

下载地址:Runtime PDF
麻烦各位大佬点个赞,谢谢!?

有关探秘Runtime - Runtime Message Forward的更多相关文章

  1. javascript - rake 数据库 :create - Could not find a JavaScript runtime - 2

    这是我得到的错误:sergio@sergio-VirtualBox:~/blog$rakedb:createrakeaborted!CouldnotfindaJavaScriptruntime.Seehttps://github.com/sstephenson/execjsforalistofavailableruntimes.(Seefulltracebyrunningtaskwith--trace)sergio@sergio-VirtualBox:~/blog$所以经过一些搜索,我似乎需要为Ruby安装一个Javascript运行时。许多不同的选项之间有区别吗?看来Node.js是

  2. ruby-on-rails - add_dependency 和 add_runtime_dependency 之间的区别? - 2

    在Rails引擎的gemspec中使用add_dependency和add_runtime_dependency有什么区别?例如:Gem::Specification.newdo|s|s.add_dependency'jquery-rails's.add_runtime_dependency'jquery-rails'end它们有什么区别? 最佳答案 它们是一样的。add_dependency只是一个alias对于add_runtime_dependency。 关于ruby-on-rai

  3. javascript - chrome.runtime.sendMessage 在重新加载 Chrome 扩展程序后从内容脚本中抛出异常 - 2

    我将消息从注入(inject)的内容脚本发送回我的Chrome扩展程序中的后台脚本:chrome.runtime.sendMessage({action:"myResult"});这工作正常,直到我重新加载我的扩展程序(通过转到“设置”->“扩展程序”->“为我的扩展程序重新加载(Ctrl+R)”。)反过来,当我的后台脚本启动时,它会为所有打开的标签反复调用chrome.tabs.executeScript,以编程方式重新注入(inject)我的内容脚本(asIshowedinthisquestion.)但是在我这样做之后,如果我从我的内容脚本调用第一行sendMessage,它会抛出

  4. javascript - HTMl+SVG+JavaScript : change text at runtime? - 2

    我准备将一些基于Flash的应用程序迁移到HTML+JavaScript+SVG(单一目标渲染引擎是Webkit)。我对SVG完全陌生,我想知道我是否可以使用SVG作为模板图像来填充屏幕并从HTML页面中嵌入的JavaScript代码动态更改包含的文本。我想做的是:在Inkscape中绘制页面的基本结构(带有一些图形和文本占位符),然后仅在HTML页面中显示SVG并通过JavaScript填充文本占位符。我可以通过在背景中显示静态SVG图像并在其顶部放置一些绝对定位的DIV来显示文本来获得相同的结果,但是我'我想在Inkscape中设计文本标签的位置、大小和样式。这能做到吗?怎么办?我

  5. javascript - 从 chrome 商店安装扩展时,chrome.runtime 未定义 - 2

    当我使用googlechrome浏览器console.log(chrome)时,我获得了某些属性,但我发现chrome的“运行时”属性不可用。app:Objectcsi:function(){nativefunctionGetCSI();returnGetCSI();}loadTimes:function(){nativefunctionGetLoadTimes();returnGetLoadTimes();}webstore:Object__proto__:Object__defineGetter__:function__defineGetter__(){[nativecode]}_

  6. javascript - 通过 chrome.runtime.sendMessage 发送带有函数的对象 - 2

    我正在开发一个chrome扩展,我想用chrome.runtime.sendMessage发送一个对象(具有一些功能)。现在做这样的事情chrome.runtime.sendMessage({something:"Funny"});工作正常。但是一旦我想创建更复杂的东西,我的消息似乎就是一个空对象。functionFunnyFunction(){return42;}varexampleObject=newObject();exampleObject.FunnyFunction=FunnyFunction;chrome.runtime.sendMessage({something:exa

  7. javascript - jquery.validate.js 内部错误 "javaScript runtime error: Unable to get property ' 调用'未定义或空引用“ - 2

    我正在开发一个asp.netmvc-5网络应用程序。我正在使用这些脚本:-jquery1.10.2jquery-ui1.8.24jQuery验证插件1.11.1现在当我在visualstudio2013中运行应用程序时,IE10会出现以下异常,而在chrome或firefox上不会有任何问题:-Unhandledexceptionatline1234,column5inhttp://localhost:49392/Scripts/jquery.validate.js0x800a138f-JavaScriptruntimeerror:Unabletogetproperty'call'of

  8. c# - jQuery "Microsoft JScript runtime error: Object expected" - 2

    我有以下代码似乎根本不起作用:(我不断收到:MicrosoftJScriptruntimeerror:Objectexpected错误似乎是在超时完成时发生的。因此,如果我将超时时间提高10秒,错误会再持续10秒。我希望能够异步更新在线好友数量。该号码显示在以下html中:Friendsonline(?)friend部分在第一次运行时设置,但是当超时回调时它不会再次触发。另外,我看不到错误发生在哪一行,因为如果我想中断错误,它只会显示“无源代码”等。下面的代码是我正在使用的代码。谢谢!$(document).ready(function(){UpdateFriendsOnline();

  9. javascript - Webpack babel-loader runtime : Module build failed: TypeError: this. setDynamic 不是函数 - 2

    我正在尝试将babel-loader与babel-plugin-transform-runtime一起使用。我已按照以下说明进行操作:https://github.com/babel/babel-loader#babel-is-injecting-helpers-into-each-file-and-bloating-my-code相关代码:rules:[//the'transform-runtime'plugintellsbabeltorequiretheruntime//insteadofinliningit.{test:/\.js$/,exclude:/(node_modules

  10. javascript - chrome.runtime 是否支持发布带有可传输对象的消息? - 2

    我正在学习可转移对象:http://updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast它们看起来非常棒,我想在我的扩展中使用它们来加速从iframe内部到iframe外部的数据传递。我已经让这个示例代码正常工作,但它使用了WebWorker:vars=document.createElement("script");s.type="text/js-worker";s.innerHTML='console.log("iran!");';document.body.appendChild(s);varblob

随机推荐