JSPatch简介 – 动态更新iOS APP

来源:互联网 发布:金融数据挖掘python 编辑:程序博客网 时间:2024/05/22 12:30

1.用途

是否有过这样的经历:新版本上线后发现有个严重的bug,可能会导致crash率激增,可能会使网络请求无法发出,这时能做的只是赶紧修复bug然后提交等待漫长的AppStore审核,再盼望用户快点升级,付出巨大的人力和时间成本,才能完成此次bug的修复。
使用JSPatch可以解决这样的问题,只需在项目中引入JSPatch,就可以在发现bug时下发JS脚本补丁,替换原生方法,无需更新APP即时修复bug。

2.例子

@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:

import "JPEngine.m"@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    [JPEngine startEngine];    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/bugfix.JS"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {    NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];    if (script) {      [JPEngine evaluateScript:script];    }}];    return YES;}@end//JSdefineClass("JPTableViewController", {  //instance method definitions  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {    var row = indexPath.row()    if (self.dataSource().length > row) {  //加上判断越界的逻辑      var content = self.dataSource()[row];      var ctrl = JPViewController.alloc().initWithContent(content);      self.navigationController().pushViewController(ctrl);    }  }}, {})

这样 JPTableViewController 里的 -tableView:didSelectRowAtIndexPath: 就替换成了这个JS脚本里的实现,在用户无感知的情况下修复了这个bug。
更多的使用文档和demo请参考[github项目主页]。

3.原理

简单来说,JSPatch用iOS内置的JavaScriptCore.framework作为JS引擎,但没有用它JSExport的特性进行JS-OC函数互调,而是通过Objective-C Runtime,从JS传递要调用的类名函数名到Objective-C,再使用NSInvocation动态调用对应的OC方法。

3.1基础原理

能做到通过JS调用和改写OC方法最根本的原因是 Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:

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, @"");

理论上你可以在运行时通过类名/方法名调用到任何OC方法,替换任何类的实现。所以 JSPatch 的原理就是:JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。

3.2方法调用

require('UIView')var view = UIView.alloc().init()view.setBackgroundColor(require('UIColor').grayColor())view.setAlpha(0.5)

引入JSPatch后,可以通过以上JS代码创建了一个 UIView 实例,并设置背景颜色和透明度。

3.3方法替换

JSPatch 可以用 defineClass 接口任意替换一个类的方法,用一种hack方式实现。

OC上,每个类都是这样一个结构体:

struct objc_class {  struct objc_class * isa;  const char *name;  ….  struct objc_method_list **methodLists; /*方法链表*/};

其中 methodList 方法链表里存储的是Method类型:

typedef struct objc_method *Method;typedef struct objc_ method {  SEL method_name;  char *method_types;  IMP method_imp;};

Method 保存了一个方法的全部信息,包括SEL方法名,type各参数和返回值类型,IMP该方法具体实现的函数指针。

通过 Selector 调用方法时,会从 methodList 链表里找到对应Method进行调用,这个 methodList 上的的元素是可以动态替换的,可以把某个 Selector 对应的函数指针IMP替换成新的,也可以拿到已有的某个 Selector 对应的函数指针IMP,让另一个 Selector 跟它对应,Runtime 提供了一些接口做这些事,以替换 UIViewController 的 -viewDidLoad: 方法为例:

static void viewDidLoadIMP (id slf, SEL sel) {   JSValue *jsFunction = …;   [jsFunction callWithArguments:nil];}Class cls = NSClassFromString(@"UIViewController");SEL selector = @selector(viewDidLoad);Method method = class_getInstanceMethod(cls, selector);//获得viewDidLoad方法的函数指针IMP imp = method_getImplementation(method)//获得viewDidLoad方法的参数类型char *typeDescription = (char *)method_getTypeEncoding(method);//新增一个ORIGViewDidLoad方法,指向原来的viewDidLoad实现class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);//把viewDidLoad IMP指向自定义新的实现class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);

这样就把 UIViewController 的 -viewDidLoad 方法给替换成我们自定义的方法,APP里调用 UIViewController 的 viewDidLoad 方法都会去到上述 viewDidLoadIMP 函数里,在这个新的IMP函数里调用JS传进来的方法,就实现了替换 -viewDidLoad 方法为JS代码里的实现,同时为 UIViewController 新增了个方法 -ORIGViewDidLoad 指向原来 viewDidLoad 的IMP,JS可以通过这个方法调用到原来的实现。

4.安全部署RSA校验

RSA校验属于数字签名,用了跟 HTTPS 一样的非对称加密,只是简化了,把非对称加密只用于校验文件,而不解决传输过程中数据内容泄露的问题,而我们的目的只是防止传输过程中数据被篡改,对于数据内容泄露并不是太在意。整个校验过程如下:

RSA校验

  1. 服务端计算出脚本文件的 MD5 值,作为这个文件的数字签名。
  2. 服务端通过私钥加密第 1 步算出的 MD5 值,得到一个加密后的 MD5 值。
  3. 把脚本文件和加密后的 MD5 值一起下发给客户端。
  4. 客户端拿到加密后的 MD5 值,通过保存在客户端的公钥解密。
  5. 客户端计算脚本文件的 MD5 值。
  6. 对比第 4/5 步的两个 MD5 值(分别是客户端和服务端计算出来的 MD5 值),若相等则通过校验。

只要通过校验,就能确保脚本在传输的过程中没有被篡改,因为第三方若要篡改脚本文件,必须计算出新的脚本文件 MD5 并用私钥加密,客户端公钥才能解密出这个 MD5 值,而在服务端未泄露的情况下第三方是拿不到私钥的。

最后有个小问题,保存在客户端的代码也可能被人篡改,需不需要采取措施?这个要看各人需求了,因为这个安全问题不大,能篡改本地文件,差不多已经有手机所有权限了,这时也无所谓脚本会不会被篡改了。若有需要,可以加个简单的对称加密,或者按上述流程每次都验证一遍MD5值。

0 0