最近接觸到熱修復(fù), 確實(shí)能解燃眉之急, 非常好用, 故分享給大家. 這里只講 JSPatch, 這個(gè)是現(xiàn)在最熱門最好用的框架, 用起來超級(jí)簡(jiǎn)單, 非常感謝 bang590 的貢獻(xiàn).
JSPatch 是一個(gè)開源項(xiàng)目, 只需要在項(xiàng)目里引入極小的引擎文件, 就可以使用 JavaScript 調(diào)用任何 Objective-C 的原生接口, 替換任意 Objective-C 原生方法. 目前主要用于下發(fā) JS 腳本替換原生 Objective-C 代碼, 實(shí)時(shí)修復(fù)線上 bug.
項(xiàng)目集成
[Github][1] 下載后, 按照[操作文檔][2]操作就可以輕松集成, 摘錄 bang590 Github 簡(jiǎn)要步驟如下:
[1]:https://github.com/bang590/JSPatch
[2]:https://github.com/bang590/JSPatch/blob/master/README-CN.md
- 拷貝
JSPatch/目錄下的三個(gè)文件JSEngine.m/JSEngine.h/JSPatch.js到項(xiàng)目里 #import "JPEngine.h"- 調(diào)用
[JPEngine startEngine] - 通過
[JPEngine evaluateScript:@""]接口執(zhí)行 JavaScript。 - 直接把下面代碼拷貝到
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中即可, app 運(yùn)行后只調(diào)用一次, 即每次運(yùn)行 app 只更新一次 JS 修復(fù) - 如若要更新即時(shí)性, 可以把方法放到
- (void)applicationWillEnterForeground:(UIApplication *)application這樣每次 app 從后臺(tái)進(jìn)入前臺(tái), 都會(huì)拉取 JS 修復(fù)文件
// 方法一: 從網(wǎng)絡(luò)拉回js腳本執(zhí)行
[JPEngine startEngine];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:kDownloadPath]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[JPEngine evaluateScript:script];
}];
// 上面代碼 kDownloadPath 換成你自己的 JS 文件地址即可
// 每次都從網(wǎng)絡(luò)拉取, 雖然文件小, 但也受限也網(wǎng)絡(luò)狀態(tài), 不太理想.
// 方法二: 先下載到本地, 再?gòu)谋镜匚募A中讀取
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:kDownloadPath] completionHandler:^(NSURL * _Nullable location,NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"location: %@", location);
// 下載任務(wù)會(huì)把下載的資源存放到臨時(shí)文件夾tmp下. block結(jié)束后, 就會(huì)自動(dòng)刪除.
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *path = [docPath stringByAppendingPathComponent:@"demo.js"];
NSLog(@"path: %@", path);// 拷貝路徑在 Finder ->前往 ->前往文件夾 可看到已下載文件
// 測(cè)試了會(huì)有緩存, 且不能把原有的 JS 文件覆蓋, 故要先移除
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
// 故把下載數(shù)據(jù)移動(dòng)到document下
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:path]error:nil];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[JPEngine startEngine];
[JPEngine evaluateScriptWithPath:path];
}];
}];
[task resume];
// 上面代碼 kDownloadPath 換成你自己的 JS 文件地址即可
實(shí)際不需要每次都拉取, 該方法也只是暫緩措施, 下次迭代版本必須把上次 JS 修復(fù)的用原生解決, 這時(shí)需要有一個(gè)后臺(tái)可以下發(fā) JS 下載路徑和管理腳本, 并且需要處理傳輸安全等部署工作.
JS 文件
JS 文件創(chuàng)建
-
使用 xcode 創(chuàng)建 JS 文件
xcode 創(chuàng)建 JS 文件.png 使用 Sublime Text 工具創(chuàng)建 JS 文件, 同樣后綴保存為 .js 即可
JS 語(yǔ)法
- 在 defineClass 里定義 OC 已存在的方法即可覆蓋, 語(yǔ)法如下:
defineClass(classDeclaration, [properties,] instanceMethods, classMethods)
@param classDeclaration: 字符串,類名/父類名和Protocol
@param properties: 新增property,字符串?dāng)?shù)組,可省略
@param instanceMethods: 要添加或覆蓋的實(shí)例方法
@param classMethods: 要添加或覆蓋的類方法
// 例如:
require('UIDevice');
defineClass("ViewController", {
viewDidLoad: function() {
var model = UIDevice.currentDevice().model();
console.log(model);
if (UIDevice.currentDevice().systemVersion().floatValue() >= 9) {
console.log("9.0版本");
} else {
console.log("其他版本");
}
console.log("js 打印, 腳本號(hào): 1.0, 替換實(shí)例成功");
}
}, {
test: function() {
console.log("js 打印, 腳本號(hào): 1.0, 替換類方法成功");
}
});
要替換多個(gè)方法, 都要重新寫
defineClass("類名", [新增屬性,], {實(shí)例方法}, {類方法}), 屬性可以省略.只有類方法或者實(shí)例方法, 就留空大括號(hào) {}, 如只需修改類方法:
defineClass("類名", {}, {類方法}).在方法名前加 ORIG 即可調(diào)用未覆蓋前的 OC 原方法:
viewDidLoad: function() { self.ORIGviewDidLoad(); },
- 在 JS 里面判斷是否為空要判斷 false
- ```java
var url = "";
var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
if (rawData != null) {} //這樣判斷是錯(cuò)誤的
應(yīng)該如下判斷:
if (!rawData){}
在JSPatch.js源碼里_formatOCToJS方法對(duì)undefined,null,isNil轉(zhuǎn)換成了false。
- Objective-C 里的常量/枚舉/宏/全局變量不能直接在 JS 上使用
- 更多語(yǔ)法見 JSPatch 基礎(chǔ)語(yǔ)法, 也可以借助 JSPatch 代碼轉(zhuǎn)換器, 當(dāng)然轉(zhuǎn)換器不是萬能了, 還需要自己細(xì)心檢查.
- JSPatch 替換的是整個(gè)方法, 哪怕只有一行代碼需要修復(fù), 整個(gè)方法都需要重寫成 JS 代碼. 倡導(dǎo)使用敏捷開發(fā)的思想, 類似于主邏輯或者是功能模塊入口的方法可以抽的更細(xì), 這樣即使需要修改, 成本也不會(huì)太大.
版本管理
公司搭建后臺(tái)
自己公司搭建后臺(tái), 除了下發(fā)拉取 JS 的地址外, 還可以加入一些參數(shù), 比如: 版本控制, 指定修復(fù)某 iOS 版本等等, 條件根據(jù)需求定, 跟一般請(qǐng)求無異, 就不敘述了.
七牛云平臺(tái)
JS 文件也可以存放到七牛云上, 七牛云同樣提供版本控制, 這樣自己公司后臺(tái)省很多事, 只需寫一個(gè)接口, 而且有一定的免費(fèi)額度, 足夠用了.

七牛云使用流程
- 注冊(cè)完七牛云賬號(hào)后, 點(diǎn)擊添加對(duì)象存儲(chǔ)創(chuàng)建儲(chǔ)存空間, 訪問控制注意選公開空間, 這樣外界才能訪問到 JS 文件.



- 上傳文件后, 復(fù)制外鏈接就是 JS 文件路徑

- 需要注意的是七牛云平臺(tái)文件是有緩存的, 所以在上傳 JS 文件的命名不要和前面重復(fù), 不然下發(fā)后看到結(jié)果會(huì)是上一次同名文件效果, 緩存時(shí)間可以在空間設(shè)置里設(shè)置

JSPatch 平臺(tái)
不想搭建后臺(tái), 可以使用 JSPatch 平臺(tái), 也不用把 JS 文件上傳到七牛云, 直接上傳到 JSPatch 平臺(tái)即可, 功能很多, 還提供條件下發(fā), 平臺(tái)文檔介紹已經(jīng)非常詳細(xì)了, 這里就不再贅述了.
不過平臺(tái)是需要收費(fèi)的

!!!使用 JSPatch 平臺(tái)注意點(diǎn)
- 注意在 JSPatch 平臺(tái)的規(guī)范里,JS 腳本的文件名必須是
main.js。 - 自定義 RSA 密鑰, 按照提示在終端輸入命令后, 生成的文件在主目錄下:

按照文檔那樣導(dǎo)入
public_key太麻煩了, 而且容易出錯(cuò), 可以把rsa_public_key.pem文件拖入工程中, 再執(zhí)行下面代碼就可以:
NSString *keyPath = [[NSBundle mainBundle] pathForResource:@"rsa_public_key" ofType:@"pem"];
NSString *publicKey = [NSString stringWithContentsOfFile:keyPath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"publicKey: %@", publicKey);
[JSPatch setupRSAPublicKey:publicKey];
//下方是 JSPatch 啟動(dòng)代碼
[JSPatch startWithAppKey:@"19ed6339k440fa3ab"];
ifdef DEBUG
[JSPatch setupDevelopment];
endif
[JSPatch sync];
######集成錯(cuò)誤錄
- 若使用 XCode8 接入,需要在項(xiàng)目 Capabilities 打開 Keychain Sharing 開關(guān),否則在模擬器下載腳本后會(huì)出現(xiàn) `decompress error, md5 didn't match` 錯(cuò)誤(真機(jī)無論是否打開都沒問題)
- pod JSPatch 平臺(tái) SDK 完成并添加依賴庫(kù), 啟動(dòng) `startWithAppKey` 和 `sync` 后報(bào)錯(cuò)
- ```objc
duplicate symbol _OBJC_METACLASS_$_JPEngine in:
/Users/issuser/Library/Developer/Xcode/DerivedData/ViewController-ajurqnqqgeaehfajwnvxgpblrcmz/Build/Intermediates/ViewController.build/Debug-iphonesimulator/ViewController.build/Objects-normal/x86_64/JPEngine.o
/Users/issuser/Library/Developer/Xcode/DerivedData/ViewController-ajurqnqqgeaehfajwnvxgpblrcmz/Build/Products/Debug-iphonesimulator/JSPatch/libJSPatch.a(JPEngine.o)
ld: 11 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
//原因是工程里有手動(dòng)導(dǎo)入 JSPatch.h JSPatch.m 和 JSPatch.js 文件, 和 cocoapods 沖突了
小結(jié)
JSPatch 熱修復(fù)集成簡(jiǎn)單吧, 難點(diǎn)在 JS 語(yǔ)法上, 沒有語(yǔ)法提示, 寫的時(shí)候更要細(xì)心.
如果沒有效果的話, 檢查 JS 語(yǔ)法是否正確, 也可以通過 Safari 的調(diào)試工具對(duì) JS 進(jìn)行斷點(diǎn)調(diào)試, 詳見 JS 斷點(diǎn)調(diào)試, 還有是否執(zhí)行之前緩存的文件.
