JSPatch熱修復入門指南和js斷點調試

背景

是否有過這樣的經歷:新版本上線后發現有個嚴重的bug,可能會導致crash率激增,可能會使網絡請求無法發出,這時能做的只是趕緊修復bug然后提交等待漫長的AppStore審核,再盼望用戶快點升級,付出巨大的人力和時間成本,才能完成此次bug的修復。

一開始我以為JSPatch是React-Native中抽取出來的,后來發現和React-Native完全沒關系。這個項目和JSPatch平臺,oc轉換工具等是原作者一個人寫的,大神就是大神。

使用JSPatch可以解決這樣的問題,只需在項目中引入JSPatch,就可以在發現bug時下發JS腳本補丁,替換原生方法,無需更新APP即時修復bug。

例子

首先拷貝 JSPatch/目錄下的三個文件 JSEngine.m/ JSEngine.h/ JSPatch.js到項目里即可。demo下載地址:JSPatchDemo

//
//  ViewController.m
//  TestJSPatch
//
//  Created by wuwei on 16/7/20.
//  Copyright ? 2016年 wuwei. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()<UITableViewDataSource,UITableViewDelegate>
@property(nonatomic,strong)UITableView* mytableView;
@property(nonatomic,strong)NSArray *dataSource;

@end

@implementation ViewController

- (void)viewDidLoad
{
    
    UITableView* tv = [[UITableView alloc]initWithFrame:self.view.bounds
                                                  style:UITableViewStylePlain];
    self.view.backgroundColor = [UIColor clearColor];
    self.mytableView = tv;
    self.mytableView.delegate = self;
    self.mytableView.dataSource = self;
    [self.view addSubview:self.mytableView];
    self.dataSource = @[@"aaaa",@"bbbbb"];
}

#pragma mark -- UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 3;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString* i=  @"cell";
    UITableViewCell* cell = [tableView  dequeueReusableCellWithIdentifier:i];
    if (cell == nil ) {
        cell =[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault
                                    reuseIdentifier:i];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"第%li行",(long)indexPath.row];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // cell有三行,當我點擊第三行的時候肯定會數組越界導致 crash
    //  還好我在JS里彌補了這個bug,詳情請看JS里的處理
    NSString *content = self.dataSource[indexPath.row];
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:content delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
    [alert show];
}

@end
//demo.js
//用 defineClass()
// 定義 Objective-C 的類,對類和實例方法進行動態替換。
defineClass("ViewController", {  
            //instance method definitions
            tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
            var row = indexPath.row()
            if (self.dataSource().count() > row) {  // 加上判斷越界的邏輯
                var alertView = require('UIAlertView').alloc().init();
                alertView.setTitle('Alert');
                alertView.setMessage('數組下標沒有越界');
                alertView.addButtonWithTitle('OK');
                alertView.show();
            }else{
                 var alertView = require('UIAlertView').alloc().init();
                 alertView.setTitle('Alert');
                 alertView.setMessage('數組下標越界');
                 alertView.addButtonWithTitle('OK');
                 alertView.show();
            }
            }
            }, {});

//AppDelegate use

// 執行本地js文件
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];
    
    return YES;
}

現在點擊第3行時,不會崩潰了。因為didSelectRowAtIndexPath函數被js里面的函數取代了。

當然,真正在項目中應該是從網絡拉取,動態修改

// 從網絡拉回js腳本執行
 [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/test.js"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        [JPEngine evaluateScript:script];
    }];

原理
JSPatch用iOS內置的JavaScriptCore.framework作為JS引擎,但沒有用它JSExport的特性進行JS-OC函數互調,而是通過Objective-C Runtime,從JS傳遞要調用的類名函數名到Objective-C,再使用NSInvocation動態調用對應的OC方法。
有興趣的可以看作者原著:JSPatch 實現原理詳解

demo下載地址:JSPatchDemo

與React Native的異同

JSPatch可以動態打補丁,自由修改APP里的代碼,理論上還可以完全用JSPatch實現一個業務模塊,甚至整個APP,跟wax一樣,但不推薦這么做,若想動態為APP添加模塊,目前React Native給出了很好的方案,解決了上述三個問題:

JS/OC不會頻繁通信,會在事件觸發時批量傳遞,提高效率。(詳見React Native通信機制詳解
開發過程無需考慮OC的感受,遵從React框架的思想進行純JS開發就行,剩下的事情React Native幫你處理好了。
React Native連IDE都準備好了。

工具

JSPatchX (XCode 代碼補全插件)
JSPatch Convertor (OC->JS 代碼自動轉換工具)JSPatch平臺
JSPatch平臺

了解更多請看原著的WIKI 完善的文檔請移步 Wiki

JS 斷點調試

在 iOS8 下,JSPatch 支持使用 Safari 自帶的調試工具對 JS 腳本進行斷點調試:

F7C514EB-81D8-40A1-9865-AE8B3FDFBCCD.png

啟動調試工具

首先需要開啟 Safari 調試菜單:Safari -> 偏好設置 -> 高級 -> 勾選[在菜單欄中顯示“開發”菜單]

接著啟動Safari -> 開發 -> 選擇你的機器 -> JSContext
并且點擊:顯示擴展創建器
即可開始調試。

2DFE40A0-726A-47C4-A7DA-A5F5ABEC12FD.png

連接真機調試時,需要打開真機的web檢查器:設置 -> Safari -> 高級 -> Web檢查器

資源列表

資源列表列出了 JSPatch 所有執行中的腳本文件,點開文件后可以對其進行斷點調試。
通過 [JPEngine evaluateScript:script]
接口執行的腳本,在資源列表里都表示為 main.js

通過 [JPEngine evaluateScriptWithPath:filePath]
接口執行的腳本,在資源列表里會以原文件名表示。

如果你都看到這里了,請給我點個贊吧,你的喜歡是我堅持原創的不竭動力。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容