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对象上。
如图,左边是修改之前的,右边是修改后的。修改前每一个 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
互转的问题。
以数组为例,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 有以下优势:
小巧。只需引入
JPEngine.h
JPEngine.m
JSPatch.js
三个小文件,体积小巧,也无需搭建环境。学习成本低。可以继续沿用原来 OC 的思维写程序,无需学习新一套规则,即刻上手。
限制少。可以说完全没有限制,OC / JS 上玩出花的各种模式都可以照搬使用,不会被某一框架思维和写法限定。所有 OC / JS 库直接使用,无需适配。
欢迎试用,github地址