李峰峰博客

热修复之 JSPatch 实现原理

2022-09-10

一、基本使用

JSPatch 基于 JavaScriptCoreRuntime 实现,可以使用 JavaScript 代码对原方法进行重写(覆盖)、新增方法等,实现对线上问题代码的修复,例如:
假设 JPViewController 中按钮点击事件如下:

1
2
3
4
5
6
@implementation JPViewController

- (void)handleBtn:(id)sender {
}

@end

现在要使用 JSPatch 覆盖该方法,使点击按钮跳转到 JPTableViewController,其对应 JSPatch 热修复代码实现如下:

1
2
3
4
5
6
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES)
}
})

将上述热修复代码放到 demo.js 文件中,使用 JPEngine 执行 JS 代码即可实现对原方法的覆盖:

1
2
3
4
[JPEngine startEngine];
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];

在 JSPatch 的热修复代码中,调用 OC 方法的方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 调用类方法
var redColor = UIColor.redColor();

// 调用实例方法
var view = UIView.alloc().init();
view.setNeedsLayout();

// 多参数方法名使用 _ 分隔
self.navigationController().pushViewController_animated(tableViewCtrl, YES)

// 在方法名前加 ORIG 即可调用未覆盖前的 OC 原方法:
defineClass("JPTableViewController", {
viewDidLoad: function() {
self.ORIGviewDidLoad();
},
})

二、实现原理

1、OC 方法的替换

前面热修复代码实例中,可以看到 defineClass 的调用:

1
2
3
4
5
defineClass('JPViewController', {
handleBtn: function(sender) {
...
}
})

在执行 defineClass 时,实际上会走进 JSPatch.js 中定义的 defineClass 中,defineClass 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
global.defineClass = function(declaration, properties, instMethods, clsMethods) {
var newInstMethods = {}, newClsMethods = {}
// 如果 properties 不是数组,则调整参数顺序
if (!(properties instanceof Array)) {
clsMethods = instMethods
instMethods = properties
properties = null
}
// 如果有 properties,为每个属性生成 getter 和 setter 方法,并添加到 instMethods 中
if (properties) {
// 遍历 properties 数组中的每一个属性名
properties.forEach(function(name){
// 如果 instMethods 中没有该属性名对应的方法,则为其生成 getter 方法
if (!instMethods[name]) {
instMethods[name] = _propertiesGetFun(name);
}
var nameOfSet = "set"+ name.substr(0,1).toUpperCase() + name.substr(1);
// 如果 instMethods 中没有该 setter 方法,则为其生成 setter 方法
if (!instMethods[nameOfSet]) {
instMethods[nameOfSet] = _propertiesSetFun(name);
}
});
}
// 提取类名,并去除多余的空格
var realClsName = declaration.split(':')[0].trim()

/**
* instMethods/clsMethods -> newInstMethods/newClsMethods
* newInstMethods/newClsMethods 都是数组,key 为原始函数的名称;value 为数组,[0] 为参数数量,[1] 函数实现
*/
_formatDefineMethods(instMethods, newInstMethods, realClsName)
_formatDefineMethods(clsMethods, newClsMethods, realClsName)
// _OC_defineClass:新增方法,或者对原方法进行覆盖
var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
var className = ret['cls']
var superCls = ret['superCls']
_ocCls[className] = {
instMethods: {},
clsMethods: {},
}
if (superCls.length && _ocCls[superCls]) {
for (var funcName in _ocCls[superCls]['instMethods']) {
_ocCls[className]['instMethods'][funcName] = _ocCls[superCls]['instMethods'][funcName]
}
for (var funcName in _ocCls[superCls]['clsMethods']) {
_ocCls[className]['clsMethods'][funcName] = _ocCls[superCls]['clsMethods'][funcName]
}
}
_setupJSMethod(className, instMethods, 1, realClsName)
_setupJSMethod(className, clsMethods, 0, realClsName)
return require(className)
}

由上可知,defineClass 定义如下:

1
defineClass(classDeclaration, [properties,] instanceMethods, classMethods)
  • classDeclaration:字符串,类名/父类名和 Protocol
  • properties:新增 property,字符串数组,可省略
  • instanceMethods:要添加或覆盖的实例方法
  • classMethods:要添加或覆盖的类方法

例如:

