一湃缎、熱修原理
JSPatch
是一個 iOS 動態(tài)更新框架,只需在項目中引入極小的引擎贯吓,就可以使用 JavaScript
調(diào)用任何 Objective-C
原生接口,獲得腳本語言的優(yōu)勢:為項目動態(tài)添加模塊,或替換項目原生代碼動態(tài)修復(fù)BUG
。
JSPatch
能做到通過 JS
調(diào)用和改寫 OC
方法最根本的原因是 Objective-C
是動態(tài)語言,OC
上所有方法的調(diào)用/類的生成都通過 Objective-C Runtime
在運行時進行,我們可以通過類名/方法名反射得到相應(yīng)的類和方法辩恼。
所以 JSPatch
的基本原理就是:JS
傳遞字符串給 OC
,OC
通過 Runtime
接口調(diào)用和替換 OC
方法谓形,這是最基礎(chǔ)的原理运挫。
1.1 熱修的方法替換過程
第1步:[JPEngine startEngine]
的調(diào)用
通過蘋果官方提供的JavaScriptCore
框架,使用JSContext
對象實現(xiàn) JS
和 Native
的交互套耕。這個框架很簡單但非常強大谁帕,可以網(wǎng)上搜索掌握它的用法,這里不再細說冯袍,但是掌握這個框架的基本使用才能繼續(xù)分析JSPatch
框架匈挖。
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
如上OC
代碼是指向 JS
注入了全局的 _OC_defineClass
方法,其具體實現(xiàn)對應(yīng)著 Native
的 block
康愤,這就是JavaScriptCore
的強大之處儡循。這樣一來,我們可以在 JS
代碼中直接調(diào)用 _OC_defineClass
這個方法征冷,即可調(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:類的實例方法和類方法列表(都是js對象的形式)检激。
defineClass
方法會首先分別對這兩個對象調(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
作用,簡單的說就是它把defineClass
中傳遞過來的JS
對象進行了修改:
原來的形式是:
{
methodName:function(){...}
}
修改之后是:
{
methodName: [argCount, function(){...新的實現(xiàn)}]
}
傳遞參數(shù)個數(shù)的目的是:runtime
在修復(fù)類的時候叔收,無法直接解析原始的JS
實現(xiàn)函數(shù)齿穗,那么就不知道參數(shù)的個數(shù),特別是在創(chuàng)建新的方法的時候饺律,需要根據(jù)參數(shù)個數(shù)生成方法簽名窃页,所以只能在JS
端拿到JS
函數(shù)的參數(shù)個數(shù),傳遞到OC
端。
第3步:_OC_defineClass
方法實現(xiàn)
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
在JPEngine
中脖卖,定了一個名為defineClass
的函數(shù)乒省,這個函數(shù)對類進行真正的重寫操作。我們知道runtime
重寫一個方法畦木,需要幾個最基本的參數(shù):類名
作儿、selector
、方法實現(xiàn)(IMP)
馋劈、方法簽名
,defineClass
做的就是把這些信息提取出來:
1晾嘶、首先是對類名進行解析妓雾,把協(xié)議名、類名垒迂、父類名都解析出來械姻。如果類不存在,那么創(chuàng)建并注冊該類机断。
2楷拳、分別對實例方法和類方法進行處理,JS
函數(shù)_formatDefineMethods
處理返回的是JS
對象吏奸,傳遞到OC
這邊會被JavaScriptCore
轉(zhuǎn)換為JSValue
對象欢揖,可以對該對象直接調(diào)用toDictionary
把JS
對象轉(zhuǎn)換成OC
字典。這樣我們就可以取到方法名
奋蔚、參數(shù)個數(shù)
她混、具體實現(xiàn)
。JSValue與Object的轉(zhuǎn)換3泊碑、遍歷字典的
key
坤按,即方法名,根據(jù)方法名取出的值還是JSValue
對象馒过,不過它代表的是數(shù)組臭脓,第一個值是參數(shù)的個數(shù),第二個值是函數(shù)的實現(xiàn)腹忽。
4来累、方法名的處理:這塊涉及到方法名的格式要求和處理,例如:在JS
中的tableView_numberOfRowsInSection
窘奏,下劃線需要被替換成':'
佃扼。
5、最后拿著處理好的方法名和具體實現(xiàn)等調(diào)用overrideMethod
函數(shù)蔼夜。
第4步:overrideMethod
函數(shù)實現(xiàn)
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
1兼耀、把
selector
對應(yīng)的具體實現(xiàn)使用class_replaceMethod
替換成_objc_msgForward
,我們知道這個對應(yīng)著消息轉(zhuǎn)發(fā)機制。
2瘤运、把forwardInvocation
的具體實現(xiàn)替換成JPForwardInvocation
實現(xiàn)窍霞。
3、向class
添加名為ORIGforwardInvocation
的方法拯坟,實現(xiàn)是原始的forwardInvocation
的IMP
但金。
4、向class
添加名為ORIG+selector
郁季,對應(yīng)原始selector
的IMP
冷溃。JS
可以通過這個方法調(diào)用到原來的實現(xiàn)。
5梦裂、向class
添加名為_JP + selector
似枕,對應(yīng)JS
重寫的函數(shù)實現(xiàn)。
1.2 熱修的方法執(zhí)行流程
第1步:JPForwardInvocation
函數(shù)
經(jīng)過上一步的處理年柠,調(diào)用被熱修的selector
時凿歼,其實調(diào)用的是objc_msgForward
,即走到了消息轉(zhuǎn)發(fā)的環(huán)節(jié)冗恨。而在此的上一步中把 forwardInvocation
方法的實現(xiàn)替換成了 JPForwardInvocation
方法答憔。
1、把
selector
前面加上_JP
前綴掀抹,構(gòu)成的新的selector
虐拓,如果本地存儲的字典中沒有存儲對應(yīng)的JS
方法實現(xiàn),說明這個不是我們重寫的方法傲武,那么走原來的消息轉(zhuǎn)發(fā)侯嘀;否則調(diào)用JS
方法實現(xiàn)。2谱轨、把
self
和其他的參數(shù)都轉(zhuǎn)換稱JS
對象戒幔,JS
端重寫的函數(shù)傳遞過來是JSValue
類型,這里對應(yīng)著JS
函數(shù)土童,可以對其調(diào)用callWithArgument
方法诗茎,參數(shù)轉(zhuǎn)換成JS
對象,執(zhí)行函數(shù)献汗。
實際上這個方法的細節(jié)是非常多的敢订,根據(jù)方法簽名,取出每個參數(shù)的類型罢吃,進行參數(shù)的封裝楚午、對于結(jié)構(gòu)體的處理等等。
講到這里尿招,就完成了函數(shù)調(diào)用環(huán)節(jié)矾柜。
第2步:callSelector
函數(shù)
在最開始startEngine
的時候阱驾,會把這些熱修的JS
代碼統(tǒng)一進行正則表達式的匹配替換,也就是把所有的函數(shù)都替換成對名為 __c()
函數(shù)的調(diào)用怪蔑,例如:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
__c()
具體的實現(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)
}
參見源碼我們可以知道 _methodFunc()
函數(shù)會調(diào)用 _OC_call
函數(shù),而在startEngine
的一開始缆瓣,我們就為JSContext
注入了 _OC_callI
喧枷、_OC_callC
函數(shù),具體實現(xiàn)是一個調(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
對象和JS
參數(shù)轉(zhuǎn)換為OC
對象隧甚;
2、判斷是否調(diào)用的是父類的方法渡冻,如果是就走父類的方法實現(xiàn)戚扳;
3、把參數(shù)等信息封裝成NSInvocation
對象并執(zhí)行菩帝,然后返回結(jié)果;
具體的實現(xiàn)細節(jié)包括對methodSignature
的字符處理茬腿,根據(jù)這些字符對JS
對象進行處理和轉(zhuǎn)換呼奢,還有對結(jié)構(gòu)體對支持等。
二切平、熱修語法
1.JSPatch
實現(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
官方的熱修語法文檔: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:
這個方法。因為這個方法在程序啟動和后臺回到前臺時都會調(diào)用苔严,并且端上可以設(shè)置一個間隔時間的策略定枷,也就是說每次來到這個方法時,先要檢測是距離上次發(fā)請求的時間間隔是否超過1小時届氢,超過則發(fā)請求欠窒,否則跳過。因為如果這個app
用戶一直放在手機的后臺(比如微信)退子,并且也沒出現(xiàn)內(nèi)存警告的話岖妄,這個 didFinishLaunchingWithOptions:
方法應(yīng)該一直不會調(diào)用。