JSPatch原理剖析


一湃缎、熱修原理

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 傳遞字符串給 OCOC 通過 Runtime 接口調(diào)用和替換 OC 方法谓形,這是最基礎(chǔ)的原理运挫。

1.1 熱修的方法替換過程
JSPatch準(zhǔn)備過程
熱修文件示例
第1步:[JPEngine startEngine]的調(diào)用

通過蘋果官方提供的JavaScriptCore框架,使用JSContext對象實現(xiàn) JSNative 的交互套耕。這個框架很簡單但非常強大谁帕,可以網(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)著 Nativeblock康愤,這就是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 方法肴捉。

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 作用,簡單的說就是它把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)用toDictionaryJS對象轉(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)是原始的 forwardInvocationIMP但金。
4、向class添加名為ORIG+selector郁季,對應(yīng)原始selectorIMP冷溃。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)用了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對象和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)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寂祥,一起剝皮案震驚了整個濱河市荐虐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丸凭,老刑警劉巖福扬,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腕铸,死亡現(xiàn)場離奇詭異,居然都是意外死亡忧换,警方通過查閱死者的電腦和手機恬惯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亚茬,“玉大人酪耳,你說我怎么就攤上這事∩卜欤” “怎么了碗暗?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長梢夯。 經(jīng)常有香客問我言疗,道長,這世上最難降的妖魔是什么颂砸? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任噪奄,我火速辦了婚禮,結(jié)果婚禮上人乓,老公的妹妹穿的比我還像新娘勤篮。我一直安慰自己,他們只是感情好色罚,可當(dāng)我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布碰缔。 她就那樣靜靜地躺著,像睡著了一般戳护。 火紅的嫁衣襯著肌膚如雪金抡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天腌且,我揣著相機與錄音梗肝,去河邊找鬼。 笑死铺董,一個胖子當(dāng)著我的面吹牛统捶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柄粹,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼喘鸟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驻右?” 一聲冷哼從身側(cè)響起什黑,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎堪夭,沒想到半個月后愕把,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拣凹,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年恨豁,在試婚紗的時候發(fā)現(xiàn)自己被綠了嚣镜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡橘蜜,死狀恐怖菊匿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情计福,我是刑警寧澤跌捆,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站象颖,受9級特大地震影響佩厚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜说订,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一抄瓦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧陶冷,春花似錦钙姊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悉罕。三九已至赤屋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壁袄,已是汗流浹背类早。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嗜逻,地道東北人涩僻。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像栈顷,于是被迫代替她去往敵國和親逆日。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容