JSPatch的基本使用及思考

作者: 肥猫记 | 来源:发表于2016-07-30 08:38 被阅读1298次

    介绍

    JSPatch是2015年由bang推出的能实现热修复的工具,只要在项目中引入极小的JSPatch引擎,就可以用 JavaScript 调用和替换任何 Objective-C 的原生方法,获得脚本语言的能力<动态更新 APP、替换项目原生代码修复 bug>。
    作者已将JSPatch商业化,提供了脚本后台托管,版本管理,保证安全传输等功能,只需引入一个SDK(+startWithAppKey:)即可使用.JSPatch Platform

    实现原理

    JSPatch 能做到通过 JS 调用和改写 OC 方法最根本的原因是 Objective-C 是动态语言,OC 上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,通过Apple在iOS7中发布的javeScriptCore.framework来解析JavaScript脚本,与Objective-C的代码进行桥接,利用运行时的特性,让app具备hit code push能力</br>
    具体实现原理作者已经给出,不再累赘描述,请看这里

    使用方法

    具体的使用方法这里有详细介绍,这里说下大概流程</br>
    从github上下者JSPatch文件,将整个JSPatch文件夹拷贝到项目中,导入JPEngine.h,调用[JPEngine startEngine]开启JSPatch引擎,通过[JPEngine evaluateScript:@"..."]接口来执行JavaScript语句.个人认为JSPatch的语法不用刻意去看,在写的过程中卡壳了就去作者的原文中查找即可.

    [JPEngine startEngine];
    
    // 直接执行js
    [JPEngine evaluateScript:@"\
     var alertView = require('UIAlertView').alloc().init();\
     alertView.setTitle('Alert');\
     alertView.setMessage('AlertView from js'); \
     alertView.addButtonWithTitle('OK');\
     alertView.show(); \
    "];
    
    // 从网络拉回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];
    }];
    
    // 执行本地js文件
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];
    

    JaveScript的基础使用方式(摘自bang):

    // 调用require引入要使用的OC类
    require('UIView, UIColor, UISlider, NSIndexPath')
    
    // 调用类方法
    var redColor = UIColor.redColor();
    
    // 调用实例方法
    var view = UIView.alloc().init();
    view.setNeedsLayout();
    
    // set proerty
    view.setBackgroundColor(redColor);
    
    // get property 
    var bgColor = view.backgroundColor();
    
    // 多参数方法名用'_'隔开:
    // OC:NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:1];
    var indexPath = NSIndexPath.indexPathForRow_inSection(0, 1);
    
    // 方法名包含下划线'_',js用双下划线表示
    // OC: [JPObject _privateMethod];
    JPObject.__privateMethod()
    
    // 如果要把 `NSArray` / `NSString` / `NSDictionary` 转为对应的 JS 类型,使用 `.toJS()` 接口.
    var arr = require('NSMutableArray').alloc().init()
    arr.addObject("JS")
    jsArr = arr.toJS()
    console.log(jsArr.push("Patch").join(''))  //output: JSPatch
    
    // 在JS用字典的方式表示 CGRect / CGSize / CGPoint / NSRange
    var view = UIView.alloc().initWithFrame({x:20, y:20, width:100, height:100});
    var x = view.bounds().x;
    
    // block 从 JavaScript 传入 Objective-C 时,需要写上每个参数的类型。
    // OC Method: + (void)request:(void(^)(NSString *content, BOOL success))callback
    require('JPObject').request(block("NSString *, BOOL", function(ctn, succ) {
      if (succ) log(ctn)
    }));
    
    // GCD
    dispatch_after(function(1.0, function(){
      // do something
    }))
    dispatch_async_main(function(){
      // do something
    })
    

    一个使用JaveScript的简单例子:

    //  AppDelegate.m
    
    #import "AppDelegate.h"
    #import "HBJSPatchMainViewController.h"
    #import "JPEngine.h"
    @interface AppDelegate ()
    
    @end
    
    @implementation AppDelegate
    
    - (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];   //从本地读取JS
        [JPEngine evaluateScript:script];   //执行JS语句
        
        self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
        self.window.rootViewController = [[UINavigationController alloc]initWithRootViewController:[[HBJSPatchMainViewController alloc ]init]];
        [self.window makeKeyAndVisible];
        
        return YES;
    }
    @end
    
    
    //  ViewController.m
    
    #import "HBJSPatchMainViewController.h"
    
    @interface HBJSPatchMainViewController ()
    
    @end
    
    @implementation HBJSPatchMainViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    ![Uploading JSPatch_353937.gif . . .]
    
        UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
        button.frame = CGRectMake(0, 0, 200, 50);
        button.center = self.view.center;
        button.backgroundColor = [UIColor orangeColor];
        [button setTitle:@"进入JS生成的UI界面" forState:UIControlStateNormal];
        [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [button addTarget:self action:@selector(buttonTouch:) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
        self.view.backgroundColor = [UIColor blueColor];
        
    }
    
    - (void)buttonTouch:(UIButton *)button {
    
        //    触发的方法在demo.JS中
    }
    @end
    
    
    
    //  HBJSPatchTsetViewController.m
    
    #import "HBJSPatchTsetViewController.h"
    
    @interface HBJSPatchTsetViewController ()
    
    @end
    
    @implementation HBJSPatchTsetViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        UILabel *lable = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 30)];
        lable.center = self.view.center;
        lable.textAlignment = NSTextAlignmentCenter;
        lable.text = @"JS到原生UI";
        [self.view addSubview:lable];
        self.view.backgroundColor = [UIColor whiteColor];
        
    }
    
    @end
    
    //demo.js文件
    
    //按钮事件
    defineClass('HBJSPatchMainViewController', {//寻址到哪个类中的
                buttonTouch: function(button) {//改写类中的方法
                //跳转到tableView
                var tableViewCtrl = HBTableViewController.alloc().init()
                self.navigationController().pushViewController_animated(tableViewCtrl, YES)
                }
    })
    
    defineClass('HBTableViewController : UITableViewController', {  //创建类并实现类中的方法
                dataSource: function() {
                //JSPatch可以通过 -getProp:, -setProp:forKey: 这两个方法给对象动态添加成员变量。
                var data = self.getProp('data')
                if (data) return data;
                var data = [];
                for (var i = 0; i < 20; i ++) {
                data.push("通过JS创建的Cell" + i);
                }
                self.setProp_forKey(data, 'data')
                return data;
                },
                numberOfSectionsInTableView: function(tableView) {
                return 1;
                },
                tableView_numberOfRowsInSection: function(tableView, section) {
                return self.dataSource().count();
                },
                tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
                var cell = tableView.dequeueReusableCellWithIdentifier("cell")
                if (!cell) {
                cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0, "cell")
                }
                cell.textLabel().setText(self.dataSource().objectAtIndex(indexPath.row()))
                return cell
                },
                tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
                return 60
                },
                tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
                
                //跳转到原生UI
                var testViewController = require('HBJSPatchTsetViewController').alloc().init()
                self.navigationController().pushViewController_animated(testViewController, YES)
                },
    })
    
    示例图

    通常我们将脚本的执行放在AppDelegate的didFinishLaunchingWithOptions:方法中,个人建议用一个管理工具类(manager)来管理脚本下载和执行,可以用来设置一个脚本下载的间隔时间和对本地补丁进行检测等等

    更多用法请点这里

    安全问题

    由于JS脚本能随意的调用OC方法,修改OC文件,权限非常大,若被人攻击替换代码,会造成较大的危害.一般我们在JS文件提交到后台后应该对其进行加密传输,这里推荐RSA和HTTPS来保证安全性,后面我会补遍加密的文章,有兴趣的请关注我,这里先推荐这遍文章:JSPatch部署安全策略

    思考

    • 现在苹果appStore的审核周期已经变成一天,使得bug的修复速度变得很快,小公司还有没有必要增加人工成本来实现热修复?
    •    使用JSPatch的重中之重就是安全问题,虽然可以人为的进行加密传输,但总是有风险,而只要一出现问题都会是重大的问题
      
    • 在一个很复杂的方法中,仅中间某一行代码需要修改,就要将整个方法用JS重写一遍,推介作者开发的Objective-C转JavaScript代码工具JSPatch Convertor,但一些复杂的语法还是要人工修正
    • 当使用JSPatch解决线上bug,应在下个版本及时用OC代码修改bug写入项目中,不应该让补丁代码存留超过一个版本,若之后JSPatch停止维护了,也不会产生严重影响

    相关文章

      网友评论

        本文标题:JSPatch的基本使用及思考

        本文链接:https://www.haomeiwen.com/subject/tazzjttx.html