一、熱修原理
JSPatch 是一個(gè) iOS 動(dòng)態(tài)更新框架,只需在項(xiàng)目中引入極小的引擎,就可以使用 JavaScript 調(diào)用任何 Objective-C 原生接口,獲得腳本語(yǔ)言的優(yōu)勢(shì):為項(xiàng)目動(dòng)態(tài)添加模塊,或替換項(xiàng)目原生代碼動(dòng)態(tài)修復(fù)BUG。
JSPatch 能做到通過(guò) JS 調(diào)用和改寫(xiě) OC 方法最根本的原因是 Objective-C 是動(dòng)態(tài)語(yǔ)言,OC 上所有方法的調(diào)用/類的生成都通過(guò) Objective-C Runtime 在運(yùn)行時(shí)進(jìn)行,我們可以通過(guò)類名/方法名反射得到相應(yīng)的類和方法。
所以 JSPatch 的基本原理就是:JS 傳遞字符串給 OC,OC 通過(guò) Runtime 接口調(diào)用和替換 OC 方法,這是最基礎(chǔ)的原理。
1.1 熱修的方法替換過(guò)程


第1步:[JPEngine startEngine]的調(diào)用
通過(guò)蘋(píng)果官方提供的JavaScriptCore框架,使用JSContext對(duì)象實(shí)現(xiàn) JS 和 Native 的交互。這個(gè)框架很簡(jiǎn)單但非常強(qiáng)大,可以網(wǎng)上搜索掌握它的用法,這里不再細(xì)說(shuō),但是掌握這個(gè)框架的基本使用才能繼續(xù)分析JSPatch框架。
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
如上OC代碼是指向 JS 注入了全局的 _OC_defineClass 方法,其具體實(shí)現(xiàn)對(duì)應(yīng)著 Native 的 block,這就是JavaScriptCore的強(qiáng)大之處。這樣一來(lái),我們可以在 JS 代碼中直接調(diào)用 _OC_defineClass 這個(gè)方法,即可調(diào)用到 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方法接收的參數(shù)是:
1:類名字符串。
2:類的實(shí)例方法和類方法列表(都是js對(duì)象的形式)。
defineClass方法會(huì)首先分別對(duì)這兩個(gè)對(duì)象調(diào)用 _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 作用,簡(jiǎn)單的說(shuō)就是它把defineClass中傳遞過(guò)來(lái)的JS對(duì)象進(jìn)行了修改:
原來(lái)的形式是:
{
methodName:function(){...}
}
修改之后是:
{
methodName: [argCount, function(){...新的實(shí)現(xiàn)}]
}
傳遞參數(shù)個(gè)數(shù)的目的是:runtime 在修復(fù)類的時(shí)候,無(wú)法直接解析原始的JS實(shí)現(xiàn)函數(shù),那么就不知道參數(shù)的個(gè)數(shù),特別是在創(chuàng)建新的方法的時(shí)候,需要根據(jù)參數(shù)個(gè)數(shù)生成方法簽名,所以只能在JS端拿到JS函數(shù)的參數(shù)個(gè)數(shù),傳遞到OC端。
第3步:_OC_defineClass方法實(shí)現(xiàn)
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
在JPEngine中,定了一個(gè)名為defineClass的函數(shù),這個(gè)函數(shù)對(duì)類進(jìn)行真正的重寫(xiě)操作。我們知道runtime重寫(xiě)一個(gè)方法,需要幾個(gè)最基本的參數(shù):類名、selector、方法實(shí)現(xiàn)(IMP)、方法簽名,defineClass做的就是把這些信息提取出來(lái):
1、首先是對(duì)類名進(jìn)行解析,把協(xié)議名、類名、父類名都解析出來(lái)。如果類不存在,那么創(chuàng)建并注冊(cè)該類。
2、分別對(duì)實(shí)例方法和類方法進(jìn)行處理,JS函數(shù)_formatDefineMethods處理返回的是JS對(duì)象,傳遞到OC這邊會(huì)被JavaScriptCore轉(zhuǎn)換為JSValue對(duì)象,可以對(duì)該對(duì)象直接調(diào)用toDictionary把JS對(duì)象轉(zhuǎn)換成OC字典。這樣我們就可以取到方法名、參數(shù)個(gè)數(shù)、具體實(shí)現(xiàn)。JSValue與Object的轉(zhuǎn)換3、遍歷字典的
key,即方法名,根據(jù)方法名取出的值還是JSValue對(duì)象,不過(guò)它代表的是數(shù)組,第一個(gè)值是參數(shù)的個(gè)數(shù),第二個(gè)值是函數(shù)的實(shí)現(xiàn)。
4、方法名的處理:這塊涉及到方法名的格式要求和處理,例如:在JS中的tableView_numberOfRowsInSection,下劃線需要被替換成':'。
5、最后拿著處理好的方法名和具體實(shí)現(xiàn)等調(diào)用overrideMethod函數(shù)。
第4步:overrideMethod 函數(shù)實(shí)現(xiàn)
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
1、把
selector對(duì)應(yīng)的具體實(shí)現(xiàn)使用class_replaceMethod替換成_objc_msgForward,我們知道這個(gè)對(duì)應(yīng)著消息轉(zhuǎn)發(fā)機(jī)制。
2、把forwardInvocation的具體實(shí)現(xiàn)替換成JPForwardInvocation實(shí)現(xiàn)。
3、向class添加名為ORIGforwardInvocation的方法,實(shí)現(xiàn)是原始的forwardInvocation的IMP。
4、向class添加名為ORIG+selector,對(duì)應(yīng)原始selector的IMP。JS可以通過(guò)這個(gè)方法調(diào)用到原來(lái)的實(shí)現(xiàn)。
5、向class添加名為_JP + selector,對(duì)應(yīng)JS重寫(xiě)的函數(shù)實(shí)現(xiàn)。
1.2 熱修的方法執(zhí)行流程

