JSPatch更新:完善开发功能模块的能力

JSPatch 开源以来大部分被用于 hotfix,替换原生方法修复线上bug,但实际上 JSPatch 一直拥有动态添加功能模块的能力,因为 JSPatch 可以创建和调用任意 OC 类和方法,完全可以用 JSPatch 写功能模块,然后动态下发加载。只是之前在性能和开发体验上有些问题,还没有太多这方面的应用。这次 JSPatch 做了较大的更新,扫除这些问题,让用纯 JS 写功能模块变得实用。这里有个用 JS 写的 Dribbble 客户端 Demo,可以体验下效果。

来看看这次更新做了什么。

性能优化

通过工具可以看到使用 JSPatch 写功能模块时,耗时较多的点在于 JS 和 OC 的通信,以及通信过程中参数的转换,于是在这块寻找优化点。写功能时需要新增很多类和方法,例如:

defineClass('JPDribbbleView:UIView', {
  renderItem: function(item) {
    ...
  },
})

defineClass('JPDribbbleViewController:UIViewController', {
  render: function(){
    var view = JPDribbbleView.alloc().init();
    view.renderItem(item);
  }
});

上面两个都是新增的类,两个方法也是新增的,按之前的流程,这里的定义会传入 OC,在 OC 生成这两个类,并在这个类上添加这里定义的方法,调用时进入 OC 寻找这些方法调用。

例如上面的 view.renderItem(item) 这句,调用流程是:

进入 __c 函数 -> 转换参数item类型(JS-OC) -> 进入 OC -> callSelector -> 字符串转 Class & Selector 并调用 -> 进入 JPForwardInvocation -> 包装参数 & 转换类型(OC-JS) -> 调用 JS 上的 renderItem 方法 -> 转换返回值类型(OC-JS)。

一个简单的方法调用要经过这么多处理,对于 hotfix 来说这样的新方法调用不常见,调用次数也少,但如果用于做业务模块,就会有大量这种新方法的调用,影响性能。实际上这么多处理都是不必要的,在 JS 定义的方法还要跑到 OC 绕一圈再回来调用,所以优化思路很明显,就是不要绕去 OC,直接在 JS 调用。

经过一轮优化,更新后的 JSPatch 上述的调用流程变成:

进入 __c 函数 -> 调用 JS renderItem 方法。

很清新的流程,去除了所有多余的处理,极大地提高了性能。实现原理就是在 JS 端用一个表保存 className 对应的 JS 方法,调用时如果在这个表找到要调用的方法,就直接调用,不再去到 OC,细节上可以直接看代码。

这个优化不需要使用者做什么修改,书写这些方法的接口并没有变,跟原来一样。实际上实现过程中碰到最大的问题就是接口问题,有机会再分享下这个过程。

那么经过这次优化,这种调用性能提高多少呢?

经测试,不带参数的方法调用提高45倍,带一个普通参数的方法调用提高70倍,带像 NSDictionary / NSArray 这些需要转换的参数时提高700倍。测试用例可以在 Demo 工程找到。

Property

之前 JSPatch 给类新增 property 是通过 -getProp:-setProp:forKey: 这两个接口:

