JSPatch技术文档
一、背景需求介绍
为什么我们需要一个热修复(hot-fix)技术?
- 工作中容易犯错、bug难以避免。
- 开发和测试人力有限。
- 苹果Appstore审核周期太长,一旦出现严重bug难以快速上线新版本。
- 作为生产力工具,用户有对稳定性和可靠性的需求。
二、JSPatch简介
JSPatch诞生于2015年5月,最初是腾讯广研高级iOS开发@bang的个人项目。
它能够使用JavaScript调用Objective-C的原生接口,从而动态植入代码来替换旧代码,以实现修复线上bug。
JSPatch在Github.com上开源后获得了3000多个star和500多fork,广受关注,目前已被应用在大量腾讯/阿里/百度的App中。
三、JSPatch与wax对比
最关键的是JSPatch可实现方法粒度的线上代码替换,能修复一切代码引起的Bug。
而Wax无法实现。
四、JSPatch实现原理
基础原理
Objective-C是动态语言,具有运行时特性,该特性可通过类名称和方法名的字符串获取该类和该方法,并实例化和调用。
Class class = NSClassFromString(“UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString(“viewDidLoad");
[viewController performSelector:selector];
也可以替换某个类的方法为新的实现:
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");
还可以新注册一个类,为类添加方法:
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
Javascript调用
我们可以用Javascript对象定义一个Objective-C类:
{
__isCls: 1,
__clsName: "UIView"
}
在OC执行JS脚本前,通过正则把所有方法调用都改成调用 __c() 函数,再执行这个JS脚本,做到了类似OC/Lua/Ruby等的消息转发机制:
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
给JS对象基类 Object 的 prototype 加上 c 成员,这样所有对象都可以调用到 c,根据当前对象类型判断进行不同操作:
Object.prototype.__c = function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
}
}
互传消息
JS和OC是通过JavaScriptCore互传消息的。OC端在启动JSPatch引擎时会创建一个 JSContext 实例,JSContext 是JS代码的执行环境,可以给 JSContext 添加方法。JS通过调用 JSContext 定义的方法把数据传给OC,OC通过返回值传会给JS。调用这种方法,它的参数/返回值 JavaScriptCore 都会自动转换,OC里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 会分别转为JS端的数组/对象/字符串/数字/函数类型。
对于一个自定义id对象,JavaScriptCore 会把这个自定义对象的指针传给JS,这个对象在JS无法使用,但在回传给OC时OC可以找到这个对象。对于这个对象生命周期的管理,如果JS有变量引用时,这个OC对象引用计数就加1 ,JS变量的引用释放了就减1,如果OC上没别的持有者,这个OC对象的生命周期就跟着JS走了,会在JS进行垃圾回收时释放。
方法替换
把UIViewController的
-viewWillAppear:
方法通过class_replaceMethod()
接口指向_objc_msgForward
,这是一个全局 IMP,OC 调用方法不存在时都会转发到这个 IMP 上,这里直接把方法替换成这个 IMP,这样调用这个方法时就会走到-forwardInvocation:
。为UIViewController添加
-ORIGviewWillAppear:
和-_JPviewWillAppear:
两个方法,前者指向原来的IMP实现,后者是新的实现,稍后会在这个实现里回调JS函数。改写UIViewController的
-forwardInvocation:
方法为自定义实现。一旦OC里调用 UIViewController 的-viewWillAppear:
方法,经过上面的处理会把这个调用转发到-forwardInvocation:
,这时已经组装好了一个 NSInvocation,包含了这个调用的参数。在这里把参数从 NSInvocation 反解出来,带着参数调用上述新增加的方法-JPviewWillAppear:
,在这个新方法里取到参数传给JS,调用JS的实现函数。整个调用过程就结束了,整个过程图示如下:
最后一个问题,我们把 UIViewController 的 -forwardInvocation:
方法的实现给替换掉了,如果程序里真有用到这个方法对消息进行转发,原来的逻辑怎么办?首先我们在替换 -forwardInvocation:
方法前会新建一个方法 -ORIGforwardInvocation:
,保存原来的实现IMP,在新的 -forwardInvocation:
实现里做了个判断,如果转发的方法是我们想改写的,就走我们的逻辑,若不是,就调 -ORIGforwardInvocation:
走原来的流程。
五、JSPatch代码示例
JSPatch在OC上的调用十分简单
- (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];
}
一个Javascript代码修复Objective-C的bug的示例:
@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
上述代码中取数组元素处可能会超出数组范围导致crash。如果在项目里引用了JSPatch,就可以下发JS脚本修复这个bug:
defineClass("JPTableViewController", {
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);
}
}
}, {})
六、股单App的Hot-fix解决方案
1.版本更新策略
- 考虑到下一个提交的App版本已经修复了上一个版本的bug,所以不同的App版本对应的补丁版本肯定也不同。同一个App版本下,可以出现递增的补丁版本。
- 补丁为全量更新,即新版本补丁包括旧版补丁的内容,更新后新版补丁覆盖旧版补丁。
- 补丁分为可选补丁和必选补丁,必选补丁用于重大bug的修复,如果不更新必选补丁则App无法继续使用。如下图2中,补丁版本v1234对应各自版本的用户,补丁v3为必须更新,补丁v1,v2,v4为可选补丁,则v1,v2的用户必须更新到v4才可使用;而v3的用户可先使用,同时后台静默更新到v4.
股单App补丁版本更新策略
2.安全策略
安全问题在于JS 脚本可能被中间人攻击替换代码。可采取以下三种方法,股单App目前采用的是第三种:
1.对称加密。如zip 的加密压缩、AES 等加密算法。优点是简单,缺点是安全性低,易破解。若客户端被反编译,密码字段泄露,则完成破解。
2.HTTPS。优点是安全性高,证书在服务端未泄露,就不会被破解。缺点是部署麻烦,如果服务器本来就支持 HTTPS,使用这种方案也是一种不错的选择。
3.RSA校验。安全性高,部署简单。
详细校验步骤如下:
1.服务端计算出脚本文件的 MD5 值,作为这个文件的数字签名。
2.服务端通过私钥加密第 1 步算出的 MD5 值,得到一个加密后的 MD5 值。
3.把脚本文件和加密后的 MD5 值一起下发给客户端。
4.客户端拿到加密后的 MD5 值,通过保存在客户端的公钥解密。
5.客户端计算脚本文件的 MD5 值。
6.对比第 4/5 步的两个 MD5 值(分别是客户端和服务端计算出来的 MD5 值),若相等则通过校验。
3.客户端策略
客户端具体策略如下图:
1.用户打开App时,同步进行本地补丁的加载。
2.用户打开App时,后台进程发起异步网络请求,获取服务器中当前App版本所对应的最新补丁版本和必须的补丁版本。
3.获取补丁版本的请求回来后,跟本地的补丁版本进行对比。
4.如果本地补丁版本小于必须版本,则提示用户,展示下载补丁界面,进行进程同步的补丁下载。下载完成后重新加载App和最新补丁,再进入App。
5.如果本地补丁版本不小于必须版本,但小于最新版本,则进入App,不影响用户操作。同时进行后台进程异步静默下载,下载后补丁保存在本地。下次App启动时再加载最新补丁。
6.如果版本为最新,则进入App。
七、参考资料和文献:
1.https://github.com/bang590/JSPatch
2.https://github.com/mmin18/WaxPatch
3.https://github.com/probablycorey/wax
4.https://github.com/alibaba/AndFix
5.http://blog.cnbang.net/tech/2879/
6.http://blog.cnbang.net/works/2767/
7.http://blog.cnbang.net/tech/2808/
8.http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/
原文链接:http://www.jianshu.com/p/0cb81bf23d7a
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
JSPatch技术文档的更多相关文章
- Atitit usrQBK1600 技术文档的规范标准化解决方案
Atitit usrQBK1600 技术文档的规范标准化解决方案 1.1. Keyword关键词..展关键词,横向拓展比较,纵向抽象细化拓展知识点1 1.2. 标题必须有高大上词汇,参考文章排行榜,1 ...
- Kafka 技术文档
Kafka 技术文档 目录 1 Kafka创建背景 2 Kafka简介 3 Kafka好处 3.1 解耦 3.2 冗余 3.3 扩展性 3.4 灵活性 & 峰值处理能力 3.5 可恢复性 ...
- RabbitMq 技术文档
RabbitMq 技术文档 目录 1 AMQP简介 2 AMQP的实现 3 RabbitMQ简介 3.1 概念说明 3.2 消息队列的使用过程 3.3 RabbitMQ的特性 4 RabbitMQ使用 ...
- [转]unity3d 脚本参考-技术文档
unity3d 脚本参考-技术文档 核心提示:一.脚本概览这是一个关于Unity内部脚本如何工作的简单概览.Unity内部的脚本,是通过附加自定义脚本对象到游戏物体组成的.在脚本对象内部不同志的函数被 ...
- Umbraco官方技术文档 中文翻译
Umbraco 官方技术文档中文翻译 http://blog.csdn.net/u014183619/article/details/51919973 http://www.cnblogs.com/m ...
- [转]chrome技术文档列表
chrome窗口焦点管理系统 http://www.douban.com/note/32607279/ chrome之TabContents http://www.douban.com/note/32 ...
- Niagara技术文档汇总
Niagara技术文档汇总http://wenku.baidu.com/view/ccdd4e2c3169a4517723a38f.html Niagara讲解要点http://wenku.baidu ...
- DL动态载入框架技术文档
DL动态载入框架技术文档 DL技术交流群:215680213 1. Android apk动态载入机制的研究 2. Android apk动态载入机制的研究(二):资源载入和activity生命周期管 ...
- 使用Jupyter Notebook编写技术文档
1.jupyter Notebook的组成 这里它的组件及其工程构成,帮助大家更好的用好jupyter Notebook 组件 Jupyter Notebook结合了三个组件: 笔记本Web应用程序: ...
随机推荐
- symfony2-不同bundle的entity的一对多关系
重点:其实和普通一个bundle中一样,只是把entity地址写全就行. 例子: 表commentone (多方) 表shopone(一方) 在Userbundle中的Commentone实体对应关系
- hdu 4034 Graph floyd
题目链接 给出一个有向图各个点之间的最短距离, 求出这个有向图最少有几条边, 如果无法构成图, 输出impossible. folyd跑一遍, 如果dp[i][j] == dp[i][k]+dp[k] ...
- 【LeetCode题意分析&解答】36. Valid Sudoku
Determine if a Sudoku is valid, according to: Sudoku Puzzles - The Rules. The Sudoku board could be ...
- 四轴飞行器1.1 Matlab 姿态显示
四轴飞行器1.1 Matlab 姿态显示 开始做四轴了,一步一步来,东西实在很多,比较杂.先做matlab上位机,主要用来做数据分析,等板子到了可以写飞控的程序了,从底层一层一层开始写..希望能好好的 ...
- php基础知识笔记
基本语法 php文件的后缀名可以是 .php .php3 .phtml 输出命令 echo , print 语句以;结束 注释 // /* */ 变量都带$前缀, 如: $x, $names, $th ...
- SPOJ 8222 Substrings(后缀自动机)
[题目链接] http://www.spoj.com/problems/NSUBSTR/ [题目大意] 给一个字符串S,令F(x)表示S的所有长度为x的子串中,出现次数的最大值. 求出所有的F. [题 ...
- NGUI使用教程(1) 安装NGUI插件
前言 鉴于当前游戏开发的大势,Unity3d的发展势头超乎我的预期,作为一个Flash开发人员,也是为Flash在游戏开发尤其是手游开发中的地位感到担忧....所以 近期一段时间都在自己学习unity ...
- 阿里云Ubuntu部署java web(2) - 配置tomcat
系统版本号:Ubuntu12.04 64位 安装: 首先要安装java(測试时使用的版本号是6b27-1.12.6-1ubuntu0.12.04.2).版本号可自行选择,但不同版本号配置方法可能不同. ...
- activity的生命周期【转】
关于activity的生命周期,越来越感觉很重要.activity的生命周期有点像asp.net中page的生命周期,经历好几个过程.重写不同的阶段,可以完成不同的功能和效果.先上一张经典的生命周期图 ...
- 九度OnlineJudge之1012:畅通工程
题目描述: 某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇.省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路 ...