一、热修原理
JSPatch
是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript
调用任何 Objective-C
原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复BUG
。
JSPatch
能做到通过 JS
调用和改写 OC
方法最根本的原因是 Objective-C
是动态语言,OC
上所有方法的调用/类的生成都通过 Objective-C Runtime
在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法。
所以 JSPatch
的基本原理就是:JS
传递字符串给 OC
,OC
通过 Runtime
接口调用和替换 OC
方法,这是最基础的原理。
1.1 热修的方法替换过程
第1步:[JPEngine startEngine]
的调用
通过苹果官方提供的JavaScriptCore
框架,使用JSContext
对象实现 JS
和 Native
的交互。这个框架很简单但非常强大,可以网上搜索掌握它的用法,这里不再细说,但是掌握这个框架的基本使用才能继续分析JSPatch
框架。
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
如上OC
代码是指向 JS
注入了全局的 _OC_defineClass
方法,其具体实现对应着 Native
的 block
,这就是JavaScriptCore
的强大之处。这样一来,我们可以在 JS
代码中直接调用 _OC_defineClass
这个方法,即可调用到 Native
中了。
global.defineClass = function(declaration, instMethods, clsMethods) {
var newInstMethods = {}, newClsMethods = {}
_formatDefineMethods(instMethods, newInstMethods)
_formatDefineMethods(clsMethods, newClsMethods)
var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
return require(ret["cls"])
}
第2步:global.defineClass
方法解析
defineClass
方法接收的参数是:
1:类名字符串。
2:类的实例方法和类方法列表(都是js对象的形式)。
defineClass
方法会首先分别对这两个对象调用 _formatDefineMethods
方法。
var _formatDefineMethods = function(methods, newMethods) {
for (var methodName in methods) {
(function(){
var originMethod = methods[methodName]
newMethods[methodName] = [originMethod.length, function() {
var args = _formatOCToJS(Array.prototype.slice.call(arguments))
var lastSelf = global.self
var ret;
try {
global.self = args[0]
args.splice(0,1)
ret = originMethod.apply(originMethod, args)
global.self = lastSelf
} catch(e) {
_OC_catch(e.message, e.stack)
}
return ret
}]
})()
}
}
_formatDefineMethods
作用,简单的说就是它把defineClass
中传递过来的JS
对象进行了修改:
原来的形式是:
{
methodName:function(){...}
}
修改之后是:
{
methodName: [argCount, function(){...新的实现}]
}
传递参数个数的目的是:runtime
在修复类的时候,无法直接解析原始的JS
实现函数,那么就不知道参数的个数,特别是在创建新的方法的时候,需要根据参数个数生成方法签名,所以只能在JS
端拿到JS
函数的参数个数,传递到OC
端。
第3步:_OC_defineClass
方法实现
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
在JPEngine
中,定了一个名为defineClass
的函数,这个函数对类进行真正的重写操作。我们知道runtime
重写一个方法,需要几个最基本的参数:类名
、selector
、方法实现(IMP)
、方法签名
,defineClass
做的就是把这些信息提取出来:
1、首先是对类名进行解析,把协议名、类名、父类名都解析出来。如果类不存在,那么创建并注册该类。
2、分别对实例方法和类方法进行处理,JS
函数_formatDefineMethods
处理返回的是JS
对象,传递到OC
这边会被JavaScriptCore
转换为JSValue
对象,可以对该对象直接调用toDictionary
把JS
对象转换成OC
字典。这样我们就可以取到方法名
、参数个数
、具体实现
。3、遍历字典的
key
,即方法名,根据方法名取出的值还是JSValue
对象,不过它代表的是数组,第一个值是参数的个数,第二个值是函数的实现。
4、方法名的处理:这块涉及到方法名的格式要求和处理,例如:在JS
中的tableView_numberOfRowsInSection
,下划线需要被替换成':'
。
5、最后拿着处理好的方法名和具体实现等调用overrideMethod
函数。
第4步:overrideMethod
函数实现
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
1、把
selector
对应的具体实现使用class_replaceMethod
替换成_objc_msgForward
,我们知道这个对应着消息转发机制。
2、把forwardInvocation
的具体实现替换成JPForwardInvocation
实现。
3、向class
添加名为ORIGforwardInvocation
的方法,实现是原始的forwardInvocation
的IMP
。
4、向class
添加名为ORIG+selector
,对应原始selector
的IMP
。JS
可以通过这个方法调用到原来的实现。
5、向class
添加名为_JP + selector
,对应JS
重写的函数实现。
1.2 热修的方法执行流程
第1步:JPForwardInvocation
函数
经过上一步的处理,调用被热修的selector
时,其实调用的是objc_msgForward
,即走到了消息转发的环节。而在此的上一步中把 forwardInvocation
方法的实现替换成了 JPForwardInvocation
方法。
1、把
selector
前面加上_JP
前缀,构成的新的selector
,如果本地存储的字典中没有存储对应的JS
方法实现,说明这个不是我们重写的方法,那么走原来的消息转发;否则调用JS
方法实现。2、把
self
和其他的参数都转换称JS
对象,JS
端重写的函数传递过来是JSValue
类型,这里对应着JS
函数,可以对其调用callWithArgument
方法,参数转换成JS
对象,执行函数。
实际上这个方法的细节是非常多的,根据方法签名,取出每个参数的类型,进行参数的封装、对于结构体的处理等等。
讲到这里,就完成了函数调用环节。
第2步:callSelector
函数
在最开始startEngine
的时候,会把这些热修的JS
代码统一进行正则表达式的匹配替换,也就是把所有的函数都替换成对名为 __c()
函数的调用,例如:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
__c()
具体的实现就是调用了 _methodFunc()
函数。
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_call
函数,而在startEngine
的一开始,我们就为JSContext
注入了 _OC_callI
、_OC_callC
函数,具体实现是一个调用了OC
的 callSelector
的block
:
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、把
JS
对象和JS
参数转换为OC
对象;
2、判断是否调用的是父类的方法,如果是就走父类的方法实现;
3、把参数等信息封装成NSInvocation
对象并执行,然后返回结果;
具体的实现细节包括对methodSignature
的字符处理,根据这些字符对JS
对象进行处理和转换,还有对结构体对支持等。
二、热修语法
1.JSPatch
实现原理教程:https://github.com/bang590/JSPatch/wiki/JSPatch-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3
2.JSPatch
官方的热修语法文档:https://github.com/bang590/JSPatch/wiki/JSPatch-%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95
3.在线OC
代码翻译成热修JS
的工具:http://bang590.github.io/JSPatchConvertor/
三、其他问题
3.1.为什么要重启,热修文件才会生效的原因。
JS
文件下载的位置是:applicationDidBecomeActive:
方法,使用JS
的代码放在 didFinishLaunchingWithOptions:
这个方法。因为这个方法在程序启动和后台回到前台时都会调用,并且端上可以设置一个间隔时间的策略,也就是说每次来到这个方法时,先要检测是距离上次发请求的时间间隔是否超过1小时,超过则发请求,否则跳过。因为如果这个app
用户一直放在手机的后台(比如微信),并且也没出现内存警告的话,这个 didFinishLaunchingWithOptions:
方法应该一直不会调用。