defineClass('JPTableViewController : UITableViewController', {
  dataSource: function() {
    return self.getProp('data');
  },
  setup: function() {
    self.setProp_forKey([1,2,3], 'data')
  }
}

这里有两个问题:

  1. 接口不友好,与 OC 原生property 的写法不一致,写起来别扭。

  2. 每个 property 都是 OC 里的一个 associatedObject,每次存取都要与 OC 通信,大量调用时性能低。

对于hotfix,很少有新增 property 的需求,接口挫点没关系,但若是用来写新功能,property 是家常便饭,就得好好优化了。 这次更新后,可以这样新增property:

defineClass('JPTableViewController : UITableViewController', [
  'data',
  'name',
], {
  dataSource: function() {
    return self.data();
  },
  setup: function() {
    self.setData([1,2,3])
  }
}

接口上做到跟原有 property 一致,解决第一个问题。

对于第二个问题,具体实现上不再是一个 property 对应一个 associatedObject,而是每个对象只有一个对应的 associatedObject,这个 associatedObject 的值是一个 JS 对象,每一个 property 都存在这个JS对象上。

JSPatch更新:完善开发功能模块的能力_第1张图片

如图,左边是修改之前的,右边是修改后的。修改前每一个 property 都单独保存在 OC,一个 property 对应一个 associatedObject,JS 通过接口去存取。修改后一个 OC 对象只有一个 associatedObject,这个 associatedObject 是个 JS 对象,所有 property 集中在这个 JS 对象里面,JS 可以直接对它进行存取操作。

这样做的好处在于在存取 property 时减少了 JS 与 OC 的通信,不需要每次都与 OC 通信,只需要第一次取出这个关联对象,后续对所有 property 的存取操作都是在 JS 内部进行,提高了性能。这个主意来自老郭(samurai-native作者)的脑洞,在此感谢~

defineJSClass()

经过上述优化,defineClass() 里方法调用的性能是提高了,但像数据层的 dataSource / manager 这些不需要依赖 OC 的类也使用 defineClass() 定义还是会比较浪费,因为定义后会生成对应的 OC 类,并在 alloc 时还是要去到 OC 生成这个对象,property 的存取还是要通过 associatedObject,这些都是没必要的。

这种类型的类与 OC 没有联系,不需要继承 OC 类,只在 JS 使用,所以直接使用 JS 原生类就行了,可以减少上述性能上的浪费。只是 JS 原生类定义和对象生成的那套写法与 defineClass() 的写法相去甚远,两种风格混在一起开发体验不太好,于是加了个 defineJSClass() 接口,辅助创建纯 JS 类:

defineJSClass('DBDataSource', {
  init: function(){
    this.data = 'xxx';
    return this;
  },
  readData: function() {
    this.super().loadData();
    return this.data;
  }
}, {
  shareInstance: function(){
    ...
  }
})

var dataSource = DBDataSource.alloc().init();
DBDataSource.shareInstance();

可以看到 defineJSClass() 的写法与 defineClass() 几乎完全一样,包括实例方法/类方法/继承的写法/super调用/对象生成都是一样的,只有有两个地方不同:

  1. 用 this 关键字代替 self

  2. property 不用 getter/setter,直接存取。

这种方式定义类和使用是比 defineClass() 性能高的,推荐不需要继承 OC 类时都用这个接口。

autoConvertOCType()

还有一个棘手问题,也是使用 JSPatch 时最让人迷惑的一点,就是 NSDictionary / NSArray / NSString 这几个类型与 JS 原生 Object / Array / String 互转的问题。

以数组为例,OC NSArray 数组传回给 JS 时都会当成一个普通的 OC 对象,可以调用它的 OC 方法(像 -objectAtIndex),而 JS 上创建的数组是 JS 数组,可以用 [] 取数组元素,不能调用 OC 方法。JS 数组传入 OC 会自动转为 NSArray,传出来会变成 NSArray。于是在 JS 端数组就会有两种类型,你知道某个变量是数组后,还需要知道它是从哪里来的,以此判断它的类型,再用相应的方法。

初期这样做是为了保持功能的完整性,像 NSMutableDictionary / NSMutableArray / NSMutableString 如果传到 JS 时自动转为 JS 类型就没法对这个对象进行修改了。

好在对于 hotfix 来说问题还不算大,因为代码量小很容易看出来源判断它的类型,但对于写功能模块,这里就很容易会被绕晕了。于是加了个开关 autoConvertOCType(),可以自由开启和关闭自动类型转换。只要在 JS 脚本开头加上 autoConvertOCType(1) 这句调用,上述几个类型在通信过程中都会自动转为 JS 类型,在 JS 上不再存在两种类型,只有一种 JS 类型,无需多考虑,这样开发起来就轻松多了。

那若需要调用这些类型 OC 对象的一些方法时怎么办?在调用前后先关后开即可:

autoConvertOCType(0)
var str = NSString.stringWithString('xx');
var data = str.dataUsingEncoding(4);
autoConvertOCType(1)

其他

这次更新还包括完善了 super 的调用,解决某些情况下调用 super 死循环的问题。另外原先放在扩展的 include() 接口合入作为核心功能提供,会自动把主脚本所在目录作为根目录去寻找 include 的文件路径,并保证只 include 一次,还增加了 resourcePath() 接口用于静态资源文件的获取。

后续

之前说到阻碍 JSPatch 用于动态更新的障碍有两个:性能问题和开发效率,这次更新后 JSPatch 在这两个方面都有所提升,接下来继续在使用的过程中挖掘更多的优化点,提供一些常用的静态变量和方法封装,并尝试做 XCode 代码自动补全插件提高开发效率。

最后

现在可以通过 JSPatch 用 JS 写完整的功能模块,再动态下发给 APP 执行,JSPatch 的这套动态化方案相对于 React Native 有以下优势:

  1. 小巧。只需引入 JPEngine.h JPEngine.m JSPatch.js 三个小文件,体积小巧,也无需搭建环境。

  2. 学习成本低。可以继续沿用原来 OC 的思维写程序,无需学习新一套规则,即刻上手。

  3. 限制少。可以说完全没有限制,OC / JS 上玩出花的各种模式都可以照搬使用,不会被某一框架思维和写法限定。所有 OC / JS 库直接使用,无需适配。

欢迎试用,github地址

你可能感兴趣的:(jspatch,ios,动态更新)