iOS【JSPatch热更新】实践一

JSPatch 是一个开源项目(Github链接),只需要在项目里引入极小的引擎文件,
使用JavaScript调用任何Objective-C的原生接口,替换任意 Objective-C 原生方法。
目前主要用于下发 JS 脚本替换原生 Objective-C 代码,实时修复线上 bug。

例如线上 APP 有一段代码出现 bug 导致 crash:

//OC FFEasyLifeHomeCtrl
self.nameArrays = @[@"0",@"1",@"2",@"3",@"4"];
...
- (void)testArray {//测试数组越界
    NSString *testName = self.nameArrays[5];
}

//JS
defineClass('FFEasyLifeHomeCtrl',['data','nameArray','totalCount'], {
        testArray: function() {//修改数组越界
            var testName = self.nameArrays([4]);
       }
});

除了修复 bug,JSPatch也可以用于动态运营,实时修改线上APP行为,
或动态添加功能。JSPatch 详细使用文档见 [Github Wiki](https://github.com/bang590/JSPatch/wiki)。

JSPatch优势:

1、JSPatch更符合Apple的规则。iOS Developer Program License Agreement里3.3.2提到不可动态下发可执行代码,但通过苹果JavaScriptCore.framework或WebKit执行的代码除外,JS正是通过JavaScriptCore.framework执行的

2、使用系统内置的JavaScriptCore.framework,无需内嵌脚本引擎,体积小巧

3、支持block

JSPatch缺点:

1、JSPatch劣势在于不支持iOS6,因为需要引入JavaScriptCore.framework

2、持续改进中存在风险:JSPatch让脚本语言获得调用所有原生OC方法的能力,不像web前端把能力局限在浏览器,使用上会有一些安全风险

3、若在网络传输过程中下发明文JS,可能会被中间人篡改JS脚本,执行任意方法,盗取APP里的相关信息,危及用户信息和APP

4、若下载完后的JS保存在本地没有加密,在越狱的机器上用户也可以手动替换或篡改脚本

JSPatch 风险

1、JSPatch脚本的执行权限很高,若在传输过程中被中间人篡改,会带来很大的安全问题,为了防止这种情况出现,在传输过程中对JS文件进行了RSA签名加密,流程如下:
服务端:计算JS文件MD5值。用RSA私钥对MD5值进行加密,与JS文件一起下发给客户端。
客户端:拿到加密数据,用RSA公钥解密出MD5值。本地计算返回的JS文件MD5值。对比上述的两个MD5值,若相等则校验通过,取JS文件保存到本地。
由于RSA是非对称加密,在没有私钥的情况下第三方无法加密对应的MD5值,也就无法伪造JS文件,杜绝了JS文件在传输过程被篡改的可能。

2、本地存储
本地存储的脚本被篡改的机会小很多,只在越狱机器上有点风险,对此JSPatch SDK在下载完脚本保存到本地时也进行了简单的对称加密,每次读取时解密。

更新能力

React Native 和 JSPatch 都能对用其开发出来的功能模块进行热更新,这也是这种方案最大的好处。

React Native: 在热更新时无法使用事先没有做过桥接的原生组件,例如需要加一个发送短信功能,需要用到原生 MessageUI.framework 的接口,若没有在编译时加上提供给 JavaScript 的接口,是无法调用到的。

JSPatch: 可以调用到任意已在项目里的组件,以及任意原生 framework 接口,不需要事先做桥接,在热更新的能力上,相对来说 JSPatch 的能力和自由度会更高一些。

性能体验

JSPatch 的性能问题主要在于 JavaScript 和 Objective-C 的通信,每次调用 Objective-C 方法都要通过 Objective-C Runtime 接口,并进行参数转换。
runtime 接口调用带来的耗时一般不会成为瓶颈,参数转换则需要注意避免在 JavaScript 和 Objective-C 之间传递大的数据集合对象。
JSPatch 在性能方面也针对开发功能做了不少优化,尽力减少了 JavaScript 和 Objective-C 的通信,来看并没有碰到太多性能问题。

集成JSPatch过程——>SDK接入

第一步 获得 AppKey

在平台上注册帐号,可以任意添加新 App,每一个 App都有一个唯一的 AppKey 作为标识。
网站:http://jspatch.com/Apps

第二步 集成SDK

通过 cocoapods 集成

在 podfile 中添加命令:
pod 'JSPatchPlatform'
再执行 pod install 即可。

手动集成
若没有使用 cocoapods,也可以手动集成。下载 SDK 后解压,将 JSPatchPlatform.framework 拖入项目中,
勾选 "Copy items if needed",并确保 "Add to target" 勾选了相应的 target。

添加依赖框架:TARGETS -> Build Phases -> Link Binary With Libraries -> + 添加 libz.dylib 和 JavaScriptCore.framework。

注意:手动集成无法断点调试 JSPatch 核心源码,推荐使用 cocoapods 方式集成。

第三步 运行
在 AppDelegate.m 里载入文件,并调用 +startWithAppKey: 方法,参数为第一步获得的 AppKey。接着调用 +sync 方法检查更新。

例子:
#import <JSPatchPlatform/JSPatch.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [JSPatch startWithAppKey:@"你的AppKey"];
    [JSPatch sync];
    ...
}
@end
至此 JSPatch 接入完毕,下一步可以开始在后台为这个 App 添加 JS 补丁文件了。

