【第1615期】React Native 图表性能优化实践
前言
今日早读文章由广发证券@谢杨玲授权分享。
回复关键词 广发证券 了解该专栏
正文从这开始~~
在证券交易类 APP 中,图表是最常用的表达数据方式之一,本文介绍了广发证券在使用 React Native 技术开发APP中对图表渲染相关技术的探索及优化实践
背景
广发证券是金融行业中最早大规模使用 React Native 的公司之一,从2016年引入开始,已经在多个端超过30个系统/工具中使用 React Native 技术进行业务开发。在证券交易类 APP 中,图表是最常用的表达数据方式之一,股票行情 K 线图,各种股票工具分析图,基金走势图等,在大量地使用图表进行应用开发中,我们不断探索在浏览器端、混合技术端、原生端如何高性能渲染这些图表,积累不少图表性能优化的实践经验。
以下为易淘金国际 APP 页面截图:
React Native 图表技术背景
在 React Native 中,官方提供的绘图方案只有 ART 组件,而 ART 组件只是一个基础绘图组件,对于复杂的图表场景并不是一个现成的解决方案。对于复杂图表的技术选型,主要有两个常见方案可以选择:WebView 与原生端组件方案。
WebView
这种方案是使用 Web 图表库,在网页里绘制图表 ,然后在 React Native 中用 WebView 将网页展示出来。这种方法简单,将 Web 成熟的图表库如:echarts,highCharts 用 WebView 组件封装一下,就能直接使用。
原生端组件
这种方案分两种情况,其中,一种是绘制逻辑由 JS 端负责:Android 与 iOS 成熟的图表组件也很多,也可以将这些成熟的图表组件封装给 JS 端使用,JS 端实现绘制逻辑,原生端负责呈现。这种做法性能比 WebView 方式性能好,但是 React Native 生态中有两端都封装好的成熟的方案较少。另一种绘制逻辑完全由原生端负责:Android 与 iOS 端分别实现图表功能,给JS 端暴露方法直接调用展示。这种方式性能最好,但是需要两端分别负责绘制逻辑,维护成本高。
方案 | 优势 | 劣势 |
---|---|---|
WebView | 使用简单 | WebView 性能问题 |
原生 + RN | 业务逻辑统一,绘制性能保证 | bridge 通信成本 |
纯原生端 | 绘制性能高 | 两套绘制代码,维护成本高 |
实际使用的方案
在实际使用场景中,对于 “股票行情 K 线” 这种用户操作频度高,实时交互性强的场景,我们使用了纯原生端绘制的技术方案。
纯原生端绘制 k 线的技术方案,是由客户端团队自研与维护,该方案已经历过百万日活考验,在我司各终端交易APP的行情场景中广泛使用。尽管客户端两端实现带来更高的维护成本,在保证K线绘制性能这个最基本的股票交易行为上,这种方案经历几年后回看依然是值得的。
而对于其他次要场景,比如基金收益折线图,资产配置饼图等,这些图表我们没有自研的方案。在项目前期,需求时间紧可投入人力也不多,为了快速上线,这种场景我们采用成本低的 WebView echarts 方案。
性能优化
WebView 方案虽然使用成本低,但是存在着首次加载速度慢,内存占用高等性能问题。随着后来项目需求迭代进入正轨,团队开始考虑取缔 WebView 图表方案。
开源方案调研
我们调研了 react-native-charts-wrapper ,这个库是对 Android 热门的图表库 MPAndroidChart 与 iOS 受欢迎的图表库 Charts 的封装。尽管有原生端组件支持,性能应该较为理想,但它存在着平台 UI 差异性,文档不全,旧项目移植成本高的问题,因此我们没有采用。
ReactJS 生态里有一个 Web 图表库 victory,并且也有基于 react-native-svg 的 React Native 端的支持 victory-native 。但是 victory 在使用上比较难,文档示例较少且没有复杂示例,而 victory-native 也有不少因为 react-native-svg 的 bug 导致的问题,这也可能是 stars 数不多,不太流行的原因。
在调研了其他项目后,依然没找到理想方案,于是往自研的方向上来。我们的团队 Web 前端开发经验偏多,自然地想从 Web 绘制技术上入手,寻求可能的突破点。
跨平台绘图
Web 底层的绘图能力主要有:Canvas 与 SVG。使用 Canvas 绘图是利用命令式方式调用 Canvas API 。使用 SVG 是使用 svg,path,line 等标签描述绘图,属于声明式的方式。我们要取缔 WebView 方案,就需要提供原生端的 Canvas 或 SVG 组件与绘图能力。
社区已有 ART 与 react-native-svg 组件对 SVG 绘图方式的支持。虽然 echarts 在 Web 上能支持 SVG 绘图,但也只有改动 echarts 源码将底层 Web SVG 组件替换成 ART 或 react-native-svg,才有可能解决平台的兼容性问题。当然这其中肯定还会碰到很多其他的问题,考虑到实施成本很高,于是我们停止了对这个方向的尝试。
而 Canvas 绘图是命令式的 API 调用,这让跨平台更为容易。 echarts 对微信小程序平台的支持,用的就是 Canvas 渲染方式。选用 Canvas 方案,我们可以继续沿用 echarts,原有项目的改动成本不大。然而 React Native 社区并没有原生端支持的 Canvas 组件。尽管有一个 react-native-canvas ,却是基于 WebView 的实现。为此,我们需要自己实现原生端 Canvas 组件与提供 Canvas API。
此外需要提及一点,阿里开源的 GCanvas 项目支持 React Native 平台,但是目前提供的 Canvas API 也还不全,不够成熟,所以我们没有采用。
React Native Canvas 组件
在对 iOS 与 Android 绘图技术方案调研后,我们敲定了 Canvas 组件技术方案:iOS 端使用 Core Graphics ,Android 端使用 graphics Canvas 相关包来实现。
整个项目主要在实现 iOS 与 Android 端的 Canvas API 上花费了很多时间并且踩了不少坑,不过项目还是按照预期完成了。
此外,为了能使用 echarts 、f2 这种已经支持微信小程序的图表库,我们设计了与小程序一样的 draw 方法,为此还需要模拟微信环境。
工作原理
Canvas 组件工作原理如下:
JS 端调用 Canvas API 方法进行绘图,这些方法会转换成绘制命令,经过 bridge 将命令传到原生端,原生端在收到命令后通过反射调用原生端实现的 Canvas API 方法,视图更新,绘制完成。
下面跟大家分享几点项目中的经验:
批量执行绘制方法
不是执行每一个 Canvas API 方法就会马上绘制,反之,我们将 JS 端的绘制命令收集设计成异步的,并且是批量发送到原生端。在所有绘制动作完成后,执行 draw 方法,才会将命令集合发送到原生端。
JS 端批量发送绘制命令的相关方法:
draw(clear = true) {
this.drawing(clear ? 1 : 0);
...
}
drawing(clear) {
if (this._isAsync) {
CanvasNativeAPI.drawAsync(this._canvasId, this.actions, clear);
} else {
CanvasNativeAPI.drawSync(this._canvasId, this.actions, clear);
}
}
iOS 端批量执行绘制命令的方法:
- (void)runActions
{
for (NSDictionary *action in _actions) {
[delegate invoke:_context method:[action objectForKey:@"method"] arguments:[action objectForKey:@"arguments"]];
}
}
高性能反射技巧
因为绘制命令是异步批量的,所以原生端需要由命令字符串来调用相关函数方法,因此需要使用反射。
使用反射会带来代码执行的性能问题,为了提高反射性能,我们参考了 React Native 的源码,将需要反射的模块方法,在模块初始化或者首次调用的时候将反射处理的这些动态化方法与对象存储起来以便重复使用。
iOS 端方法首次执行时存储 invocation:
- (void)buildInvocation
{
SEL selector = NSSelectorFromString(_method);
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:selector];
_argumentsNumber = methodSignature.numberOfArguments - 2;
_invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
_invocation.selector = selector;
};
- (void)invoke:(id)instance arguments:(NSArray *)arguments
{
if (_invocation == nil) {
[self buildInvocation];
}
...
}
Android 端模块初始化时存储 method,执行时直接使用:
public void invoke(Object moduleClassInstance, Object[] parameters) {
Object[] arguments = new Object[parameters.length];
if (mArgumentExtractors == null) {
throw new Error("processArguments failed");
}
for (int i = 0; i < mArgumentExtractors.length; i++) {
arguments[i] = mArgumentExtractors[i].extractArgument(parameters, i);
}
try {
mMethod.invoke(moduleClassInstance, arguments);
} catch (IllegalArgumentException ie) {
throw new RuntimeException("Could not invoke " + mMethodName, ie);
} catch (IllegalAccessException iae) {
throw new RuntimeException("Could not invoke " + mMethodName, iae);
} catch (InvocationTargetException ite) {
throw new RuntimeException("Could not invoke " + mMethodName, ite);
}
}
同步方法调用
虽然 React Native 官方的模块与组件对外提供给 JS 端都是异步方法,文档也没有提及同步方法,但是 React Native 内部却有些地方使用了同步调用方式。我们找到了使用方式,并在原生端将同步方法暴露给了 JS 端。同步方法在 JS 线程中同步调用,不会走 React Native 的异步队列。
Canvas measureText 的 API 是同步方法,我们相应的也在原生端提供了这个同步方法。此外,对 JS 端发送命令集到原生端的方法,我们 Canvas 组件也提供了同步调用方式,可以满足手势识别等交互实时性强的,需要实时重绘的场景。
对于提供同步方法,iOS 端使用 RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD 宏:
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(id, drawSync
: (NSString *)tag actions
: (id)actions
: (int)clear)
{
[CanvasAPI draw:tag actions:actions clear:clear];
return @1;
}
Android 端在 @ReactMethod 装饰器方法中传 isBlockingSynchronousMethod = true 来表示同步方法:
@ReactMethod(isBlockingSynchronousMethod = true)
public Integer drawSync(final String tag, ReadableArray actions, final Integer clear) {
draw(tag, actions, clear);
return 1;
}
性能对比
为了测量 Canvas 组件的性能,我们针对 WebView 与 Canvas 分别加载五个相同配置的 echarts 组件进行试验,并观测页面展示前后相关性能数据变化。
为了测量页面加载速度,试验中连续进入 Canvas 与 WebView 两次,来观察加载速度的差异性。
很明显,在首次加载速度方面,Canvas 表现正常,而 WebView 可体验到明显的延迟,第二次进入时才正常。
具体性能数据方面上,主要关注内存、FPS、CPU 使用率方面的数据。试验中分别进入 Canvas 与 WebView 一次,其中,内存在每次杀掉进程后单独统计,观测结果如下:
Android 端:
iOS 端:
方案 | 首次加载速度 | 内存 | FPS | CPU使用率 |
---|---|---|---|---|
Android WebView | 慢 | 高 | 高 | 较高 |
Android Canvas | 正常 | 一般 | 高 | 一般 |
iOS WebView | 慢 | 高 | 高 | 高 |
iOS Canvas | 正常 | 一般 | 高 | 高 |
相比于 WebView 方案,iOS 端 Canvas 组件内存使用少,FPS 稳定,Android 端 Canvas 组件 CPU 使用率低,内存使用少,性能理想。
不足与改进之处
虽然 Canvas 组件性能上比较优秀,但是目前也存在一些不足:
待完成部分 API
目前还有部分 API 没有实现,主要有:
createImageData
getImageData
putImageData
isPointInPath
isPointInStroke
createLinearGradient
createRadialGradient
createPattern
这些方法虽然不太常用,但是之后也会慢慢实现。
iOS CPU 使用率
Canvas 在 iOS 端是使用 Core Graphics 实现,因为是软件绘制,所以对 CPU 利用率高。这个是技术方案的限制,之后可能尝试使用 OpenGL 等 GPU 绘制技术方案来改善。
总结
通过在原生端实现 Canvas API ,我们得以在 React Native 平台上使用 echarts 这样方便成熟的 Web 图表库。我们正逐步将旧项目中 WebView echarts 方案替换为 Canvas echarts 方案,也将在更多的绘图与动画场景中使用 Canvas 组件。
关于本文
作者:@谢杨玲
原文:https://mp.weixin.qq.com/s/fubQyZDecKwd4deymg-SKA
为你推荐
【第1412期】React Native vs. Cordova、PhoneGap、Ionic,等等