引子
在长桥 iOS App 上,如果有用户刚好是 iOS 开发者,并且在 iOS 的开发者设置中开启了“显示HUD图形”和“记录图形性能”选项,那么在进入股票详情页时,就会在 K 线图表上看到一个浮层,内容包含了 FPS、GPU 内存、GPU 使用率等数据。
看过手机游戏性能评测的用户可能会对这个浮层有点熟悉,通常这个是用来展示游戏帧率以及 GPU 压力的,但是长桥 App 在这个查看股票行情的场景下,怎么会跟游戏一样呢,难道是集成了一个 Unreal 引擎吗?
当然长桥 App 并没有集成一个 Unreal 引擎,这一切就得从 K 线图表组件的迁移升级说起。
源起
在 2022 年中时,长桥 App 已经发展了不少时间,整个 App 已经颇具规模,内容越来越丰富,功能也越来越强大,而股票行情相关业务,更是这其中的重点部分,而 K 线图表组件,则是行情业务中承载最多业务功能的组件。
在当时,整个 K 线图表组件在面临越来越多的产品需求时,已经呈现了疲于应对的现象,每次新增的功能都需要花大力气去开发,并且最终实现效果、iOS 和 Android 两端一致性也得不到保障,这使得我们不得不去剖析,K 线图表组件所面临的问题,以及需要确定一个解决方案。
问题
业务复杂度变高
在行情业务模块逐渐完善的过程中,K 线图表组件也会面临越来越复杂的需求,例如复权、公司行动、财报事件、盘前盘后等,这些需求不光是在数据处理上提出了更高的要求,也对图表的渲染提出了更多的需求,原有图表组件的架构,都是基于单个需求独立去开发的,面对这些需求时,在灵活性上的欠缺,会导致整体需求和功能的推进相当缓慢,并且在需求测试时,暴露比较多的 bug。
两端重复开发
还有另外一个问题,在当时,iOS 和 Android 的 K 线图表组件是分别使用各端的 Native 开发框架开发的,这就导致在面临越来越多的新需求时,两端就必然存在相当大一部分重复功能,数据需要重复计算、图表时间段的渲染需要重复实现,这在业务快速发展过程中,会让图表组件相关功能的开发拖慢整体需求的交付。
指标计算与渲染
与此同时,对于技术分析图表来说,技术指标是非常重要的一个组成部分,缺少技术指标,K 线图表就失去了很多从技术面分析行情的能力。但是对于之前使用的图表组件来说,指标是在各个端使用原生编程语言实现的,例如在 iOS App 中,使用 Objective-C 实现了例如 MA、EMA 等技术指标的计算和展示,这会导致以下问题:
- 跨平台之间无法复用,Android 客户端需要使用 Kotlin 重新实现一遍,工作量冗余不说,在开发过程中还容易出现对于公式理解不一致导致最终计算结果不一致的问题。
- 新增和修改指标不方便,由于使用编译性的语言来编写技术指标,这会导致在添加新的技术指标时,依赖于 App 新版本的发布,并且如果指标计算过程中存在问题,也无法及时修正。
性能瓶颈
在老的图表库中,所有的渲染操作都是使用 CPU 指令进行的,例如在 iOS 上,绘制操作是在 UIView 的 drawRect 方法中进行,这就会导致,如果 K 线图表中的元素过多时,图表的渲染会占用大量 CPU 时间,这就会导致其他主线程操作所能使用的 CPU 时间变少,在极端情况下,如果整体渲染时间超过了 16ms,那就会导致页面渲染 FPS 达不到 60 FPS,对于用户来说,最直观的感受就是 App 变卡了。
并且从 iPhone 13 Pro 开始,iPhone 就支持 ProMotion 技术,刷新率可以最高达 120Hz,这时对于 CPU 执行时间的要求就更高了。
探索
在明确问题之后,我们就可以开始寻找解决方案,对于问题一,业务复杂度变高,这是无法避免的,但是可以通过解决问题二,两端重复开发的问题,来让复杂业务只开发一次,即可以减少开发工作量,也可以保障两端业务逻辑实现一致,一举两得。
跨平台
为了解决两端重复开发的问题,首先想到的就是跨平台解决方案,目前可以选择的跨平台方案也挺多,但是适合于集成到 App 中作为一个组件来使用的方案并不好找。
H5
H5 应该算是最容易作为组件集成,以及最容易开发的技术栈了,在 iOS 和 Android 都可以通过在一个视图嵌入一个 WebView 来实现集成 H5 组件。
但是使用 H5 也会有明显的问题,从而与目标不太匹配:
- WebView 的性能不符合预期,图表内容丰富之后,会包含大量 DOM, 以及用户在添加多个指标后,整体绘制压力会相当大,以及 WebView 本身加载时间较长,会存在一定的白屏时间。
- 使用 WebView 渲染图表会使得图表组件的手势处理变得更加复杂,在整个页面包含多种纵向以及横向滑动手势时,如何解决 WebView 内部的手势与外部的手势冲突将面临比较大的挑战。
React Native
React Native 作为一个流行度比较高的跨平台解决方案,也在考虑范围内,但是它与 H5 方案一样,也存在一些问题:
- React Native 在各端仍然需要各自实现特定组件。
- React Native 在数据量较大时,仍然存在一定的性能问题。
- React Native 的桌面端支持和 Web 端支持还不太完善。
虽然 React Native 已经有一些现成的图表库,例如 react-native-echarts,但是这些图表库是面向通用场景开发的,支持各种类型的图表,在 K 线图表这个领域,需要做大量的二次定制开发工作,以及界面主题的适配工作,从开发效率上来说,并没有额外优势。
Flutter
Flutter 虽然跨平台,性能也还可以,但是通常是作为一个完整的页面去嵌入到应用中,这一点就没办法使它作为一个组件来使用,以及 Flutter 在桌面端以及 Web 上的跨平台支持还不太完善,在筛选阶段就直接放弃了。
ImGui 调研
在调研过程,还发现了一个新的 GUI 编程概念,Immediate mode GUI。
An immediate mode graphic user interface (GUI), also known as IMGUI, is a graphical user interface design pattern which uses an immediate mode API to render controls, as opposed to a retained mode one.
相比传统 UI 开发框架,它最大的特点就是不维持控件状态,每次渲染都是将所有控件完整的渲染一遍,通常用在游戏领域,用于显示设置界面等。它的典型渲染流程如下图所示:
在 Immediate mode GUI 领域中,比较有名的框架是 Dear ImGui。
Dear ImGui is a bloat-free graphical user interface library for C++. It outputs optimized vertex buffers that you can render anytime in your 3D-pipeline-enabled application. It is fast, portable, renderer agnostic, and self-contained (no external dependencies).
注:为叙述方便,后续 ImGui 都将指代为 Dear ImGui 框架。
在看到 ImGui 后,把它的 examples 浏览了一下,看到它目前已经适配了相当多的平台和渲染框架,例如在 Apple 上的 Metal 以及 OpenGL,Android 上的 OpenGL3,Windows 上的 OpenGL 和 DirectX 等等,这跟我们在寻找解决方案时的目标比较匹配,相比其他几个选项,ImGui 具有以下几个特点:
- 使用 GPU 渲染内容,相比使用 CPU 去绘制内容,性能底线会更高。
- 使用各平台支持最好的渲染框架来渲染内容,例如在 macOS 和 iOS 上使用 Metal 框架,理论上可以充分利用各平台的 GPU 性能。
- 整个 ImGui 使用 C++ 编写,可以很方便的移植和兼容各个平台和操作系统,在 Web 上,也支持使用 Emscripten 编译为 WASM 来运行。
再来探索一下 ImGui 的 API,在示例中找到 examples/xample_apple_metal/main.mm,看看一个简单的窗口是如何渲染的:
可以看到整体 API 相当简单,在每次渲染的循环中,根据实际需要展示的控件和内容,按顺序去渲染就可以了,ImGui 会自动去控制布局。
但是对于一个图表组件来说,这些控件并不是必需的,再看一下它的示例 App 中,有没有其他绘制方面相关的内容。
ImGui 绘制能力
接下来我们需要看一下 ImGui 是否支持 K 线图表组件需要的各种绘制能力,K 线图表中的各种元素,抽象出来就是一些多边形、线、图形等元素,只要支持了这些元素,就可以认为它具备了作为 K 线图表渲染框架的能力。
示例 app 中有一个 Custom rendering 示例,在默认 Tab 中,可以看到 ImGui 支持渲染各种自定义图形,并且支持设置各种参数,这已经跟图表绘制的基础元素比较接近了。
再在 Canvas Tab 中可以看到,ImGui 具备完整的画布能力:
因此,从这两个示例中可以看到,ImGui 完全可以作为一个 K 线图表的底层绘制支撑组件来使用,要开发一个跨平台的 K 线图表组件,我们已经解决了一半问题,通过 ImGui 来完成跨平台的渲染,剩下只需要解决具体的业务需求即可。
实现
在确定渲染框架之后,就可以开始真正开发整个 K 线图表组件了,整个 K 线图表组件包含了三块内容:
- 数据处理,主要用于处理从服务端接口获取的数据,处理成绘制组件和指标引擎能使用的格式。
- 绘制,定义了一套图表绘制所需要的数据格式,这样在数据处理完后,可以直接根据所定义的数据格式来实现绘制折线图、蜡烛图、柱状图等。
- 指标引擎,通过指标脚本对处理过的数据进行计算,得到指标渲染需要的数据,最终交由绘制组件进行绘制。
整体架构
整体 K 线图表组件将业务逻辑与图表渲染解耦,这样两部分可以独立开发,并且互不影响。指标引擎作为一个第二方组件引入,定义好与数据处理部分的接口后,就可以任意替换,实际上在整个图表组件的发展过程中,指标引擎就已经做过一次更换,而这个更换对于上层应用来说,是完全无感的。
另外,为了更好地适配各个平台的使用体验,在实际实现过程中,还根据每个平台的特点,将用户 UI 交互部分的内容单独进行封装,从而给用户提供更贴近平台特色的使用感受。
iOS/macOS
在 iOS 及 macOS 上,后端渲染使用 MetalKit,用户交互使用 UIView 以及 UIGestureRecognizer 进行捕捉,在由 K 线图表组件中的用户事件处理模块处理后,反应到对应图表内容的变化,最终交给 MTKView 或 CAMetalLayer 进行渲染。
Android
在 Android 上,整体图表组件都以 C++ so 库的方式来引入,后端渲染使用 OpenGL,通过 GLSurfaceView 呈现给用户,在交互上通过 TouchGestureDetector 进行捕捉。通过 JNI 从 Java 层传递到 C++ so 库中,最终通过 OpenGL Context 进行渲染。
另外,GLSurfaceView 在初始化中,会自动启动一个 GLThread 线程,后续所有图表的整体操作及 JNI 的操作全部都会在 GLThread 线程中进行,这样图表的数据处理和渲染就不会占用主线程的 CPU 时间片了,从而使得主线程可以获得更多 CPU 时间用于处理其他事件,例如页面中其他控件的响应和绘制,从而给用户提供更好的响应和体验。
桌面端/Web
得益于 Emscripten 可以将 C++ 代码编译为 WebAssembly,我们的图表组件也可以直接通过 WebAssembly 部署到浏览器中使用,由于目前长桥桌面端是基于 Electron 进行开发,因此整个 K 线图表组件就可以同时支持桌面端和 Web 的图表业务。
同时由于整个图表的渲染过程是在 WebAssembly 中进行,因此图表整体的性能相比使用纯 HTML 实现会有一个比较大的提升,在测试过程中,即使加载了 10 年的日 K 线数据,拖动图表也不会有卡顿。
性能表现
这里以 iOS 为例,可以看到在苹果这支股票上,加载了 20 年的日 K 线数据,并且设置了 10 个技术指标,在滑动时,仍然可以保持接近 60 FPS 的刷新率:
应用
目前,长桥新的 K 线图表组件已经应用在多个平台和应用中,包括长桥 iOS App、长桥 Android App、长桥桌面端 Longbridge Pro 等,在股票详情页中,图表部分已经全面替换为使用新的图表组件。
5 日分时
日 K 线
桌面端
总结
经过一年多的开发、灰度测试,目前新的 K 线图表组件已经完整上线,长桥技术团队致力于给用户带来更好的使用体验,如果在使用过程中碰到任何问题,也可以直接评论或私信反馈。
号外
Longbridge 长桥是来自新加坡的新一代社交型券商,成立于 2019 年 3 月,于新加坡、香港、美国及新西兰等全球四地获批逾 21 张金融合规牌照或资质。长桥致力于金融科技的创新应用,助力每一个人都能以更低门槛、更低费率获得更加丰富的全球投资体验。
欢迎下载使用体验:长桥 iOS & Android & 桌面端 App 下载