一、用途
是否有過這樣的經歷:新版本上線後發現有個嚴重的bug,可能會導致crash率激增,可能會使網絡請求無法發出,這時能做的只是趕緊修復bug然後提交等待漫長的AppStore審覈,再盼望用戶快點升級,付出巨大的人力和時間成本,才能完成此次bug的修復。
使用JSPatch可以解決這樣的問題,只需在項目中引入JSPatch,就可以在發現bug時下發JS腳本補丁,替換原生方法,無需更新APP即時修復bug。
二、原理
JSPatch用iOS內置的JavaScriptCore.framework作爲JS引擎,但沒有用它JSExport的特性進行JS-OC函數互調,而是通過Objective-CRuntime,從JS傳遞要調用的類名函數名到Objective-C,再使用NSInvocation動態調用對應的OC方法。
三、通過實例瞭解JSPatch使用過程。
首先簡單介紹一下這個實例要實現的功能
新建一個工程,有三個視圖
視圖1 ,rootViewController,
視圖2,firstViewControoler,
視圖3,sceondViewControll. (可以給firstViewControoler和SecondViewController分別加上不同的標題和背景色用以便好區分)
源代碼中,視圖1中有一個按鈕,點擊按鈕進入視圖2,代碼如下
- (void)enterNextView:(id)sender
{
FirstViewController* vc =[[FirstViewController alloc]init];
[self.navigationControllerpushViewController:vc animated:YES];
}
現在我們要定義一個js文件,通過JSPatch框架來實現,點擊按鈕進入的視圖不是視圖2而是視圖3.
1.首先需要引入JSPatch插件。
通過CocoaPod引入JSPatch插件:
pod ‘JSPatch’
工程引入系統框架 JavaScriptCore.framework
2.編寫js代碼(demo.js)。
require('SecondViewController')//聲明引用的object c中的類
defineClass(rootViewController, {
enterNextView: function(sender) {
var vc = SecondViewController.alloc().init()
self.navigationController().pushViewController_animated(vc, YES)
}
})
//defineClass覆蓋rootViewController裏原來的按鈕點擊事件enterNextView
3.加載js文件
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- {
// Override point for customization after application launch.
[JPEngine startEngine];//啓動JP引擎
//加載本地demo.js文件。
NSString *sourcePath = [[NSBundle mainBundle]pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:sourcePathencoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
return YES;
}
運行程序 ,這時點擊按鈕,執行的不是rootViewController裏的enterNextView函數,而是demo.js文件裏的enterNextView,界面也就從視圖1進入了視圖3界面而不是視圖2界面。
注:此處爲了方便實現加載本地的js文件 ,真正應用的時候,需要把這個js文件放在服務器上程序啓動時通過以下代碼加載。
[JPEngine startEngine]; //啓動JP引擎
//下載服務器端js文件並加載到JPEngine
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/bugfix.JS"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (script) {
[JPEngine evaluateScript:script];
}
四,JSPatch Loader使用
JSPatchLoader 負責根據版本號向服務端拉取 JSPatch代碼,並對代碼進行 RSA校驗/解壓/執行,整個校驗原理在JSPatch部署安全策略 這篇文章裏詳細說明,不再複述。安全策略可參考http://blog.cnbang.net/tech/2879/.
1.安裝
拷貝 Loader/ 目錄下的文件到你的項目。
2.配置
1)設 JPLoader.h的rootUrl 爲你的服務器地址。腳本文件在服務器的存放路徑是${rootUrl}/${appVersion}/${patchFile}
2)自行生成 RSA 公鑰私鑰,替換 JPLoader.h 裏的 publicKey 和 tools/pack.php 裏的 privateKey。
3腳本打包
1 JSPatch腳本文件規則:可以有多個 js 文件,腳本內可以調用 include() 接口包含,沒有目錄層級,必須包含一個 main.js 文件作爲入口。
2 在命令行使用 Loader/tools/pack.php腳本打包 JS 文件,由用戶放到自己的服務器上給客戶端下載。
示例
$ php pack.php main.js other.js
會在當前目錄生成 v1.zip 文件,打包了所有 js 文件幷包含了校驗文件。也可以在最後通過 -o 指定輸出文件名:
$ php pack.php main.js -o v2
腳本文件名代表當前 patch 版本,與後續的+updateToVersion:callback: 接口相關。
4 加載
下載/更新腳本
客戶端在得知服務端腳本有更新時,調用 +updateToVersion:callback:接口下載對應版本的腳本。至於如何得知服務端腳本更新可以自行定義,可以另外加個請求每次喚醒時詢問服務器,也可以在 APP 原有的請求里加上這個信息。
舉個例子,客戶端當前 App 版本號爲1.0,上述配置 rootUrl 變量配爲 http://localhost/JSPatch/,服務端告訴客戶端最新腳本版本號爲2,於是調用 [JPLoader updateToVersion:2callback:nil],這時會去請求 http://localhost/JSPatch/1.0/v2.zip這個文件並解壓驗證,保存到本地目錄等待執行。
執行腳本
通過 +run 接口執行已下載到本地的 JSPatch 腳本文件,建議在程序啓動的 -application:didFinishLaunchingWithOptions: 裏第一句調用這個接口,防止調用後執行 JSPatch 腳本過程中其他線程同時在執行相關代碼,導致意想不到的問題。
5測試
在腳本文件還沒打包上傳到服務器前,可以先把文件加入項目工程 bundle 進行測試,加入後調用 +runTestScriptInBundle 就會執行項目工程裏的 main.js 文件,並且 JS 腳本里 include() 接口也可以正常使用。
(參考https://github.com/bang590/JSPatch/wiki/JSPatch-Loader-使用文檔)
五、總結
要在項目中使用JSPatch實現動態更新ios,需要以下步聚
1.引入JSPatch和JavaScriptCore.framework
2.實現js文件並放到服務端供終端加載
(具本的實現js到object c轉換的語法和需要注意事項目可參考文檔https://github.com/bang590/JSPatch/wiki)
3.在AppDelegate中加載服務端的js文件
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[JPEngine startEngine];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/bugfix.JS"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (script) {
[JPEngine evaluateScript:script];
}
}];
….
return YES;
}
@end