第1步:JPForwardInvocation函數(shù)
經(jīng)過(guò)上一步的處理,調(diào)用被熱修的selector時(shí),其實(shí)調(diào)用的是objc_msgForward,即走到了消息轉(zhuǎn)發(fā)的環(huán)節(jié)。而在此的上一步中把 forwardInvocation 方法的實(shí)現(xiàn)替換成了 JPForwardInvocation 方法。
1、把
selector前面加上_JP前綴,構(gòu)成的新的selector,如果本地存儲(chǔ)的字典中沒(méi)有存儲(chǔ)對(duì)應(yīng)的JS方法實(shí)現(xiàn),說(shuō)明這個(gè)不是我們重寫(xiě)的方法,那么走原來(lái)的消息轉(zhuǎn)發(fā);否則調(diào)用JS方法實(shí)現(xiàn)。2、把
self和其他的參數(shù)都轉(zhuǎn)換稱JS對(duì)象,JS端重寫(xiě)的函數(shù)傳遞過(guò)來(lái)是JSValue類型,這里對(duì)應(yīng)著JS函數(shù),可以對(duì)其調(diào)用callWithArgument方法,參數(shù)轉(zhuǎn)換成JS對(duì)象,執(zhí)行函數(shù)。
實(shí)際上這個(gè)方法的細(xì)節(jié)是非常多的,根據(jù)方法簽名,取出每個(gè)參數(shù)的類型,進(jìn)行參數(shù)的封裝、對(duì)于結(jié)構(gòu)體的處理等等。
講到這里,就完成了函數(shù)調(diào)用環(huán)節(jié)。
第2步:callSelector函數(shù)
在最開(kāi)始startEngine的時(shí)候,會(huì)把這些熱修的JS代碼統(tǒng)一進(jìn)行正則表達(dá)式的匹配替換,也就是把所有的函數(shù)都替換成對(duì)名為 __c() 函數(shù)的調(diào)用,例如:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
__c() 具體的實(shí)現(xiàn)就是調(diào)用了 _methodFunc() 函數(shù)。
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)
}
參見(jiàn)源碼我們可以知道 _methodFunc() 函數(shù)會(huì)調(diào)用 _OC_call 函數(shù),而在startEngine的一開(kāi)始,我們就為JSContext注入了 _OC_callI、_OC_callC 函數(shù),具體實(shí)現(xiàn)是一個(gè)調(diào)用了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 函數(shù)中主要做的事情有:
1、把
JS對(duì)象和JS參數(shù)轉(zhuǎn)換為OC對(duì)象;
2、判斷是否調(diào)用的是父類的方法,如果是就走父類的方法實(shí)現(xiàn);
3、把參數(shù)等信息封裝成NSInvocation對(duì)象并執(zhí)行,然后返回結(jié)果;
具體的實(shí)現(xiàn)細(xì)節(jié)包括對(duì)methodSignature的字符處理,根據(jù)這些字符對(duì)JS對(duì)象進(jìn)行處理和轉(zhuǎn)換,還有對(duì)結(jié)構(gòu)體對(duì)支持等。
二、熱修語(yǔ)法
1.JSPatch實(shí)現(xiàn)原理教程: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官方的熱修語(yǔ)法文檔: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/
三、其他問(wèn)題
3.1.為什么要重啟,熱修文件才會(huì)生效的原因。
JS文件下載的位置是:applicationDidBecomeActive: 方法,使用JS的代碼放在 didFinishLaunchingWithOptions: 這個(gè)方法。因?yàn)檫@個(gè)方法在程序啟動(dòng)和后臺(tái)回到前臺(tái)時(shí)都會(huì)調(diào)用,并且端上可以設(shè)置一個(gè)間隔時(shí)間的策略,也就是說(shuō)每次來(lái)到這個(gè)方法時(shí),先要檢測(cè)是距離上次發(fā)請(qǐng)求的時(shí)間間隔是否超過(guò)1小時(shí),超過(guò)則發(fā)請(qǐng)求,否則跳過(guò)。因?yàn)槿绻@個(gè)app用戶一直放在手機(jī)的后臺(tái)(比如微信),并且也沒(méi)出現(xiàn)內(nèi)存警告的話,這個(gè) didFinishLaunchingWithOptions: 方法應(yīng)該一直不會(huì)調(diào)用。