上述例子是把 JSPatch 同步放在 -application:didFinishLaunchingWithOptions: 里,
若希望补丁能及时推送,可以把 [JSPatch sync] 放在 -applicationDidBecomeActive: 里,每次唤醒都能同步更新 JSPatch 补丁,不需要等用户下次启动。

项目结构

  • 图片 1.png

本地创建main.js
终端创建JS文件命令:touch test.js

项目代码

#import "AppDelegate.h"
#import <JSPatchPlatform/JSPatch.h>

@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    //[self hotJSPatch];
    //本地测试
   [self hotLocalJSPatch];

    return YES;
}

- (void)hotJSPatch {

    //传入在平台申请的 appKey。会自动执行已下载到本地的 patch 脚本。
    [JSPatch startWithAppKey:@"c38c725a42102b45"];


    /*
     定义用户属性
     用于条件下发,例如:
     [JSPatch setupUserData:@{@"userId": @"100867", @"location": @"guangdong"}];
     在 `+sync:` 之前调用
     */
    //[JSPatch setupUserData:@{@"userId": @"1000876", @"isMale": @(1)}];

    #ifdef DEBUG
    //进入开发模式
    [JSPatch setupDevelopment];  
    #endif

     //与 JSPatch 平台后台同步,发请求询问后台是否有 patch 更新,如果有更新会自动下载并执行可调用多次(App启动时调用或App唤醒时调)
    [JSPatch sync];

    //在状态栏显示调试按钮,点击可以看到所有 JSPatch 相关的 log 和内容

    [JSPatch showDebugView];
}

- (void) hotLocalJSPatch {

    //用于发布前测试脚本
    //测试完成后请删除,改为调用 +startWithAppKey: 和 +sync

    //加载本地js调试
    [JSPatch testScriptInBundle];

    //在状态栏显示调试按钮,点击可以看到所有 JSPatch 相关的 log 和内容
    [JSPatch showDebugView];

}
FFEasyLifeHomeCtrl.m
====================
1. 数组越界;
2. 未实现按钮事件方法
====================

#import "FFEasyLifeHomeCtrl.h"

@interface FFEasyLifeHomeCtrl ()
@property (nonatomic, strong) NSArray      *nameArrays;
@property (nonatomic, strong) UIButton     *catButton;

@end

@implementation FFEasyLifeHomeCtrl
- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.catButton];
    [self testArray];

}

// MARK: - 方法
- (void)testArray {//数组越界
    NSString *testName = self.nameArrays[5];
}

// MARK: - getter
- (UIButton *)catButton {
    if (!_catButton) {
        _catButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _catButton.backgroundColor = k_COLORRANDOM;
        [_catButton setTitle:@"美图" forState:UIControlStateNormal];

        /** 未实现事件方法 */
        [_catButton addTarget:self action:@selector(actionPicture:) forControlEvents:UIControlEventTouchUpInside];
   }
    return _catButton;
}

JS代码

main.js
====================
1.处理数组越界问题;
2.添加按钮事件方法;
3.跳转到一个新控制器(用js创建的新控制器)
====================