1
2
3
4
5
6
7
defineClass("xxxClassName", ['property1', 'property2'], {
instanceMethod1: function() {},
instanceMethod2: function() {}
}, {
classMethod1: function() {},
classMethod2: function() {}
})

defineClass 中主要逻辑如下:

  • 如果有 properties,为每个属性生成 gettersetter 方法,并添加到 instMethods 中。
  • instMethodsclassMethods 进行包装处理,
    • 包装前:key 为原始函数的名称;value 为函数实现。
    • 包装后:key 为原始函数的名称;value 为数组,[0] 为参数数量,[1] 函数实现。
      • 如果包装的原因是,从 JS 将数据传给 OC,OC 收到的是 JSValue 类型,OC 无法从中读取参数数量,而 OC 后续的逻辑需要使用参数数量。
  • 执行 _OC_defineClass,进行方法的新增和替换。

_OC_defineClass 是 OC 中通过 JSContext 注入的函数:

1
2
3
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};

其会走进 OC 实现的 defineClass 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
替换或新增方法。并返回字典:@{@"cls": className, @"superCls": superClassName}
*/
static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
{

// 使用 NSScanner 扫描 classDeclaration 字符串,提取类名、父类名和协议名
// classDeclaration 示例:JPTableViewController : UITableViewController <UIAlertViewDelegate>
NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];

NSString *className;
NSString *superClassName;
NSString *protocolNames;

// 扫描到 ":" 之前的内容作为类名
[scanner scanUpToString:@":" intoString:&className];
if (!scanner.isAtEnd) {
// 跳过 ":" 字符
scanner.scanLocation = scanner.scanLocation + 1;
// 扫描到 "<" 之前的内容作为父类名
[scanner scanUpToString:@"<" intoString:&superClassName];
if (!scanner.isAtEnd) {
// 跳过 "<" 字符
scanner.scanLocation = scanner.scanLocation + 1;
// 扫描到 ">" 之前的内容作为协议名
[scanner scanUpToString:@">" intoString:&protocolNames];
}
}

// 如果 superClassName 没取到,就赋值为 NSObject
if (!superClassName) superClassName = @"NSObject";
className = trim(className);
superClassName = trim(superClassName);

// 协议可能是多个,拆分成数组
NSArray *protocols = [protocolNames length] ? [protocolNames componentsSeparatedByString:@","] : nil;

Class cls = NSClassFromString(className);
if (!cls) {
Class superCls = NSClassFromString(superClassName);
if (!superCls) {
_exceptionBlock([NSString stringWithFormat:@"can't find the super class %@", superClassName]);
return @{@"cls": className};
}
// 类不存在,但是父类存在。则动态创建一个新的类
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
// 将新创建的类注册到运行时系统中
objc_registerClassPair(cls);
}

if (protocols.count > 0) {
for (NSString* protocolName in protocols) {
// 将协议添加到的类中,使类遵循该协议
Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
class_addProtocol (cls, protocol);
}
}

for (int i = 0; i < 2; i ++) {
// 分别针对 instanceMethods、classMethods 处理
BOOL isInstance = i == 0;
JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
/**
objc_getMetaClass 获取元类,接收参数是字符串。等价于 object_getClass([xxxObject class])
元类:类的所属类,普通类存储了实例方法,而元类存储了类方。
*/
Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
NSDictionary *methodDict = [jsMethods toDictionary];
for (NSString *jsMethodName in methodDict.allKeys) {
/**
在 JS 的 _formatDefineMethods 中,methodName 对应的 value 被设置成了数组,[0] 为参数数量,[1] 为包装后的函数实现
*/
JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
// 获取方法的参数个数
int numberOfArg = [jsMethodArr[0] toInt32];
/**
将 JS 热修复代码的方法名转成正常方法名。例如:
tableView_cellForRowAtIndexPath -> tableView:cellForRowAtIndexPath:
*/
NSString *selectorName = convertJPSelectorString(jsMethodName);

if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
selectorName = [selectorName stringByAppendingString:@":"];
}

JSValue *jsMethod = jsMethodArr[1];
if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
// 如果 currCls 中已实现对应方法,说明是想覆盖原方法,HOOK 替换原方法实现
overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
} else {
// currCls 中如果没有实现过,则无法直接获取方法的类型编码(overrideMethod 的最后一个参数),需要获取正确的类型编码传进去
// 先看协议中使用有这个方法,如果有,则从协议中获取方法类型编码
BOOL overrided = NO;
for (NSString *protocolName in protocols) {
char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);
if (types) {
overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);
free(types);
overrided = YES;
break;
}
}

