JSPatch是目前iOS平台上比较主流的热更新方案之一。它主要使用了OC的runtime技术并通过JS语言来调用OC代码来实现代码的更新与修复。本篇主要就该方案中两种语言间的通信方案做下探索。
通信机制
两种语言间通信的机制是苹果官方提供的JavaScriptCore.framework框架,它可以让我们脱离Webview直接运行JS代码。其具体使用可以参考这篇文章。而JSPatch正是通过这个框架在两种语言间架起了一座相互来往的桥梁。整体架构可以用下图来表示:
代码流程
[JPEngine startEngine];
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
这是一段标准的JSPatch调用代码,热更新的代码都位于demo.js文件中。首先调用startEngine将JSPatch的整个运行环境都调用起来,然后就可以使用evaluateScript将JS文件中的修复代码运行起来并通过runtime体系注入到OC代码实现中。下面让我们具体的跟踪下evaluateScript运行轨迹。
+ (JSValue *)_evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURL
{
...
if (!_regex) {
_regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil];
}
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 {
if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
return [_context evaluateScript:formatedScript withSourceURL:resourceURL];
} else {
return [_context evaluateScript:formatedScript];
}
}
...
}
这里使用了正则表达式,具体作用是将demo.js中的js函数调用都替换为__c函数,这样做的具体原因是将简化JS侧注入的OC函数数量,大家也可移步到JSPatch作者的具体说明文章,里面有详尽的解说。下面来看__c的具体实现。
var _customMethods = {
__c: function(methodName) {
var slf = this
if (slf instanceof Boolean) {
return function() {
return false
}
}
if (slf[methodName]) {
return slf[methodName].bind(slf);
}
if (!slf.__obj && !slf.__clsName) {
throw new Error(slf + '.' + methodName + ' is undefined')
}
if (slf.__isSuper && slf.__clsName) {
slf.__clsName = _OC_superClsName(slf.__obj.__realClsName ? slf.__obj.__realClsName: slf.__clsName);
}
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)
}
}
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)
}
}
}
for (var method in _customMethods) {
if (_customMethods.hasOwnProperty(method)) {
Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})
}
}
在这个接口中解析出要调用的OC函数的具体信息,如类名、方法名、父类名、调用参数及调用的对象信息等。然后再_methodFunc中根据JSPatch函数全名的规则将JS函数名转换成OC函数名。再调用_OC_callI或者_OC_callC来实际执行函数内部的具体代码,这属于runtime中的内容了,将于下一章节介绍。
最后,函数执行的结果经包装后返回到JS侧来,转换成JS可用的数据对象,执行转换的主体代码位于formatOCToJS接口中。