JSPatch原理剖析


一、熱修原理

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ò)程
JSPatch準(zhǔn)備過(guò)程
熱修文件示例
第1步:[JPEngine startEngine]的調(diào)用

通過(guò)蘋(píng)果官方提供的JavaScriptCore框架,使用JSContext對(duì)象實(shí)現(xiàn) JSNative 的交互。這個(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)著 Nativeblock,這就是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 方法。

JSPatch的全局函數(shù)
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)用toDictionaryJS對(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)是原始的 forwardInvocationIMP。
4、向class添加名為ORIG+selector,對(duì)應(yīng)原始selectorIMPJS 可以通過(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)用了OCcallSelectorblock:

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)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容