[TOC]
动态方法解析与消息转发的应用
2020-03-14
动态方法解析(dynamic method resolution)和消息转发(message forwarding)都是 Objective-C runtime 中实现动态消息响应的机制。消息转发有两种方式:快速消息转发(fast forwarding)、完整消息转发(normal forwarding)。本文主要从流行的开源第三方库中,挑选几个使用了动态消息响应的模块,并结合该模块的源代码分析消息转发的具体使用场景。
一、动态方法解析的应用
当对象/类调用目标实例方法/类方法时:若在对象的类/类的元类的继承链上搜索均不到目标实例方法/类方法,则 runtime 会调用类的**+(BOOL)resolveInstanceMethod:(SEL)sel/+(BOOL)resolveClassMethod:(SEL)sel,其中sel参数传入目标实例方法/类方法的 selector,若返回YES则 runtime 会再次尝试在对象的类/类的元类的继承链上搜索目标实例方法/类方法**(注意第二次搜索不会进入方法动态解析过程),若搜索到目标实例方法/类方法则该方法就得到响应,若搜索不到则进入消息转发流程。
因此可以断定,实现方法动态解析,是在+(BOOL)resolveInstanceMethod:(SEL)sel/+(BOOL)resolveClassMethod:(SEL)sel中,为类/元类的方法列表动态添加实例方法/类方法。
注意:动态方法解析实际上不属于消息转发的范畴,因为方法动态解析时,消息的接收者始终是对象本身或者对象的类,网上有很多文章将方法动态解析归如消息转发流程实际上是不严谨的。
1.1 基本使用范例
从方法动态解析的原理不难发现,其功能实现是和单个 Class 强耦合的,因此通常用于具体业务开发中,在开源第三方框架中很少见到方法动态解析的身影。以下实现代码为例,动态方法解析给这块业务带来的好处有:
- 接口兼容:额外支持
saySomething接口; - IMP 复用:具有相同实现逻辑的
saySomething和sayHello接口,统一使用sayHello的 IMP;
@implementation HelloDynamicMethodResolution
- (void)sayHello{
NSLog(@"Hello!");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(saySomething)) {
IMP sayHelloIMP = class_getMethodImplementation(self,@selector(sayHello));
Method sayHelloMethod = class_getInstanceMethod(self,@selector(sayHello));
const char *sayHelloType = method_getTypeEncoding(sayHMethod);
// 实例方法添加到类的方法列表
return class_addMethod(self,sel,sayHelloIMP,sayHelloType);
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(saySomething)) {
IMP sayHelloIMP = class_getMethodImplementation(self,@selector(sayHello));
Method sayHelloMethod = class_getInstanceMethod(self,@selector(sayHello));
const char *sayHelloType = method_getTypeEncoding(sayHMethod);
// 类方法添加到元类的方法列表
return class_addMethod(objc_getMetaClass(class_getName([self class])),sel,sayHelloIMP,sayHelloType);
}
return [super resolveInstanceMethod:sel];
}
@end
注意:由于 IMP 的本质就是函数指针,因此类方法和实例方法复用同一个 IMP 是没有问题的,除非实例方法的 IMP 引用了实例变量,此时 IMP 就不能被类方法复用。
1.2 动态方法解析进阶
在 1.1 中动态方法解析添加方法时,直接使用了实例方法/类方法列表中的 IMP。进一步结合实际业务情况考虑。假设 APP 的SomeModule类有一块功能,它很少被用到但是不可或缺,例如 VVVVVIP 功能。
-
若定义
VVVVVIP业务类,或定义SomeModule的VVVVVIP分类,则在 APP 的运行加载阶段,runtime 必定需要加载VVVVVIP业务类或分类,但是这个类的功能很少用到,开发者压根就不想在程序启动时加载任何关于该功能的元素; -
若采用 1.1 中的方法,除非有可复用的 IMP,不然动态方法解析机制根本没起到任何促进作用,反而凭空在响应 VVVVVIP 接口时增加了动态添加方法的操作,有点画蛇添足的感觉;
因此上述两种方式都不是好的选择。实际上还可以使用函数指针定义 IMP 来实现 VVVVVIP 业务功能,再结合动态方法解析启用这些功能。由于函数指针是直接编译到 APP 的可执行文件,因此不会增加类的方法列表大小。另外,函数指针在 APP 加载阶段不需要经 runtime 处理,因此也不会为 APP 加载过程的带来额外负担。
注意:动态链接库可以完美解决上述业务场景,可惜 iOS 系统不支持自定义动态链接库或者说不允许使用自定义动态链接库的 iOS 应用上架到 AppStore。
使用函数指针的动态方法解析的实现范例如下:
void vvvvvip_sayHelloIMP(id slf, SEL sel){
NSLog("Hello VVVVVIP");
}
@implementation SomeModule
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(saySomething)) {
void(*funcPtr)(id, SEL) = vvvvvip_sayHelloIMP;
IMP imp = (IMP)funcPtr;
const char* type = "v16@0:8";
class_addMethod(objc_getMetaClass(class_getName([self class])), @selector(saySomething), imp, type);
// 实例方法添加到类的方法列表
return class_addMethod(self,sel,sayHelloIMP,sayHelloType);
}
return [super resolveInstanceMethod:sel];
}
@end
最后必须要承认的现实是,即使是用函数指针的方式实现方法动态解析,对 APP 的可执行文件大小、APP 加载过程负担起到的优化作用也是十分有限的,因此方法动态解析的主要功能还是 1.1 中提到的接口兼容及 IMP 复用。
二、快速消息转发的应用
当对象/类调用目标实例方法/类方法并进入了动态方法解析阶段,且**+(BOOL)resolveInstanceMethod:(SEL)sel/+(BOOL)resolveClassMethod:(SEL)sel**方法返回NO时,就会进入快速转发流程(fast forwarding)。快速转发流程是将对象接收到的消息,转发到一个备用响应对象去响应消息。
2.1 基本使用范例
备用响应对象的指定通过重写类的-(id)forwardingTargetForSelector:(SEL)aSelector实例方法实现。基本范例代码如下:
/** 消息备用接收者 */
@implementation ForwardingTarget
- (void)sayHello{
NSLog(@"Hello!");
}
@end
/** 消息第一接收者 */
@implementation HelloFastForwarding
-(id)forwardingTargetForSelector:(SEL)aSelector{
if(aSelector == @selector(saySomething){
return [ForwardingTarget new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
1.2 快速消息转发进阶
快速转发可以指定一个与自己关联的对象去响应消息,这很容易让人联想到设计模式中的代理模式。代理是一个不包含具体业务实现逻辑的对象,它只暴露了一套接口,所有通过这套接口的访问均直接转发到真实接收者去响应。
Foundation 框架中提供了NSProxy类专门用于定义代理,其定义如下。NSProxy遵循NSObject协议,说明其表征的同样是一个对象,它具有接收消息、接受引用计数内存系统管理、继承链等对象的基本特征。但是它不具有NSObject类型的copy系列方法,甚至连init方法都没有的“非常规行为”,暴露了NSProxy的价值并不在于表征数据,而在于消息转发。
@interface NSProxy <NSObject> {
Class isa;
}
+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
// - (id)forwardingTargetForSelector:(SEL)aSelector;
@end
SDWebImage项目中定义了SDWeakProx就是用NSProxy实现的。其关键代码如下,注意SDWeakProxy只包含一个属性target,target作为消息的真实接收者,SDWeakProxy对象接收到的所有消息均通过快速转发机制转发到target属性所指向的对象去响应。
而且**SDWeakProxy对象对target的引用是弱引用**,也就是说当target属性所指向的对象被释放时,target属性会自动置nil,即SDWeakProxy对target属性所指向的对象的引用不复存在,这一点非常关键。
@interface SDWeakProxy : NSProxy
@property (nonatomic, weak, readonly, nullable) id target;
- (nonnull instancetype)initWithTarget:(nonnull id)target;
+ (nonnull instancetype)proxyWithTarget:(nonnull id)target;
@end
@implementation SDWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[SDWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
...
@end
Weak Proxy 是为了很好的解决了多个对象之间循环引用,造成不得不手动释放其中一方对另一方的引用时才能释放所有对象占用的内存空间的困境。例如,Controller 在实现计时功能时,有时 Controller 需要持有NSTimer对象,以在必要时对它进行必要的invalidate等操作,若指定 Controller 作为NSTimer的 target,则NSTimer对象的targets数组也是强持有 Controller,三者明显形成了循环引用。
这样可能会带来的隐患是:当开发者想退出 Controller 且没有手动invalidate计时器,且计时器是个没有尽头的循环计时器时,NSTimer对象和 Controller 就会一直得不到释放。
解决这种隐患的其中一种方式就是引入 Weak Proxy。开发者可以构建一个SDWeakProxy对象,并将target属性指向 Controller,在构建NSTimer对象时指定 Target 为刚刚创建的 Weak Proxy,Controller 照旧持有NSTimer对象。由此形成的各方引用关系如下:
实现代码如下:
@interface Controller
@property (strong, nonatomic) NSTimer* timer;
@end
@implementation Controller
-(void)viewDidLoad{
[super viewDidLoad];
SDWeakProxy* weakSelfProxy = [SDWeakProxy proxyWithTarget:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.f
target:weakSelfProxy
selector:@selector(oneSecondTick:)
userInfo:nil
repeats:YES];
}
@end
注意:新版本 Objective-C 暴露的
NSProxy接口中注释掉了-(id)forwardingTargetForSelector:(SEL)aSelector不过实际上依然有执行快速转发过程。后续 Weak Proxy 应当转为通过完整消息转发实现。
三、完整消息转发的应用
当对象/类调用目标实例方法/类方法并进入了快速消息转发阶段,且-(id)forwardingTargetForSelector:(SEL)aSelector返回nil,就会进入完整消息转发流程(normal forwarding)。
完整消息转发流程需要先向对象发送-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector消息,查询消息的方法签名。什么是方法签名呢?可以从NSMethodSignature的构建方法+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types可以简单推测出,方法签名是方法 IMP 的类型编码(type encode)的面向对象表示。为什么需要方法签名呢?这是因为 IMP 的本质是函数指针,在调用 C 语言函数时,CPU 需要为其分配栈空间,栈空间的栈底若干字节是固定用来存储函数的参数列表以及返回值的,方法签名就是为了告诉 CPU 应如何对栈底的这段内存空间进行布局。至于,如何指导 CPU 进行内存布局,则是通过提供参数列表各个参数以及返回值的类型,以及在栈空间中的偏移地址来实现。至此就来到了数据类型编码、方法类型编码的内容,本文不作深究。
拿到方法签名后,runtime 紧接着向对象发送- (void)forwardInvocation:(NSInvocation *)anInvocation消息,此时可以拿到本次调用的NSInvocation对象,它包含了本次调用动作所有关键信息,包括消息接收者、消息名称、消息的参数列表空间及值、消息的返回值空间,开发者就可以对本次调用各种“上下其手”了。
3.1 基本使用范例
接下来介绍开发者可以如何通过完整消息转发机制“胡作非为”。
假设有这样一个程序,发送方向接收方发送-(NSString*)sayHelloTo:(NSString*)someone消息,参数传入发送人称呼,然后接收方会返回给发送方打招呼的话(使用NSLog模拟),发送方还可以根据sayHello:返回值知道对端的名字。但是接收端只是个 Controller 不响应sayHello:消息,所以 Controller 需要通过完整消息转发机制将消息转发给John,John可以相应sayHello:消息。例程如下:
@interface John : NSObject
@end
@implementation John
-(NSString*)sayHelloTo:(NSString*)content{
NSLog(@"Hello! %@!", content);
return @"John";
}
@end
@interface Controller : UIViewController
@end
@implementation Controller
-(void)viewDidLoad{
[super viewDidLoad];
// 模拟 Bob 要求对方向自己打招呼
id who = [self performSelector:@selector(sayHelloTo:) withObject:@"Bob"];
NSLog(@"It's %@ speaking!", who);
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(sayHelloTo:)){
return [NSMethodSignature signatureWithObjCTypes:"@24@0:8@16"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL aSelector = [anInvocation selector];
if(aSelector == @selector(sayHelloTo:)){
John* john = [John new];
[anInvocation setTarget:john];
// 重要:需要手动触发该调用
[anInvocation invoke];
return;
}
[super forwardInvocation:anInvocation];
}
@end
上面例程中,Controller通过实现-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector、- (void)forwardInvocation:(NSInvocation *)anInvocation方法,实现了Controller将自己不能响应的sayHello:消息转发到给John对象响应,只需anInvocation的setTarget:方法指定响应者即可。打印内容如下,Bob 看到以下来自 John 的打招呼内容,场面一度非常和谐。
Hello! Bob! It's John speaking!
然而,接下来我们要让Controller通过完整消息转发机制做出点“不和谐”的事情。只需将- (void)forwardInvocation:(NSInvocation *)anInvocation替换如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL aSelector = [anInvocation selector];
if(aSelector == @selector(sayHelloTo:)){
John* fakeJohn = [John new];
[anInvocation setTarget:fakeJohn];
NSString* name;
[anInvocation getArgument:&name atIndex:2];
// 我要狠狠地操作你一波!!
NSString* fakeContent = [NSString stringWithFormat:@"%@! I’m gonna operate you hard!", name];
[anInvocation setArgument:&fakeContent atIndex:2];
[anInvocation invoke];
NSString* fakeSignature = @"God";
[anInvocation setReturnValue:&fakeSignature];
return;
}
[super forwardInvocation:anInvocation];
}
修改后 Bob 收到的打招呼内容如下:
Hello! Bob! I’m gonna operate you hard!! It's God speaking!
此时:
Bob:你这是 无中生有 暗度陈仓 凭空想象 凭空捏造 无可救药。。。 Controller:没错,我就是为老不尊,胡作非为,为富不仁,为所欲为。。。 John:我什么都不知道,我很冤。。。
回归正题,其实完整消息转发机制就是如此无所不能,拿到方法调用invocation可以给予开发者很大的权限。上面的例程中增加了几个操作,就使输出结果就完全变了味。其中,getArgument获取调用参数,其index的表示参数在本次调用的方法 IMP中的位置索引,由于方法 IMP 第一个参数是 target,第二个参数是 selector,因此第一个参数的索引就是2。同理,setArgument是设置参数。setReturnValue是设置返回值,需要注意,设置返回值必须在[anInvocation invoke]完成之后。
附上简单调试过程:
-
首先在
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector第一行打个断点; -
然后点开 lldb 调试区选择调用栈中的
__forwarding__层; -
__forwarding__函数中包含了完整消息转发流程相关的所有关键调用,如下所示; -
当然
__forwarding__函数中也包含快速转发过程的关键调用,如下所示;
3.2 完整消息转发进阶 JSPatch
JSPatch 热修复第三方库的实现就是借助了完整消息转发机制。JSPatch 工程中包含了一些低版本兼容代码、各平台兼容代码、而且几乎所有 Objective-C 源代码均集中在 JPEngine.m 中,非常难以阅读,因此这里只挑选部分重要代码。本章携带了很多 JSPatch 原理解析相关的内容,这些是 JSPatch 应用完整消息转发的业务上下文,也是有必要了解的。
3.2.1 关键 JS 函数
JSPatch.js 为 JSPatch 核心 JS 脚本,主要用于定义热修复中需要使用到的全局变量以及 JS 函数(其实现很多调用了来自 OC 注入的方法)。其中比较关键的函数如下:
require:记录热修复脚本中需要使用到的Class类型,记录为global[clsName] = {"__clsName": clsName}JS 对象,其中clsName为类名;defineClass:指定热修复的目标Class、新增属性、目标修复实例方法、目标修复类方法。defineClass函数的关键处理逻辑是调用从 JPEngine.m 中注入到 JS 脚本的_OC_defineClass函数,该函数返回热修复的目标Class的新增实例方法、新增修复类函数信息,注意返回的是 JS 函数信息,并保存到_ocCls[className] = { "instMethods": instMethods, "clsMethods": clsMethods,},其中instMethods保存所有热修复实例方法,clsMethods保存所有热修复类方法,两者均是以函数名为关键字的 JS 对象;__c:通过Object.defineProperty(Object.prototype, ...)注入到 JS 根类的所有 JS 对象都可以通过.符号调用的方法。
__c是 JSPatch 实现的精髓,这里单独介绍。其中:
slf[methodName].bind(slf)保证了,支持通过self.__c(methodName)获取this本身的函数或对象;_ocCls[clsName][methodType][methodName].bind(slf)保证了,支持通过self.__c(methodName)获取__ocCls中保存的实例方法和类方法对应的 JS 函数;_methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)保证了,支持通过self.__c(methodName)调用由JSContext 注入的_OC_callI、_OC_callC函数触发其他未备案的 OC 实例方法和类方法;
function(methodName) {
var slf = this
...
// 1. 保证能够获取`this`本身的函数或对象
if (slf[methodName]) {
return slf[methodName].bind(slf);
}
...
// 2. 保证能够获取`__ocCls`保存的函数或对象
var clsName = slf.__clsName
if (clsName && _ocCls[clsName]) {
var methodType = slf.__obj ? 'instMethods': 'clsMethods'
if (_ocCls[clsName][methodType][methodName]) {
slf.__isSuper = 0;
return _ocCls[clsName][methodType][methodName].bind(slf)
}
}
// 3. 其他未备案的函数通过调用 JSContext 注入的`_OC_callI`、`_OC_callC`触发
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)
}
}
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
var selectorName = methodName
if (!isPerformSelector) {
methodName = methodName.replace(/__/g, "-")
selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
var marchArr = selectorName.match(/:/g)
var numOfArgs = marchArr ? marchArr.length : 0
if (args.length > numOfArgs) {
selectorName += ":"
}
}
var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
_OC_callC(clsName, selectorName, args)
return _formatOCToJS(ret)
}
3.2.2 关键 OC 函数
上一小节实际上已经提取到其中一部分关键 OC 方法_OC_defineClass、_OC_callI、_OC_callC,无疑_OC_defineClass是第一入口,因此选择以此为突破口。
defineClass 函数
将不重要的实现代码删除,得到defineClass方法的关键代码如下。其中第一层for循环的两次迭代分别处理热修复的实例方法和类方法;第二层for循环遍历方法列表,对方法逐一执行覆盖处理。也就是说,无论热修复的方法在类中存不存在,其IMP都会指向 JSPatch 所指定的函数。这是探索 JSPatch 实现原理得到的第一个重点。
static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
{
NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];
NSString *className;
NSString *superClassName;
NSString *protocolNames;
...
// 1. for 循环两次迭代分别处理实例方法和类方法
for (int i = 0; i < 2; i ++) {
// 2. 将传入的 JS 方法列表对象转为 NSDictionary
BOOL isInstance = i == 0;
JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
NSDictionary *methodDict = [jsMethods toDictionary];
// 3. for 循环处理方法列表中的所有方法
for (NSString *jsMethodName in methodDict.allKeys) {
// 3.1 解析 JS 脚本传入的方法最终转为 JSValue 类型的 jsMethod
JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
int numberOfArg = [jsMethodArr[0] toInt32];
NSString *selectorName = convertJPSelectorString(jsMethodName);
if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
selectorName = [selectorName stringByAppendingString:@":"];
}
JSValue *jsMethod = jsMethodArr[1];
// 3.2 调用 overrideMethod 执行方法覆盖。
if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
// 3.2.1 若类中已存在该方法则不需要传入方法的 type encode
overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
} else {
// 3.2.2 若类中不存在该方法则传入方法的 type encode,参数和返回值都指定为 id 类型
BOOL overrided = NO;
...
if (!overrided) {
if (![[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
NSMutableString *typeDescStr = [@"@@:" mutableCopy];
for (int i = 0; i < numberOfArg; i ++) {
[typeDescStr appendString:@"@"];
}
overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
}
}
}
}
}
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");
return @{@"cls": className, @"superCls": superClassName};
}
overrideMethod 函数
至此,定位到了另外一个关键方法overrideMethod,方法名透露出该方就是用于方法覆盖。删减后其关键代码如下。其中的关键步骤有两步:
- 添加/替换
forwardInvocation的实现为JPForwarInvocation并备份原forwardInvocation方法为ORIGforwardInvocation; - 添加/替换热修复方法,注意
class_replaceMethod的功能为当传入的 selector 不存在时,则添加该 selector 方法,存在则执行方法替换,将方法的实现直接换位_objc_msgForward,也就是调用热修复方法则直接进入完整消息转发阶段,对象接收到forwardInvocation消息,实际处理者则是上一步替换了原forwardInvocation方法 IMP 的JSForwardInvocation函数。此时**JSForwardInvocation获得了所有热修复方法调用的NSInvocation**,这就意味着,JSPatch 开始可以“为所欲为”了;
也就是说,执行overrideMethod后,APP 调用热修复方法时,会立即进入完整消息转发流程,实际上都是调用 JSPatch 自定义的JPForwarInvocation函数。这是探索 JSPatch 实现原理得到的第二个重点。
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
// 1. 获取热修复方法的选择器
SEL selector = NSSelectorFromString(selectorName);
// 2. 若未指定方法的 type encode,则使用原方法的 type encode
if (!typeDescription) {
Method method = class_getInstanceMethod(cls, selector);
typeDescription = (char *)method_getTypeEncoding(method);
}
IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
IMP msgForwardIMP = _objc_msgForward;
...
// 3. (关键步骤)添加/替换 forwardInvocation 的实现为 JPForwarInvocation, 并备份
// 原 forwardInvocation 方法为 ORIGforwardInvocation
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
if (originalForwardImp) {
class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
}
}
...
// 4. 以"ORIG"为前缀备份原方法 IMP,注意本步将原方法的也添加到类的方法列表中
if (class_respondsToSelector(cls, selector)) {
NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
SEL originalSelector = NSSelectorFromString(originalSelectorName);
if(!class_respondsToSelector(cls, originalSelector)) {
class_addMethod(cls, originalSelector, originalImp, typeDescription);
}
}
// 5. 备案已替换的方法列表
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
_initJPOverideMethods(cls);
_JSOverideMethods[cls][JPSelectorName] = function;
// 6. (关键步骤)替换/添加热修复方法,将方法的实现替换为 _objc_msgForward
class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
}
JPForwardInvocation 函数
至此,定位到了另外一个关键函数JPForwardInvocation。这个函数特别长,但是其中大部分函数是关于根据热修复方法的 type encode 获取方法参数列表及返回值空间,将这些冗长的代码逻辑及其他不重要的逻辑省略,得到的关键代码如下。
其实,JPForwardInvocation处理流程并不复杂。由于热补丁中的 JS 函数触发及返回值都是 JS 对象,因此不能直接用NSInvocation中的 Objective-C 类型的参数,直接调用热补丁的 JS 修复函数,而需要将参数转化为 JS 对象。同理,热补丁的 JS 修复函数返回的 JS 对象不能直接作为NSInvocation的返回值,需要转化为 Objective-C 对象后,才能交到NSInvocation手上,这样 Objective-C 中对热修复函数的调用才能得到合法的返回值类型。因此JPForwardInvocation中的处理逻辑主有四个:
- 在
defineClass时生成的_JSOverideMethods字典中找到热补丁修复函数,若没有则调用"ORIG"为前缀的原有方法的实现; - 参数列表转换为 JS 对象;
- 用上一步转换的参数列表触发热补丁修复 JS 函数;
- 热补丁修复函数返回值转换为 Objective-C 对象,写入
NSInvocation的返回空间;
从上述流程可以发现,Objective-C 和 JS 之间的相互调用,必须以完备的参数类型转换、返回值类型转换为基础。这是探索 JSPatch 实现原理得到的第三个重点。
static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{
BOOL deallocFlag = NO;
id slf = assignSlf;
BOOL isBlock = [[assignSlf class] isSubclassOfClass : NSClassFromString(@"NSBlock")];
// 1. 获取热修复方法的 type encode 以及对应的 JS 函数 jsFunc
NSMethodSignature *methodSignature = [invocation methodSignature];
NSInteger numberOfArguments = [methodSignature numberOfArguments];
NSString *selectorName = isBlock ? @"" : NSStringFromSelector(invocation.selector);
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
// 2. 获取热修复方法的对 JS 函数 jsFunc,注意这里 getJSFunctionInObjectHierachy 就是在上一部分
// _JSOverideMethods 字典中获取数据
JSValue *jsFunc = isBlock ? objc_getAssociatedObject(assignSlf, "_JSValue")[@"cb"] : getJSFunctionInObjectHierachy(slf, JPSelectorName);
// 3. 若找不到热修复方法的 JS 函数,则通过 JPExecuteORIGForwardInvocation 函数去调用原方法
if (!jsFunc) {
JPExecuteORIGForwardInvocation(slf, selector, invocation);
return;
}
// 4. 构建参数列表并添加 self 参数
NSMutableArray *argList = [[NSMutableArray alloc] init];
if (!isBlock) {
if ([slf class] == slf) {
[argList addObject:[JSValue valueWithObject:@{@"__clsName": NSStringFromClass([slf class])} inContext:_context]];
} else if ([selectorName isEqualToString:@"dealloc"]) {
// dealloc 方法需要特殊处理1(貌似是不支持 dealloc 热修复)
[argList addObject:[JPBoxing boxAssignObj:slf]];
deallocFlag = YES;
} else {
[argList addObject:[JPBoxing boxWeakObj:slf]];
}
}
// 5. 省略:根据方法 type encode 获取本次调用的参数列表
...
// 6. 将参数列表封装成 JSValue 列表格式
NSArray *params = _formatOCToJSList(argList);
// 7. 省略:根据方法 type encode 获取本次调用的返回值空间
char returnType[255];
...
switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
// 8. 调用 [jsFunc callWithArguments:params] 触发热修复函数调用,返回值转化为 Objective-C
// 类型保存到 NSInvocation 的返回空间。这段宏是无参返回通用处理,后面的 XXX_RET_CASE 便于实现 switch 的 case
// 触发事件修复函数调用
#define JP_FWD_RET_CALL_JS \
JSValue *jsval; \
[_JSMethodForwardCallLock lock]; \
jsval = [jsFunc callWithArguments:params]; \
[_JSMethodForwardCallLock unlock]; \
while (![jsval isNull] && ![jsval isUndefined] && [jsval hasProperty:@"__isPerformInOC"]) { \
NSArray *args = nil; \
JSValue *cb = jsval[@"cb"]; \
if ([jsval hasProperty:@"sel"]) { \
id callRet = callSelector(![jsval[@"clsName"] isUndefined] ? [jsval[@"clsName"] toString] : nil, [jsval[@"sel"] toString], jsval[@"args"], ![jsval[@"obj"] isUndefined] ? jsval[@"obj"] : nil, NO); \
args = @[[_context[@"_formatOCToJS"] callWithArguments:callRet ? @[callRet] : _formatOCToJSList(@[_nilObj])]]; \
} \
[_JSMethodForwardCallLock lock]; \
jsval = [cb callWithArguments:args]; \
[_JSMethodForwardCallLock unlock]; \
}
// 这段宏是对返回可直接转化为 Objective-C 对象的 JSValue 的处理
#define JP_FWD_RET_CASE_RET(_typeChar, _type, _retCode) \
case _typeChar : { \
JP_FWD_RET_CALL_JS \
_retCode \
[invocation setReturnValue:&ret];\
break; \
}
// 这段宏是返回需要通过 toObject 转化为 Objective-C 对象的 JSValue 的处理
#define JP_FWD_RET_CASE(_typeChar, _type, _typeSelector) \
JP_FWD_RET_CASE_RET(_typeChar, _type, _type ret = [[jsval toObject] _typeSelector];) \
...
}
// 8. 返回值中的指针所指向的内存空间需要释放
if (_pointersToRelease) {
for (NSValue *val in _pointersToRelease) {
void *pointer = NULL;
[val getValue:&pointer];
CFRelease(pointer);
}
_pointersToRelease = nil;
}
// 9. dealloc 方法需要特殊处理2(貌似是不支持 dealloc 热修复)
if (deallocFlag) {
slf = nil;
Class instClass = object_getClass(assignSlf);
Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc"));
void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));
}
}
callSelector 函数
剩余还有一个未解决的问题:上文提到,热补丁内调用 Objecitvie-C 方法时,会触发JPEngine注入的_OC_callI或OC_CALLC函数,那么这两个函数是怎么实现的呢?它们实际上都只是简单调用了JPEngine的callSelector函数。
从JSForwardInvocation的实现可以预见,callSelector又是一个相当长的函数。没错,它就是很长。不过处理流程也已经不难推测。没错,它就是JPForwardInvocation那种套路。代码就不贴出来了,其处理流程如下:
- 将来自 JS 热补丁脚本的
_OC_callI和_OC_callC的消息接收对象转换为 Objective-C 对象或类; - 将调用传入的方法名参数转换为对应的 Objective-C 方法 selector;
- 根据方法名 selector 及消息接收对象查找 Objective-C 方法的方法签名,并用方法签名调用
invocationWithMethodSignature构建本次 Objective-C 调用的NSInvocation对象,并置 target 及 selector; - 将传入的参数列表转化为 Objective-C 类型(这里是很长一段根据方法 type encode 进行参数列表类型转换过程);
- 调用上面构建的
NSInvocation对象的invoke方法触发本次调用; - 调用上面构建的
NSInvocation对象的getReturnValue:方法取出 Objective-C 类型的返回值(这里又是很长一段根据方法 type encode 进行返回值类型转换过程),将其转化为 JS 对象,然后返回该对象;
3.2.3 JSPatch 处理流程总结
至此,一次完整的热修复函数的调用流程就基本走通了。当然 JSPatch 中还有很多细节处理,譬如对低版本的兼容、超类的兼容、GCD 函数兼容、Block 兼容等处理,这些都不与完整消息转发流程相关,不属于本文范畴因此本文不再深入探究。
总结 JSPatch 各流程调用如下。其中
- 左侧虚线框为 JavaScript 代码,右侧虚线框为 Objective-C 代码;
- 绿色标记为 JS 热修复脚本代码;
- 蓝色标记为 JSPatch 实现代码;
- 红色标记为 Objective-C 内部代码;
- JSHotfixFuntion 为热修复方法的 JS 函数;
- ObjectiveCHotfixedMethod 为热修复方法的;
四、总结
- Objective-C runtime 中消息动态响应的方式有两种:动态方法解析、消息转发。其中,消息转发的方式又分两种:快速消息转发和完整消息转发;
- 动态方法解析通常与具体业务强耦合,比较难运用于通用框架中;
- 快速消息转发是指定备用消息接收者的消息转发快速处理模式;
- 完整消息转发可以获得方法调用的
NSInvocation对象,获取该对象可以获取到很高的权限,可以操作方法的参数列表、返回值空间、控制方法是否实际调用、附加处理等等; - 完整消息转发具有最高的通用性,具有最强的扩展性能,因此可广泛运用于各种框架,例如:JSPatch、Aspect 等等,但是运用的广泛性也可能会带来各框架之间的共存问题,开发、使用时必须考虑这个因素;
- 最后附上 JSPatch 实现的核心流程:
- JSPatch 将热修复脚本中的热修复函数对应的方法的实现,替换为
_objc_msgForward,调用热修复方法会立刻进入完整消息转发流程; - JSPatch 将原
forwardInvocation的 IMP 替换为自定义的JPForwarInvocation函数,因此 Objective-C 内部调用热修复方法会立刻进入完整消息转发流程触发都统一触发JPForwarInvocation函数; JPForwarInvocation函数会根据其NSInvocation参数,获取并调用热修复补丁中定义的热修复函数,该调用过程是以完备的 OC-JS 参数类型转换、返回值类型转换为基础;- JSPatch 脚本调用 Objective-C 方法时通过
__c函数,调用由JPEngine注入到 JS 的_OC_callI或_OC_callC函数,该函数能获取传递进来的 target 对象、selector 方法选择器、参数列表、返回值,在 OC-JS 类型转换的基础上,通过构建NSInvocation调用目标 Objective-C 方法。
- JSPatch 将热修复脚本中的热修复函数对应的方法的实现,替换为