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') }}
这里有两个问题:
接口不友好,与 OC 原生property 的写法不一致,写起来别扭。
每个 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对象上。
[图片上传中。。。(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调用/对象生成都是一样的,只有有两个地方不同:
用 this 关键字代替 self
property 不用 getter/setter,直接存取。
这种方式定义类和使用是比defineClass()
性能高的,推荐不需要继承 OC 类时都用这个接口。
autoConvertOCType()
还有一个棘手问题,也是使用 JSPatch 时最让人迷惑的一点,就是NSDictionary
/NSArray
/NSString
这几个类型与 JS 原生Object
/Array
/String
互转的问题。
以数组为例,OCNSArray
数组传回给 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 有以下优势:
小巧。只需引入JPEngine.h
JPEngine.m
JSPatch.js
三个小文件,体积小巧,也无需搭建环境。
学习成本低。可以继续沿用原来 OC 的思维写程序,无需学习新一套规则,即刻上手。
限制少。可以说完全没有限制,OC / JS 上玩出花的各种模式都可以照搬使用,不会被某一框架思维和写法限定。所有 OC / JS 库直接使用,无需适配。