// 如果协议中没有找到,参数、返回值全按照 id 类型生成 类型编码
if (!overrided) {
if (![[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
// 第一个 @,代表返回值是 id 类型、第二个 @,代表 id 类型参数 self、第三个 : 代表 SEL 类型参数 _cmd
NSMutableString *typeDescStr = [@"@@:" mutableCopy];
// 其余参数全按照 id 类型处理
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};
}

简而言之,OC 中实现的 defineClass 主要逻辑如下:

  • 解析出类、方法名、类型编码等信息
  • 利用 Runtime 进行新增或替换方法

其中,方法的动态替换是通过其 overrideMethod 方法实现的,其具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
SEL selector = NSSelectorFromString(selectorName);

if (!typeDescription) {
Method method = class_getInstanceMethod(cls, selector);
typeDescription = (char *)method_getTypeEncoding(method);
}

// 获取 OC 原方法的实现
IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;

IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
if (typeDescription[0] == '{') {
//In some cases that returns struct, we should use the '_stret' API:
//http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
//NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
}
#endif

/**
把 -forwardInvocation: 方法的实现给替换掉了,如果程序里真有用到这个方法对消息进行转发,原来的逻辑怎么办?
首先在替换 -forwardInvocation: 方法前会新建一个方法 -ORIGforwardInvocation:,保存原来的实现 IMP,在新的 -forwardInvocation: 实现里做了个判断,如果转发的方法是我们想改写的,就走我们的逻辑,若不是,就调 -ORIGforwardInvocation: 走原来的流程。
*/
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
// 替换类 cls 中的 forwardInvocation: 方法的实现为 JPForwardInvocation
IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
if (originalForwardImp) {
class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
}
}

// 新增一个 ORIGXXX 方法,指向原方法的实现
[cls jp_fixMethodSignature];
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);
}
}

NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];

_initJPOverideMethods(cls);
_JSOverideMethods[cls][JPSelectorName] = function;

/**
通过 class_replaceMethod() 接口将原方法直接指向 _objc_msgForward,使调用方法的时候,直接走消息转发流程,这样调用这个方法时就会走到 -forwardInvocation:
*/
class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
}

overrideMethod 逻辑总结如下:

  • 先替换 forwardInvocation: 实现,替换成 JPForwardInvocation

    • 同时将原保存到 forwardInvocation: 实现保存到 ORIGforwardInvocation:
    • 执行 JPForwardInvocation 时,如果没有找到对应 JS 方法,则走原始转发流程,执行 ORIGforwardInvocation:
  • 添加 -ORIGXXXXX-_JPXXXXX 两个方法,前者指向原来的 IMP 实现,后者是 JS 热修复代码中的新实现。

    • 如果热修复代码需要调用原方法,可以在方法名前拼上 ORIG 去调用,因为这一步保存了方法原实现 -ORIGXXXXX
  • 将原方法实现指向 _objc_msgForward,使方法直接走消息转发流程。

  • JPForwardInvocation 里,获取 Invocation 参数,传给 -_JPXXXXX 方法并执行,实现了 JS 方法替换 OC 方法。

该流程可用下图表示:

2、JavaScriptCore 执行 OC 方法

还以前面的热修复代码为例:

1
2
3
4
5
6
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES)
}
})

上述 JS 实现的热修复代码中,有类似 allocinit 等 OC 方法的调用,而在 JS 中,是不存在这些方法的,如果想要 JS 代码执行不报错,就需要保证这些方法在 JS 中正常被调用。

实际上,在 JPEngine 中执行 JS 代码时,逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
+ (JSValue *)_evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURL
{
if (!script || ![JSContext class]) {
_exceptionBlock(@"script is nil");
return nil;
}
[self startEngine];

if (!_regex) {
_regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil];
}

/**
1、正则匹配找到热修复 JS 代码中的所有方法调用,改成调用 __c()
UIView.alloc().init() -> UIView.__c('alloc')().__c('init')()
object.methodName(param1, param2); -> object.__c("methodName")(param1, param2);

2、将修改后的 JS 放到 try catch 中
*/
NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];