include(‘FFEasyLifeHomeCtrl.js')

//用js创建的新控制器
include('FFEasyLifePictureCtrl.js')
FFEasyLifeHomeCtrl.js
require('UIView, UIColor, UILabel, UIFont, UIImageView, UIImage')
require('FFEasyLifePictureCtrl')

defineClass('FFEasyLifeHomeCtrl', {
        
     testArray: function() {
           // 1. 处理数组越界问题
           var nameArrays = self.nameArrays().toJS();
           var testName = nameArrays[4];
           console.log('----- testName: ' + testName);       
     },

     // 2. 添加按钮事件方法
     actionPicture: function(button) {
          var ctrl = FFEasyLifePictureCtrl.alloc().init();
          ctrl.view().setBackgroundColor(UIColor.lightGrayColor());
          //self.navigationController().pushViewController_animated(ctrl, YES);
          
          3. 跳转到一个新控制器(用js创建的新控制器)
          self.presentViewController_animated_completion(ctrl, YES, null);
     },
});
FFEasyLifePictureCtrl.js
require('UIColor');

defineClass('FFEasyLifePictureCtrl : UITableViewController <UIAlertViewDelegate>', ['data'], {
        
        init: function() {
            self = self.super().init()
            return self
        },
        
        viewDidLoad: function() {
        },
        
        dataSource: function() {
        
            //数组
            var data = self.data();
            if (data) return data;
        
            var data = [];
            for (var i = 0; i < 20; i ++) {
                data.push("cell from js " + i);
            }
        
            self.setData(data)
            console.log('data:'  + self.data());

            return data;
        },
        
        // MARK: - tableDelegate
        numberOfSectionsInTableView: function(tableView) {
            return 1;
        },
        
        tableView_numberOfRowsInSection: function(tableView, section) {
            return self.dataSource().length;
        },
        
        tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
            return 200;
        },
        
        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()[indexPath.row()])
            cell.setBackgroundColor(UIColor.colorWithRed_green_blue_alpha((Math.random() *255) / 255.0, (Math.random() *255) / 255.0, (Math.random() *255) / 255.0, 1));

            return cell
        },
        
        tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
        
            //弹窗
            var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert",self.dataSource()[indexPath.row()], self, "OK",  null);
            alertView.show()
        },
        
        alertView_willDismissWithButtonIndex: function(alertView, idx) {
            console.log('click btn ' + alertView.buttonTitleAtIndex(idx).toJS())
        }
})

JSPatch 创建应用

  • app.png

JSPatch执行顺序问题:

JSPatch所有动态替换的函数,都必须在JS执行完了之后,第二次再执行,才会全面以新替换的js代码进行工作。
时间顺序
#• application:didFinishLaunchingWithOptions:
#• JSPatch发起网络请求拉patch
#• app的rootViewController触发ViewDidload运行完毕,依然是未修正的错误界面
#• JSPatch网络请求拉取回来,执行JS
#• JS已经执行成功ViewDidLoad已经被替换,但是界面已经生成,新的正确的ViewDidLoad并不会再次执行
效果:我的viewDidLoad为啥不能修改啊?
比喻:
#• viewDidload的函数代码就好比建筑设计图
#• 运行起来后的界面就好比建好的建筑
时间顺序:
#• viewDidLoad有bug需要改(建筑设计图图纸错了)
#• 旧viewDidLoad先执行,并且创建好了界面(工人已经按着错图纸把建筑建好了)
#• JSPatch执行了hotfix(设计师修改设计图纸)
#• JSPatch看起来没效果(就算你改好了建筑图纸,已经建好的建筑是不会有任何改变的)

解决办法:2个(未去实践过)
#• 在建造建筑之前,把图纸改好
JSPatch在使用的时候,第一次下载网络请求是要时间的,所以才会发生修改图纸,在建筑建好之后。
但是补丁已经下载完成,第二次运行app,新的图纸已经存在本地,是可以在创建rootViewController之前,就先把patch运行,让新图纸生效的。

#•  不要修改图纸了,直接去修改建筑
当你网络请求在JSPatch下载完Patch之后,通过callback,进行完全自定义的处理,窗户坏了,直接改窗户,门坏了修门,你也可以自定义把房子推倒了重建
如果你使用的是JSPatchSDK,那么头文件有一个callback的API,JSPatchSDK提供了JS下载完成的这个时机,具体怎么修,纯看使用者自己

帮助网址:
JSPatch官网:http://jspatch.com
JSPatch官方文档:http://jspatch.com/Docs/dev

注意项:
1:补丁版本号与app版本号一样;
2:多个js时,放在一个文件夹里压缩成zip;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 简介: JSPatch是最近业余做的项目,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何...
    Luciena阅读 928评论 0 8
  • 1, JSPatch热更新 众所周知,AppStore 上发布需要有个一非常恶心的审核期,而且很可能被拒绝掉,发布...
    嘹亮的浩哥阅读 1,075评论 2 3
  • JSPatch是什么 JSPatch是一个开源项目,只需要在项目里引入极小的引擎文件,就可以使用 JavaScri...
    ImmortalSummer阅读 2,527评论 7 11
  • 背景介绍 IOS平台提交审核的周期太长,快则45天,慢则半个月或者20天,如果碰到圣诞节等假日,可能一个月都有可能...
    恒源宾馆阅读 2,233评论 10 27
  • 和朋友吃饭,餐刚到,来了一个大叔,高高的个子,带着一个鼓鼓的旧帆布包。周围有很多干净的桌子,他偏偏走向角落未收拾的...
    阿北先生阅读 223评论 0 2