淺析JSPatch的使用
一. 背景介紹
背景:iOS作為蘋果獨家開發和運營的生態圈,具有非常封閉的運作環境,其App上線需要通過提交,排隊,審核,上線這四個很長的過程,過往的排隊時間長至于7~15天,對一個軟件迭代就有了極大的限制,如果產品中出現了小bug, 需要修復問題的話就是及其困難的了,雖然目前蘋果的審核時間已經短至1天,但是其App更新方式為全量更新,如若為了修復一個極小的問題而強迫用戶更新一個版本的話,體驗也是不好的, 所以在多年的技術進化中,產生了App熱更新這樣的一個需求。
發展: 在這幾年來的iOS開發界,各個廠商和各種極客各現神通,通過各種黑科技來實現各種熱更新技術的實現,在iOS7之前,著名的有Wax框架,Wax框架主要使用Lua語言進行腳本實現,并且需要在原生的App中植入Wax的腳本引擎,使用上也不是太方便,所以逐漸被淘汰。2013年,蘋果在iOS7中引入了原生框架JavascriptCore這樣一個原生框架,徹底打開了JS腳本和native調用之間的橋梁,也為熱更新技術的實現提供了原生的技術支持,各種極客和軟件開發商都定制自己的腳本來調用本地的部分代碼,但是都沒有統一的方案,在這一個沉淀的過程中,產生了較為成熟易用的JSPatch框架。
二. JSPatch介紹
- 誕生:JSPatch誕生于2015年5月,最初出自于騰訊廣研院的高級iOS開發工程師@bang的個人項目,其超級深厚的開發基礎和過人的天賦在這個項目中表現的淋漓盡致,其基本原理為使用javascriptcore中提供的調用Objective-C的原生入口,并充分利用Objective-C的動態特點來在運行時修改方法的入口和實現,用于替換原有的舊方法。 目前JSPatch在Github上已經開源,擁有大量的擁簇,并且充分在騰訊,阿里,百度各個大廠的產品中得到了驗證,騰訊甚至提供了JSPatch托管SDK平臺用于商用,可見其實現已經成熟到了商用的地步,可以放心使用了。以下為其他開發者總結的JSPatch和Wax的區別:
基本原理
- Objective-C是動態語言,具備運行時特性,所以能夠在運行時通過類名稱或者方法的名稱來獲取執行入口,并且進行實際調用,而且還可以通過runtime特性來swizzle各種方法,進行動態修改。
- 通過類和方法名稱進行運行時調用的片段:
Class class = NSClassFromString(@"UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString(@"viewDidLoad");
[viewController performSelector:selector];
動態替換類方法為新方法的片段:
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
正是由于OC語言具有如上片段展示的runtime時決定調用方法的特性,成就了熱更新的基礎原理。
具體的實現原理比較復雜,作者已經開源了代碼,并且在博客中寫明了實現過程中的各種問題,如果有興趣的可以參閱以下鏈接:
JSPatch在github的源地址,
JSPatch詳細實現原理
三. 使用方法
- 基本語法解析
下面展示一下OC代碼和熱更新的JS腳本的對應代碼片段,例如線上 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:
//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);
}
}
}, {})
除了修復一些bug之外,也可以動態的修改一些App的行為, 詳細越發可以查閱
JSPatch語法WIKI
- SDK接入使用
前面已經提到,騰訊已經將JSPatch進行了商用包裝,使用方式非常簡單,進入JSPatch的官方站點,下載SDK,將SDK拖入工程,然后添加官方javascriptcore.framework就可以按照文檔進行使用了,騰訊官方還進行了后端平臺托管,使用者在后端注冊App獲取Key,然后在App端初始化即可使用,需要熱更新的腳本也配置在后端進行下發,使用非常方便簡潔,該平臺提供每天1W次下發的免費量,超過1W次需要收費服務,詳細使用方式可以查閱:
JSPatch官方網站以及介紹
- 源代碼引入使用
首先構建一個簡單的demo, demo潔面只有一個按鈕,按鈕下面一個Label框,demo的簡單邏輯是,點擊按鈕之后,Label框背景顏色變為藍色,并且顯示native code字樣。
原始代碼片段如下:
初始運行界面為:
點擊changeColor按鈕之后,事件響應并執行,運行界面變為:
以上為native代碼的原始邏輯,下面我們開始進行動態替換:
- 思路
我們要動態替換changeColor的點擊事件,其實就是需要動態替換clickChangeColorEvent這個函數,大家可以去按照上面的JSPatch的語法wiki中去查閱詳細語法,我們這里有更簡單直接的方法,JSPatch的作者為了使用者方便,開發了
JSPatchConverter這個工具,大家可以去詳細看一下使用,也可以直接下載成品App, 我們這里使用成品App進行JS代碼生成,只需要在App的左側寫入要替換的方法,點擊convert按鈕,右邊窗口即可生成熱更新的腳本代碼,如圖:
將右邊窗口的成品JS片段自己保留出來,然后放在自建的服務器準備下發使用。
- 本地工程準備
從github中下載JSPatch的源碼,并在源碼工程中找到JSPatch文件夾,將該文件夾拖入demo工程,如圖:
同時添加蘋果官方的javascriptcore.framework,如圖:
- 代碼實現
首先要在AppDelegate.h中添加對應的頭文件:
#import "JSPatch/JPEngine.h"
繼而在iOS的App啟動入口函數中添加JSEngine的初始化:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[JPEngine startEngine];
return YES;
}
以上兩步已經完成了JSEngine的簡單的初始化過程,然后進入下一步,獲取遠端腳本的過程。 將前面在JSConverter中生成的腳本代碼自己放入自建服務器中(這一步自己尋找后端兄弟協助完成), 然后在實現一個獲取腳本的函數,進行腳本獲取,并將執行獲取腳本的函數放在[JPEngine startEngine]之后執行,然后在獲取到腳本之后,執行腳本行為,以下為完整范例:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[JPEngine startEngine];
[self getAndRunJSPatchScript];
return YES;
}
- (void)getAndRunJSPatchScript
{
NSURLSession *urlSession = [NSURLSession sharedSession];
NSMutableURLRequest *httpRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxxxx/api"]];
[httpRequest setHTTPMethod:@"POST"];
NSURLSessionDataTask *dataTask = [urlSession dataTaskWithRequest:httpRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if(!error)
{
NSString *targetJSPatchScript = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[JPEngine evaluateScript:targetJSPatchScript];
}
}];
[dataTask resume];
}
然后運行新的程序,點擊changeColor按鈕,行為已經發生了改變:
可以看到,圖中的色塊已經變成了黃色,圖中的提示語言也顯示JSPatch代碼進行了執行,而這一個過程當中,native的邏輯代碼并沒有改變。
- 歸納
上面代碼片段中的getAndRunJSPatchScript函數只是簡單的調用了接口,獲取了腳本,然后執行。更長遠的規劃,服務端完全可以實現一個腳本管理服務,提供一系列結構,將不同App版本,不同運營行為的腳本進行分類管理,App只需要上報自己的App基本信息,然后由服務端來返回要熱更新的腳本,更加的靈活和體系化。
總結
上述內容可以看到JSPatch確實是一個很方便的熱更新庫,但是個人認為在App的開發過程中,大家還是應該把質量管控在開發階段,不可依賴于這樣的熱更新修復,這樣的修復可以用于應急來修補漏網的bug, 但是不建議作為App的主要邏輯開發層,因為這樣的會帶來更多的外部管理App的功能,從某種意義上來說也添加了運營的復雜性, 而且在蘋果主推的未來開發語言swift中,這種OC語言所具有的動態性將會不再存在,以這套原理來運作的JSPatch庫也就無從使用了,但是在當下的實際OC仍然是很多App的主開發語言的時代,這個熱更新修復的庫還是非常非常實用的,至于各位開發者將會對這個庫依賴多深,這就是自己定奪的問題了。