@try {
// 3、执行热修复 JS
if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
return [_context evaluateScript:formatedScript withSourceURL:resourceURL];
} else {
return [_context evaluateScript:formatedScript];
}
}
@catch (NSException *exception) {
_exceptionBlock([NSString stringWithFormat:@"%@", exception]);
}
return nil;
}

在执行 JS 代码之前,JSPatch 使用正则找到了所有方法的调用,将方法的调用改成 __c 函数的调用,并放到 try catch 中,例如前面热修复代码,经过这一步之后,会变成:

1
2
3
4
5
6
7
8
;(function(){try{
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)
}
})
}catch(e){_OC_catch(e.message, e.stack)}})();

JSPatch 通过正则把所有方法调用都改成调用 __c() 函数,再执行这个 JS 脚本,达到类似 OC/Lua/Ruby 等的消息转发的目的。

__c 函数实现方式:在 JSPatch.js 中,给 JS 对象基类 Object 加上 __c 成员,这样所有对象都可以调用到 __c

1
2
3
4
5
6
7
8
Object.defineProperty(Object.prototype, '__c', {value: function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
}
}})

在上述的 _methodFunc() 中,把相关信息传给 OC,OC 再利用 Runtime 去调用相应方法,返回结果值。这样就达到了 JS 调用任意 OC 方法的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
}

_methodFunc() 中,通过 _OC_callI/_OC_callC 调用具体 OC 方法,_OC_callI/_OC_callC 会执行 OC 实现的 callSelector,在 callSelector 中利用 Runtime 去调用相应方法,返回结果值给到 JS。这样就达到了 JS 调用任意 OC 方法的目的:

1
2
3
4
5
6
context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
return callSelector(nil, selectorName, arguments, obj, isSuper);
};
context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
return callSelector(className, selectorName, arguments, nil, NO);
};

该过程总结如下:

  • 通过正则替换方法的调用:在执行 JS 代码之前,JSPatch 使用正则匹配找到所有方法的调用,并将其替换为调用 __c 函数。
    • object.methodName(param1, param2); -> object.__c("methodName")(param1, param2);
  • __c 函数的实现: 在 JSPatch.js 中,给 JS 对象基类 Object 添加 __c 成员函数,这样所有对象都可以调用到 __c,在 __c 中会调用 _methodFunc 函数。
  • _methodFunc 函数:负责处理具体的方法调用信息,通过 _OC_callI_OC_callC 传递方法调用信息。
  • _OC_callI/_OC_callCJSEngine 注入的方法,实际调用的是 OC 实现的 callSelector
  • callSelector:利用 Runtime 执行方法,并将返回值返回给 JS。

3、JS 与 OC 数据传递

还以前面示例为例:

1
2
3
4
5
6
7
8
;(function(){try{
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)
}
})
}catch(e){_OC_catch(e.message, e.stack)}})();

上述实现有两个方向的数据传递:

  • OC -> JS

    • JS 调用 OC ,获取 OC 返回值:
      • var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
      • tableViewCtrl 为 OC 返回给 JS 的数据。
  • JS -> OC

    • JS 获取到 OC 返回值后,再调用 OC 方法转给 OC:
      • self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)
      • tableViewCtrlYES 为 JS 传给 OC 的数据。

(1)OC -> JS 数据传递

前面已经提到,JS 的 __c 函数实际上是调用 _methodFunc()_methodFunc() 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
}

可以看到,_methodFunc() 中主要逻辑是获取获取具体方法调用信息以及具体参数,再去调用 _OC_callI_OC_callC_OC_callI_OC_callC 最终都会走进 OC 的 callSelector 中:

1
2
3
4
5
6
7
context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
return callSelector(nil, selectorName, arguments, obj, isSuper);
};

context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
return callSelector(className, selectorName, arguments, nil, NO);
};

callSelector 逻辑伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper)
{

invocation = ...
[invocation setTarget:xxxx];

id argumentsObj = formatJSToOC(arguments);

NSMethodSignature *methodSignature = ......;
NSUInteger numberOfArguments = methodSignature.numberOfArguments;


for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
id valObj = argumentsObj[i-2];
// 根据 argumentType 将 valObj 转成对应的类型
value = covert(valObj);
[invocation setArgument:&value atIndex:i];
}

void *result;
[invocation getReturnValue:&result];

return formatOCToJS(returnValue);
}

