這次筆記中主要描述的有:
- JSPath原理理解(學習作者大牛博客)
- JSPatch使用的時機
- AppDelegate中更新JS文件后何時生效
- 其他時機手動更新JS文件的效果
- JS調(diào)用OC方法中的幾個坑
- JS腳本文件的版本控制管理
- 更多思考
1. JSPatch原理淺談
JSPatch用iOS內(nèi)置的JavaScriptCore.framework作為JS引擎,但沒有用它JSExport的特性進行JS-OC函 數(shù)互調(diào),而是通過Objective-C Runtime,從JS傳遞要調(diào)用的類名函數(shù)名到Objective-C,再使用NSInvocation動態(tài)調(diào)用對應的OC方法。
詳細原理介紹可見作者博客:JSPatch 實現(xiàn)原理詳解
另外JSPatch已經(jīng)有商業(yè)化的平臺jspatch.com,可以使用里面的SDK,通過這個平臺上傳的js腳本都存儲在七牛云。
2. JSPatch使用的時機
確切地說應該是在經(jīng)過怎樣的流程之后開始載入調(diào)用JS腳本。
在董鉑然的博客:JSPatch使用小記中有這樣的一套方案:


這個方案的特點:
- 添加了上次請求的時間,避免多余的網(wǎng)絡請求
- 把更新js腳本的代碼放在了
applicationDidBecomeActive:方法中,避免程序在后臺的時候也進行不必要的腳本更新檢查。 - 對js文件進行code校驗,避免傳輸過程中被修改。實際使用中應對js腳本文件進行加密。(作者的博客中也建議用RSA等非對稱加密對文件進行加密傳輸)
- 連續(xù)崩潰次數(shù)的判斷,能夠做到程序自我選擇性修復
3. AppDelegate中更新JS文件后何時生效
當經(jīng)過本文第二點中流程之后,本地載入了最新的js腳本文件,那么程序什么時候會使用這個腳本中的代碼呢?
為此我寫了demo測試:
AppDelegate中的代碼
判斷本地是否存在hotfix.js文件:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSString *docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *hotfixPath = [docuPath stringByAppendingPathComponent:@"hotfix.js"];
if ([[NSFileManager defaultManager] fileExistsAtPath:hotfixPath]) {
[JPEngine startEngine];
NSString *script = [NSString stringWithContentsOfFile:hotfixPath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
}
return YES;
}
檢測是否需要更新:
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
[HotFixManager checkUpdateCompleteHandle:^(BOOL status, NSString *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!status){
if (!error) {
NSLog(@"沒有更新");
} else {
NSLog(@"%@", error.userInfo);
}
return ;
}
NSLog(@"Hotfix文件更新成功");
[JPEngine startEngine];
NSString *script = [NSString stringWithContentsOfFile:response encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
});
}];
}
- 首先第一點可以確定的是:
判斷本地是否存在js文件需要加載的代碼不能夠?qū)懺诰W(wǎng)絡請求是否需要更新的回調(diào)中
原因:
首先看下我的hotfix.js文件中的代碼
defineClass('JSPatchController', [/*新增的屬性*/'updateLabel'], {//實例方法
viewDidLoad: function() {
// self.ORIGviewDidLoad()
self.setTitle("測試JS1")
self.view().addSubview(self.getUpdateLabel())
},
//實現(xiàn)Label的getter方法
getUpdateLabel: function() {
var _updateLabel = self.updateLabel()
if (!_updateLabel) {
_updateLabel = require('UILabel').alloc().init()
_updateLabel.setFrame({x:50, y:100, width:100, height:30})
_updateLabel.setText("點擊按鈕更新JS代碼--->")
_updateLabel.setFont(require('UIFont').systemFontOfSize(15))
_updateLabel.setTextColor(require('UIColor').redColor())
_updateLabel.sizeToFit()
self.setUpdateLabel(_updateLabel)
}
return _updateLabel
},
})
代碼中是有添加了updateLabel這樣一個屬性,并添加到JSPatchController的視圖中,其中JSPatchController為window下navigationController的rootViewController。
當我嘗試在檢查更新的block沒有更新的區(qū)塊中執(zhí)行
[JPEngine startEngine];
NSString *script = [NSString stringWithContentsOfFile:hotfixPath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script]
發(fā)現(xiàn)JSPatchController并沒有添加上updateLabel,起初以為是沒有回到主線程中更新視圖,經(jīng)過嘗試并不是。當執(zhí)行NSURLSessionDataTask的網(wǎng)絡請求后,會新開啟一個線程。而主線程繼續(xù)執(zhí)行編譯JSPatchController,當新的線程中的網(wǎng)絡請求完成了,然后加載了js腳本文件,此時已經(jīng)不能再對JSPatchController動態(tài)添加updateLabel了。
第二點:
didFinishLaunchingWithOptions:的執(zhí)行優(yōu)先級是高于applicationDidBecomeActive:方法的,檢查本地的js文件應發(fā)在前者中。第三點:
applicationDidBecomeActive:方法中檢測需要更新并下載了js腳本文件,但是只有下一次啟動App的時候才能生效在根控制器中的動態(tài)修改代碼只能下一次啟動App的時候才能生效。
4. 其他時機手動更新JS文件的效果
其他時機這里指的是例如UIControl事件,點擊按鈕后更新js腳本,那么這個腳本文件中的代碼何時生效呢?
在demo中的JSPatchController中 “更新JS” 這個按鈕,點擊執(zhí)行的代碼如下:
- (IBAction)updateJS:(UIButton *)sender {
sender.selected = !sender.isSelected;
NSString *url = sender.isSelected ? jsfile1 : jsfile2;
[sender setTitle:@"當前JS1" forState:UIControlStateSelected];
[sender setTitle:@"當前JS2" forState:UIControlStateNormal];
NSLog(@"下載的是%@",url);
[HotFixManager downLoadHotFixJSfileWithURL:url completeHandle:^(BOOL status, id response, NSError *error) {
if (!status){
NSLog(@"下載出錯\n%@", error.userInfo);
return ;
}
NSLog(@"Hotfix文件更新成功");
[JPEngine startEngine];
NSString *script = [NSString stringWithContentsOfFile:response encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
}];
}
點按按鈕有兩個js腳本文件可以進行切換,兩個js文件中的區(qū)別部分為對下一個控制器中的lable文字和navigation title的控制
jsfile1:
defineClass('SecondViewController', {
viewDidLoad: function() {
self.ORIGviewDidLoad()
var label = self.myLabel()
label.setText("這是在JS1中修改的文字")
self.setTitle("JS1推出的頁面")
}
})
jsfile2:
defineClass('SecondViewController', {
viewDidLoad: function() {
self.ORIGviewDidLoad()
var label = self.myLabel()
label.setText("這是在JS2中修改的文字")
self.setTitle("JS2推出的頁面")
}
})
測試結(jié)果是:更新的js腳本中對之后的頁面的js修復代碼是可以生效的,但是對之前的頁面跟同級的頁面是沒有效果的。
其實這些,只要搞清楚原理就都能知道緣由和規(guī)律。
5. JS調(diào)用OC方法中的幾個坑
- 在js中 NSNumber不需要在處理,可直接當數(shù)值使用。
- NSRang 初始化:
var range = {location: 0, length: senderName.length()}; - 無論變量還是方法,單下劃線全部改為雙下劃線
- CGRect 取寬高, 直接
rect.width, 不用rect.size.width。其他結(jié)構(gòu)體類似 - js 中 YES 為 ture,NO 為false
- oc對象轉(zhuǎn)js對象可操作
toJS(),js對象轉(zhuǎn)oc對象暫時沒找到方法。
js內(nèi)創(chuàng)建的字典為js對象,傳入oc方法無效 - jspatch 不支持變參方法,如
stringWithFormat:,可用js字符串方法或NSMutableString代替。
6. JS腳本文件的版本控制管理
一般使用的js腳本文件都是從服務器下發(fā)過來,服務器的接口需要返回時候需要更新的參數(shù),需要則下載于當前程序版本號對應版本的js腳本文件。對于多個版本的項目來說,js腳本文件最好也進行版本控制避免出錯,可以在服務器單獨建立js腳本文件的Git倉庫來進行管理。
7. 更多思考
- JS熱修復的代碼在下一次更新中應當使用原生的代碼替換,不能超過一個版本。避免對JSPatch有過多的依賴
- 使用JS語法來調(diào)用OC的方法,沒有代碼自動補全顯得非常吃力。作者bang開發(fā)了Xcode插件:JSPatchX,然而Xcode8不支持插件了。。。
- 在一個很復雜的方法中,僅中間某一行代碼需要修改,就要將整個方法用JS重寫一遍,推介作者開發(fā)的Objective-C轉(zhuǎn)JavaScript代碼工具JSPatch Convertor,但一些復雜的語法還是要人工修正