例如線上 APP 有一段代碼出現 bug 致使 crash:前端
@implementation JPTableViewController ... - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSString *content = self.dataSource[[indexPath row]]; //可能會超出數組範圍致使crash JPViewController *ctrl = [[JPViewController alloc] initWithContent:content]; [self.navigationController pushViewController:ctrl]; } ... @end
能夠經過下發這樣一段 JS 代碼,覆蓋掉原方法,修復這個 bug:git
//JS
defineClass("JPTableViewController", {
//instance method definitions
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var row = indexPath.row()
if (self.dataSource().length > row) { //加上判斷越界的邏輯
var content = self.dataArr()[row];
var ctrl = JPViewController.alloc().initWithContent(content);
self.navigationController().pushViewController(ctrl);
}
}
pod 'JSPatchPlatform'
pod install
便可。
JSPatchPlatform.framework
拖入項目中,勾選 "Copy items if needed",並確保 "Add to target" 勾選了相應的 target。
libz.dylib
和
JavaScriptCore.framework。
AppDelegate.m
裏載入文件,並調用
+startWithAppKey:
方法,參數爲第一步得到的 AppKey。接着調用
+sync
方法檢查更新。例子:
decompress error, md5 didn't match
錯誤(真機不管是否打開都沒問題):
-application:didFinishLaunchingWithOptions:
開頭處調用。
+startWithAppKey:
並不會詢問後臺 patch 更新,必須調用
+sync
方法。
+sync
就會請求一次後臺,對於實時性要求不高的 APP,只需在
-application:didFinishLaunchingWithOptions:
處調用一次,這樣用戶會在啓動時去同步 patch 信息。對於實時性要求高的 APP,能夠在
-applicationDidBecomeActive:
處調用這個接口,這樣會在每次用戶喚醒 APP 時去同步一次後臺,請求次數會增多,但有 patch 更新時用戶會及時收到。
NSLog()
打出,若你的 APP 有本身的日誌系統,但願把 log 打在你的日誌系統裏,能夠在調用 +startWithAppKey 以前調用這個接口:
[JSPatch setLogger:^(NSString *msg) {
//msg 是 JSPatch log 字符串,用你自定義的logger打出
YOUR_APP_LOG(@"%@", msg);
}];
+startWithAppKey:
方法,測試完成後須要刪除。
typedef NS_ENUM(NSInteger, JPCallbackType){ JPCallbackTypeUnknow = 0, JPCallbackTypeRunScript = 1, //執行腳本 JPCallbackTypeUpdate = 2, //腳本有更新 JPCallbackTypeUpdateDone = 3, //已拉取新腳本 JPCallbackTypeCondition = 4, //條件下發 JPCallbackTypeGray = 5, //灰度下發 };
舉例:github
[JSPatch setupCallback:^(JPCallbackType type, NSDictionary *data, NSError *error) {
switch (type) {
case JPCallbackTypeUpdate: {
NSLog(@"updated %@ %@", data, error);
break;
}
case JPCallbackTypeRunScript: {
NSLog(@"run script %@ %@", data, error);
break;
}
default:
break;
}
}];
+sync:
以前調用,用於條件下發,例如:
[JSPatch setupUserData:@{ @"userId": @"100867", @"location": @"guangdong" }];
+sync:
以前調用,詳見
自定義 RSA 密鑰。
+setupDevelopment
的客戶端生效。
DEBUG
時設置,詳見
開發預覽。
首先項目必須接入 JSPatch SDK,並關聯 AppKey,線上版本必須帶有這個 SDK。web
假設已接入 JSPatch SDK 的某線上 APP 發現一處代碼有 bug 致使 crash:編程
上述代碼中取數組元素處可能會超出數組範圍致使 crash,對此咱們寫了以下 JS 腳本準備替換上述方法修復這個 bug:數組
注意在 JSPatch 平臺的規範裏,JS腳本的文件名必須是 main.js
。接下來就看如何把這個 JS 腳本下發給全部用戶。瀏覽器
在上線以前須要對腳本進行本地測試,看看運行是否正常。SDK 提供了方法 +testScriptInBundle
用於發佈前的測試:七牛雲存儲
調用這個方法後,JSPatch 會在當前項目的 bundle 裏尋找 main.js 文件執行,效果與最終線上用戶下載腳本執行同樣,測試完後就能夠準備上線這個腳本。緩存
注意 +testScriptInBundle
不能與 +startWithAppKey:
一塊兒調用,+testScriptInBundle
只用於本地測試,測試完畢後須要去除。安全
進入 JSPatch 平臺後臺,在個人 APP 裏選擇這個 APP,點擊添加版本。填入當前線上 APP 的版本號,能夠在項目 TARGETS -> General -> version 上能夠找到:
注意這裏版本號必須一致,JSPatch 平臺會只針對這個版本號下發對應的 JS 腳本,若版本號對應不上,客戶端也就請求不到相應的 JS 腳本。
點擊進入剛添加的版本,上傳 main.js
便可。
上傳能夠直接全量下發,也能夠選擇 開發預覽 或 灰度或條件下發,也可使用自定義 RSA key 對腳本進行加密簽名。
上傳完成後,對應版本的 APP 會請求下載這個腳本保存在本地,之後每次啓動都會執行這個腳本。至此線上 bug 修復完成。
若後續須要對這個腳本進行修改,能夠從新上傳新的腳本,APP 客戶端會在請求時發現腳本已更新,下載最新腳本覆蓋原來的,下次啓動時執行。
JPDispatch: 提供完整GCD接口 JPLocker: 提供@synchronized接口 JPNumber: 包裝 NSNumber JPProtocol: 提供@protocol接口 JPSpecialInit: 特殊類 UIWebview 和 NSCalendar 的初始化
pod 'JSPatchPlatform' pod 'JSPatchPlatform/Extensions' pod 'JSPatchPlatform/JPCFunction'
pod install
即完成接入。
服務端:
客戶端:
openssl
,再執行如下三句命令,生成 PKCS8 格式的 RSA 公私鑰,執行過程當中提示輸入密碼,密碼爲空(直接回車)就行。
openssl > genrsa -out rsa_private_key.pem 1024 pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM –nocrypt rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
rsa_private_key.pem
和
rsa_public_key.pem
這兩個文件。這裏生成了長度爲 1024 的私鑰,長度可選 1024 / 2048 / 3072 / 4096 ...。
+setupRSAPublicKey:
設置自定義的 RSA Public Key,注意應該在
+sync
以前調用,由於
+sync
可能會下載到腳本,這時已經要用 RSA key 去驗證了。
\n
,例:
//rsa_public_key.pem -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApgeqKYKPVFk1dk2JGrKv EaSqqXxU2S1x32xn2M2jWK/lz7YOPRFcPhH8UgBgpUQGqbW2ooOrtlE0Ur6WHOgZ HvozA71xKEgpQhLbX8ourcyC638zfEQJ3aUezjy5ADzlIAWr3ayBYmLBYj4OkRRG bffxwA+i16jNVFWJFzgCrRs44cpn+nX0VsNrNjntt59J3xIhMGE+eQ2K9WDwYmv4 sw8+3MsW++z2Uornmi9v2atZnBKd/dBsGz05d++NBks7b2ot/TAiMRnit+VNTZrs 1rYQOcoCJlMUK4GDkK6bdKAPfVcD5vy2PAxDA84P2txcSkFozmZABcVvSyASB6Bn MQIDAQAB -----END PUBLIC KEY-----
[JSPatch setupRSAPublicKey:@"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApgeqKYKPVFk1dk2JGrKv\nEaSqqXxU2S1x32xn2M2jWK/lz7YOPRFcPhH8UgBgpUQGqbW2ooOrtlE0Ur6WHOgZ\nHvozA71xKEgpQhLbX8ourcyC638zfEQJ3aUezjy5ADzlIAWr3ayBYmLBYj4OkRRG\nbffxwA+i16jNVFWJFzgCrRs44cpn+nX0VsNrNjntt59J3xIhMGE+eQ2K9WDwYmv4\nsw8+3MsW++z2Uornmi9v2atZnBKd/dBsGz05d++NBks7b2ot/TAiMRnit+VNTZrs\n1rYQOcoCJlMUK4GDkK6bdKAPfVcD5vy2PAxDA84P2txcSkFozmZABcVvSyASB6Bn\nMQIDAQAB\n-----END PUBLIC KEY-----"];
下發腳本時在發佈腳本界面勾選 使用自定義RSA Key
選項,會出現文件上傳框,選擇本地的 rsa_private_key.pem
文件,與腳本一同上傳,JSPatch 平臺會使用這個上傳的 Private Key 對腳本 MD5 值進行加密,再下發給客戶端。若客戶端通過上述第二步設置了對應的 Public Key,就會用設置的 Public Key 對腳本進行驗證,驗證經過後運行腳本,不然不會運行。
rsa_private_key.pem
只是一次性使用,不會保存在服務端,因此只有經過用戶本身保存的
rsa_private_key.pem
文件才能夠針對 APP 下發腳本,即便 JSPatch 平臺或者七牛雲被黑,第三方也沒法對你的 APP 下發惡意腳本(能夠下發,但驗證不過,不會執行),保證安全性。
rsa_private_key.pem
請妥善保管,避免泄露。
+sync
以前調用
setupDevelopment
方法,建議只在 debug 模式下開啓:
[JSPatch startAppWithKey:@""]; #ifdef DEBUG [JSPatch setupDevelopment]; #endif [JSPatch sync];
開發預覽
,就能夠在 debug 模式下測試這個補丁。測試完成後能夠選擇全量下發或灰度/條件下發,下發給現網用戶。
SDK 1.2
版本開始支持腳本的灰度與條件下發。
userId==10000876
,
iOS>9.0&&isMale==1
。
條件語句裏用到的 key/value 須要事先在 APP 裏經過 +setupUserData:
設置,支持設置多個字段,用 NSDictionary 表示,例如能夠設置當前登陸的用戶ID以及性別:
//_userId = @"1000876" //_isMale = @(1) [JSPatch setupUserData:@{@"userId": _userId, @"isMale": _isMale}];
這樣在下發腳本時填入條件 userId==1000876
後,這個腳本就只對這個用戶生效,若是填入 isMale==0
則對這個用戶不生效,對其餘在 SDK 設置了 @"isMale": @"0"
的用戶生效。
&&
||
==
!=
>=
<=
>
<
,意思跟程序裏同樣。>=
<=
>
<
會把值轉爲數值進行對比。例如 userId>200000
,即便客戶端調用 +setupUserData:
接口時設置的 userId 字段是字符串,也會轉爲數值進行對比。==
!=
符號時,會以字符串形式判斷是否相等,例如 1.0 == 1
結果是 NO。location!=guangdong
userId!=31242&&location==guangdong&&name==bang
&&
和 ||
,&&
的優先級較高。例如 userId<200000||location==guangdong&&name==bang
,會先分別計算 userId<200000
和 location==guangdong&&name==bang
的結果,再進行 ||
運算。在發佈腳本1時設條件爲 userId==1000876
,某設備A設置了 @{@"userId": @"1000876"}
命中了這個條件,執行了這個腳本1。設備B設置了 @{@"userId": @"2000876"}
沒有命中。
接着在後臺修改條件爲 userId>=2000000
,設備A並不符合這個條件,但由於以前的條件命中過,因此設備A不會再受這個改變影響,繼續執行腳本1。設備B命中了這個條件,也執行了腳本1。
此外若想撤銷條件全量發佈,提交空條件便可。
iOS
和
isPad
,分別表示 iOS 版本號和是否iPad,不須要設置就能夠拿這兩個字段用於條件判斷。
例如只針對 iOS8 的 iPad 下發,能夠直接寫這個條件:iOS>=8.0&&iOS<9.0&&iPad==1
。
注意 iOS 版本號只會精確到兩位,例如 9.2.1 會記錄成 9.2,iOS==9.2
會命中 9.2.x 版本。
+setupUserData:
接口要在 +sync:
接口以前調用。SDK 1.1
及如下版本會無視任何條件和灰度值,直接全量接收。參數名:name
參數值:bang
。
+application:didFinishLaunchingWithOptions:
裏調用
+updateConfigWithAppKey:
方法,傳入
appKey
,APP 就會在調用處發請求獲取剛纔設置的在線參數。
+getConfigParams
拿到全部參數,也能夠經過
+getConfigParam:
接口拿到單個參數,例如:
NSDictionay *configs = [JSPatch getConfigParams]; //configs == @{@"name": @"bang"} NSString *name = [JSPatch getConfigParam:@"name"]; //name == bang
+updateConfigWithAppKey:
的請求返回時進行一些操做,能夠經過
+ setupUpdatedConfigCallback:
接口設置 callback:
[JSPatch setupUpdatedConfigCallback:^(NSDictionary *configs, NSError *error) { NSLog(@"%@ %@", configs, error); }];
爲了不重複請求浪費資源,默認 +updateConfigWithAppKey:
接口請求時間間隔至少爲30分鐘,也就是30分鐘內屢次調用 +updateConfigWithAppKey:
只會請求一次。若想 APP 對在線參數響應更實時,能夠經過 +setupConfigInterval:
接口修改這個間隔值。
+updateConfigWithAppKey:
方法算一次請求。[JSPatch sync]
時從新拉取,重試 3 次失敗後,會上報失敗數據,在實時監控這裏也能夠看到每一條失敗數據以及對應的錯誤碼,方便排查問題。
POST http://jspatch.com/Apps/uploadPatch @params email 登陸郵箱 @params password 登陸密碼 @params appKey APP惟一鍵值 @params appVersion APP版本號 @params gray (可選)灰度策略,值爲1-9,表明10%-90% @params condition (可選)條件下發 @params patch[] 補丁文件 @params rsaKey (可選)rsa private密鑰文件 //失敗返回 @return {errMsg: ''} //成功返回 @return {succ: 1, patchVersion: {$patchVersion}}
curl -F 'email=test@qq.com' -F 'password=test1234' -F 'appKey=2ba21d234fa69915' -F 'appVersion=2.0' -F 'gray=4' -F 'patch[]=@main.js' http://jspatch.com/Apps/uploadPatch
POST http://jspatch.com/Apps/updatePatch @params email 登陸郵箱 @params password 登陸密碼 @params appKey APP惟一鍵值 @params appVersion APP版本號 @params gray (可選)修改灰度策略,值爲1-9,表明10%-90% @params condition (可選)修改條件下發規則 @params all (可選)修改成全量下發 //失敗返回 @return {errMsg: ''} //成功返回 @return { succ: 1, patch: { patchID: 5804, gray: 3, condition:null, isDev:0 } }
curl -F 'email=test@qq.com' -F 'password=test1234' -F 'appKey=2ba21d234fa69915' -F 'appVersion=2.0' -F 'condition=userId=21' http://jspatch.com/Apps/updatePatch
NSLog('xx')
,應該用 console.log('xx')
self.navigationItem()
,而不是 self.navigationItem
self.valueForKey()
和 self.setValue_forKey()
接口存取。self
+testScriptInBundle
接口執行腳本看有沒有問題,(詳情參照
使用範例),若沒達到預期效果,能夠一步步調試,第一步請在
main.js
開頭打
console.log('run success')
,肯定 XCode 控制檯有輸出這條 log,肯定腳本有被執行到,再進行其餘調試。通常調試使用
console.log()
就足夠,如有更多需求能夠用
Safari斷點調試。
appKey
和
版本號
沒有錯誤。
2016-04-27 19:04:42.212 ... JSPatch: runScript 2016-04-27 19:04:42.399 ... JSPatch: evaluated script, length: 28
2016-04-27 19:04:42.399 ... JSPatch: request http://7xkfnf.com1.z0.glb.clouddn.com/6d2fddf24c5d8af2/1.0?v=1461755082.399732 2016-04-27 19:04:42.621 ... JSPatch: request success { v = 2; }
這兩句表示請求到了當前版本補丁版本號,這裏 url 裏的 6d2fddf24c5d8af2
是 appkey
,後續跟的 1.0
是 App 版本號,能夠檢查下這兩個值是否正確。若 url 不正確或者腳本沒有正確上傳,這裏會返回 error = "Document not found"
。
--
2016-04-27 19:09:43.798 ... JSPatch: updateToVersion: 2 2016-04-27 19:09:43.798 ... JSPatch: request file http://7xkfnf.com1.z0.glb.clouddn.com/6d2fddf24c5d8af2/1.0/file2 2016-04-27 19:09:43.900 ... JSPatch: request file success, data length:3072 2016-04-27 19:09:43.908 ... JSPatch: updateToVersion: 2 success
這幾句表示檢測到的補丁版本號比本地版本更新,去下載補丁文件,下載後會當即執行,到這一步應該就沒問題了。若這個版本的補丁以前已經下載過,就不會再下載。
新建 APP 版本 -> 上傳補丁 -> 刪除APP版本 -> 新建同一個APP版本 -> 上傳補丁
,會中緩存邏輯,致使請求到的腳本是刪除 APP 以前上傳的舊補丁。這時只須要再上傳一次補丁更新版本號就能夠了。
decompress error, md5 didn't match
錯誤(真機不管是否打開都沒問題):