可以看到,在 callSelector 中,执行完 OC 方法,会将 OC 方法返回值,调用 OC 中实现的 formatOCToJS 进行处理后再返回给 JS。

formatOCToJS 的作用就是将传入的 OC 对象包装成一个 NSDictionary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static id formatOCToJS(id obj)
{
if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSDictionary class]] || [obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDate class]]) {
return _autoConvert ? obj: _wrapObj([JPBoxing boxObj:obj]);
}
if ([obj isKindOfClass:[NSNumber class]]) {
return _convertOCNumberToString ? [(NSNumber*)obj stringValue] : obj;
}
if ([obj isKindOfClass:NSClassFromString(@"NSBlock")] || [obj isKindOfClass:[JSValue class]]) {
return obj;
}

/**
将 obj 包装成字典
字典主要有两个 key:
__obj:当前对象
__clsName:对象对应的 class 字符串
*/
return _wrapObj(obj);
}

其中 _wrapObj 实现如下:

1
2
3
4
5
6
7
static NSDictionary *_wrapObj(id obj)
{
if (!obj || obj == _nilObj) {
return @{@"__isNil": @(YES)};
}
return @{@"__obj": obj, @"__clsName": NSStringFromClass([obj isKindOfClass:[JPBoxing class]] ? [[((JPBoxing *)obj) unbox] class]: [obj class])};
}

字典中至少保存了两个信息:

  • __obj
    • 对象实例
  • __clsName
    • ClassName 字符串

这样 JS 中就可以通过判断对象是否有 __obj 属性得知这个对象是否表示 OC 对象指针。

该过程总结如下:

  • 如果是 NSStringNSDictionaryNSArray 类型,则先将 obj 包装成 JPBoxing,再调用 _wrapObjJPBoxing 包装成字典。

    • NSMutableArray/NSMutableDictionary/NSMutableString 从 OC 传到 JS 时,JavaScriptCore 把它们转成了 JS 的 Array/Object/String,再回传给 OC 时,OC 收到的不再是 NSMutableXXX,脱离了跟原对象的联系,导致在 JS 中无法调用三者可变对象中的方法去修改对象,解决办法是:
      • 如果对象是 NSMutableXXX,就先将对象保存到 JPBoxing,JS 拿到的是 JPBoxing,从 JS 再传回给 OC 时就可以通过 JPBoxing 对象成员取到原始对象。
      • 为了规则简单,JSPatch 让 NSArray/NSDictionary/NSString 也同样以封装的方式传递,避免在调用 OC 方法返回对象时还需要关心它返回的是可变还是不可变对象。
  • 如果是其他普通 OC 对象,直接调用 _wrapObj 包装成字典。

  • JS 中通过判断对象是否有 __obj 属性得知这个对象是否表示 OC 对象指针。

    • OC 的字典传到 JS,就变成 JS 的对象 Object 类型。

(2)JS -> OC 数据传递

1
2
var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)

上述 tableViewCtrl 是调用 JS 调用 OC 获取的返回值,即调用 _methodFunc 的返回值:

1
2
3
4
5
6
7
8
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {

// 省略 ......

var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
_OC_callC(clsName, selectorName, args)
return _formatOCToJS(ret)
}

_methodFunc 中,在获取到 OC 返回值的时候,会调用 JS 函数 _formatOCToJS 对返回值进行处理,JS 中的 _formatOCToJS 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var _formatOCToJS = function(obj) {
if (obj === undefined || obj === null) return false

if (typeof obj == "object") {
/**
* OC 返回给 JS 的字典(OC 对象保存到字典的 __obj 中),到 JS 里就会自动变成 JS 的 Object
* 如果有 __obj 标志,就代表是 OC 对象,取出 OC 对象返回
*/
if (obj.__obj) return obj
if (obj.__isNil) return false
}

// 如果 obj 是一个数组,则递归地调用 _formatOCToJS 处理数组中的每一个元素,并返回新的数组。
if (obj instanceof Array) {
var ret = []
obj.forEach(function(o) {
ret.push(_formatOCToJS(o))
})
return ret
}

// 省略 ......
}

可以看到,参数 tableViewCtrl 在传给 OC 之前,就已经从 OC 传递的字典中解析出原始对象,再将解析出原始对象传给 OC。
即:JS 传给 OC 的对象,是 OC 原始对象。

JS 与 OC 数据流转可用下图表示: