JSPatch是什么
JSPatch是一個開源項目,只需要在項目里引入極小的引擎文件,就可以使用 JavaScript 調(diào)用任何 Objective-C 的原生接口,替換任意 Objective-C 原生方法。目前主要用于下發(fā) JS 腳本替換原生 Objective-C 代碼,實時修復(fù)線上 bug.
目錄
- JSPatch接入
- 示例代碼
- 安全問題
- 自定義RSA Key
- 常見問題:發(fā)布補丁后不起作用
- 一點兒小總結(jié)
JSPatch接入
- 注冊
注冊jspatch賬號并登錄.(http://www.jspatch.com).
- 添加應(yīng)用,獲取appKey
在"我的app"中添加新的app,未上線項目無需填寫AppStoreID,添加完成后可以看到appKey. - 導(dǎo)入framework和相關(guān)類庫
在官網(wǎng)下載SDK,將解壓得到的JSPatch.framework
導(dǎo)入到項目,并添加依賴的類庫:libz.dylib
和JavaScriptCore.framework
即可. - 測試補丁和發(fā)布補丁
新建一個main.js文件,在這個文件中編寫js代碼(用于替換oc代碼的),這個main.js文件如果在項目中,通過[JSPatch testScriptInBundle];
方法測試補丁,補丁測試通過后,可以將這個main.js文件上傳到j(luò)spatch官網(wǎng),通過[JSPatch startWithAppKey:@"APPKey"];
方法調(diào)用在線補丁,項目中的appDelega.m文件中的代理方法中寫入這個方法,發(fā)布的app每次啟動時會在線查找補丁,如果有版本號一致的補丁,則會下載這個補丁,用來替換相應(yīng)的方法. - 發(fā)布補丁包
在app管理中"新建版本",新建版本后,上傳main.js文件再點擊提交即可.需要注意的是新建版本中的版本,對應(yīng)項目的版本號,app在線上尋找補丁時,版本號是一個篩選條件.
代碼如下:
///本地測試
///AppDelegate.m
#import <JSPatch/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//本地測試版本
[JSPatch testScriptInBundle];
[JSPatch sync];
}
///線上補丁
///AppDelegate.m
#import <JSPatch/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//本地測試版本
[JSPatch startWithAppKey:@"APPKey"];
[JSPatch sync];
}
舉個栗子
OC代碼如下
///viewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.array = @[@"one",@"two",@"three"];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(135, 250, 100, 50)];
[button setBackgroundColor:[UIColor blueColor]];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
//按鈕點擊事件,模擬數(shù)組越界
-(void)buttonClicked:(UIButton *)sender{
UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"提示" message:self.array[3] delegate:self cancelButtonTitle:nil otherButtonTitles:@"確定", nil];
[av show];
NSLog(@"%@",self.array[3]);
}
下邊用js替換點擊方法buttonClicked
///main.js
defineClass("ViewController", {
buttonClicked: function(sender) {
//1.獲取/修改 Property: 等于調(diào)用這個 Property 的 getter / setter 方法,獲取時記得加()
//2.OC中的nil -->jspatch中的null
//3.require('className')
//4.NSLog(@"123") -->console.log("123")
var message = self.array().objectAtIndex(0);
var temAlertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("提示",message, self, "OK", null);
temAlertView.show()
console.log(message);
}
})
///AppDelegate.m
#import <JSPatch/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//本地測試版本
[JSPatch startWithAppKey:@"APPKey"];
[JSPatch sync];
}
然后上傳這個main.js,發(fā)布補丁
關(guān)于安全問題
傳輸安全
JSPatch腳本的執(zhí)行權(quán)限很高,若在傳輸過程中被中間人篡改,會帶來很大的安全問題,為了防止這種情況出現(xiàn),我們在傳輸過程中對JS文件進行了RSA簽名加密,流程如下:
服務(wù)端:
計算 JS 文件 MD5 值。
用 RSA 私鑰對 MD5 值進行加密,與JS文件一起下發(fā)給客戶端。
客戶端:
拿到加密數(shù)據(jù),用 RSA 公鑰解密出 MD5 值。
本地計算返回的 JS 文件 MD5 值。
對比上述的兩個 MD5 值,若相等則校驗通過,取 JS 文件保存到本地。
由于 RSA 是非對稱加密,在沒有私鑰的情況下第三方無法加密對應(yīng)的 MD5 值,也就無法偽造 JS 文件,杜絕了 JS 文件在傳輸過程被篡改的可能。
本地存儲
本地存儲的腳本被篡改的機會小很多,只在越獄機器上有點風(fēng)險,對此 JSPatch SDK 在下載完腳本保存到本地時也進行了簡單的對稱加密,每次讀取時解密。
自定義RSA Key
客戶端和 JSPatch 后臺默認(rèn)有一對 RSA 密鑰,默認(rèn)會用這對密鑰進行加解密驗證。
若對安全要求較高,可以自定義 RSA 密鑰,然后將公鑰通過:
[JSPatch setupRSAPublicKey:@"******"]
方法寫入app代碼中,而私鑰需要開發(fā)者自行保留,以后每次發(fā)布補丁的時候,需要同時上傳這個私鑰,如圖:(生成秘鑰的具體方法看官方文檔,點這里)
上傳私鑰.png
這里上傳的 rsa_private_key.pem 只是一次性使用,不會保存在服務(wù)端,所以只有通過用戶自己保存的 rsa_private_key.pem 文件才可以針對 APP 下發(fā)腳本,即使 JSPatch 平臺或者七牛云被黑,第三方也無法對你的 APP 下發(fā)惡意腳本(可以下發(fā),但驗證不過,不會執(zhí)行),保證安全性。rsa_private_key.pem 請妥善保管,避免泄露。
使用自定義RSA秘鑰的作用:
使用自定義的RSA秘鑰,可以保障以下兩種情況下的安全:
- 當(dāng)jspatch賬號被盜時,盜號者沒有私鑰,即使發(fā)布惡意補丁包,該補丁也不會被執(zhí)行.(因為沒有私鑰,無法通過驗證)
- 當(dāng)jspatch服務(wù)器被黑,由于jspatch方面并不保留上傳的私鑰,所以app依然是安全的.
常見問題:發(fā)布補丁后不起作用
原因可能有:
1.APPKey寫錯了(好好檢查).
錯誤信息:JSPatch: request success {error = "Document not found";}
2.發(fā)布的版本號和項目的版本號不一致(一定要一致,1.2版本的項目只能用1.2版本的補丁包)
錯誤信息:JSPatch: request success {error = "Document not found";}
3.main.js文件錯誤,是否找對了"類"和"方法"(類名和方法名要是寫錯,補丁想起作用也做不到啊)
錯誤信息:不會出現(xiàn)任何"JSPatch: request success {}"提示
4.main.js文件語法錯誤(main.js文件中的js語法錯誤,或者是oc方法名錯誤都會導(dǎo)致編譯不通過,從而不執(zhí)行main.js方法,最可怕的是他還不報錯,jspatch的原則是,js文件能用就用,不能用,我不告訴你,就是不用.)(一個很長的oc方法轉(zhuǎn)寫成js方法是,是沒有提示的,一個字母寫錯了,整個js文件就廢了,最關(guān)鍵的是,這些錯誤的是隱形的,不提示,難發(fā)覺,更難找,所以我建議兩點,第一,寫補丁時現(xiàn)在本地測試,在本地的main.js文件中寫一行測試一行,保證每一行都是正確的.第二,把要改的oc代碼,暫時copy到main.js文件中,這樣,當(dāng)你在js中寫oc的方法名時,而這個方法名在你copy的那段代碼中,xcode就會給提示.)
錯誤信息:輸出成功信息JSPatch: request success {v = 1;},但是依然執(zhí)行oc代碼而不是js代碼
一點兒小總結(jié)
補丁發(fā)布后,app運行時會把補丁下載下來,一切驗證通過后,補丁就可以運行了,這時候,如果你在jspatch管理補丁頁面停用了補丁,下載過補丁的app還是會執(zhí)行補丁.
那么問題來了,我們?nèi)绾巫屢呀?jīng)下載過補丁的app不執(zhí)行補丁呢?
最簡單的方法是,先停用補丁,然后把app卸載后重新安裝.但是總不能讓客戶先卸載再重新安裝吧.
然后我做了以下幾個實驗:
實驗假設(shè): 我們已經(jīng)發(fā)布補丁修改了代碼中的方法 funA,下載過補丁的app執(zhí)行補丁中的方法,不再執(zhí)行原方法funA,現(xiàn)在如何讓下載過補丁的app不再執(zhí)行補丁,而是執(zhí)行原來的方法呢?
實驗1. 發(fā)布一個空白的補丁,也就新建一個空白的main.js文件,然后上傳,企圖用這個空白的補丁覆蓋原來的.
結(jié)果: 失敗了,下載過補丁的app依然執(zhí)行補丁.
實驗2. 把原來補丁里的方法名改掉,比如把funA 改成 funAXiugai,這樣補丁找不到對應(yīng)的方法,原方法就可以執(zhí)行了.
結(jié)果: 失敗了,下載過補丁的app依然執(zhí)行補丁.
實驗3. 在代碼中有一個永不執(zhí)行的(不被調(diào)用)方法funForeverNotDo,這個方法就是用來預(yù)防這種情況而存在的(好凄慘的備胎啊),然后寫一個main.js,里邊只修改funForeverNotDo方法,不再修改funA.
結(jié)果: 成功啦
更新
擦擦擦擦,打臉了,如果想要已經(jīng)下載過補丁的app不再執(zhí)行補丁,直接"刪除版本"就可以了:
再更新
有一個神奇的網(wǎng)站,可以把OC代碼翻譯成JS代碼:
傳送門
Github上有本地源碼:
Github本地源碼