JSPatch 开源以来大部分被用于 hotfix,替换原生方法修复线上bug,但实际上 JSPatch 一直拥有动态添加功能模块的能力,由于 JSPatch 能够建立和调用任意 OC 类和方法,彻底能够用 JSPatch 写功能模块,而后动态下发加载。只是以前在性能和开发体验上有些问题,尚未太多这方面的应用。此次 JSPatch 作了较大的更新,扫除这些问题,让用纯 JS 写功能模块变得实用。这里有个用 JS 写的 Dribbble 客户端 Demo,能够体验下效果。git
来看看此次更新作了什么。github
经过工具能够看到使用 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 工程找到。
以前 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做者)的脑洞,在此感谢~
通过上述优化,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 类时都用这个接口。
还有一个棘手问题,也是使用 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地址