JSPatch诞生于2015年5月,最初是腾讯广研高级iOS开发@bang的我的项目。
它可以使用JavaScript调用Objective-C的原生接口,从而动态植入代码来替换旧代码,以实现修复线上bug。
JSPatch在Github.com上开源后得到了3000多个star和500多fork,广受关注,目前已被应用在大量腾讯/阿里/百度的App中。javascript
最关键的是JSPatch可实现方法粒度的线上代码替换,能修复一切代码引发的Bug。
而Wax没法实现。java
Objective-C是动态语言,具备运行时特性,该特性可经过类名称和方法名的字符串获取该类和该方法,并实例化和调用。git
Class class = NSClassFromString(“UIViewController"); id viewController = [[class alloc] init]; SEL selector = NSSelectorFromString(“viewDidLoad"); [viewController performSelector:selector];
也能够替换某个类的方法为新的实现:github
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对象定义一个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,根据当前对象类型判断进行不一样操做:bash
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在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); } } }, {})
安全问题在于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 值),若相等则经过校验。
客户端具体策略以下图:
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/