从土生土长到反应土生土长到飘动

2022-02-14 20:40:40

在Zerodha,我们的旗舰交易平台Kite的第一个移动版本于2015年作为原生安卓应用程序编写。2017年,在React Native中构建了一个跨平台版本后,我们终于决定在2018年对Flitter进行全面重写,这一选择对我们来说回报颇丰。有几个因素和权衡促使这些重写。

这篇文章介绍了我们使用每一个框架的过程和经验,以及为什么我们最终把赌注押在flift上,即使它是最前沿的alpha技术。它还展示了我们的思维过程和第一个基于原则的方法,使两个移动开发者能够构建和维护数百万人使用的多个金融应用程序。

起初,只有Kite web,它是作为Kite Connect API的web前端构建的。2015年的某个时候,我们开始将Kite Android作为本机应用进行开发。有趣的是,当时在印度资本市场上,手机交易基本上不常见,智能手机的普及率与今天相比非常低。由于我们没有开发移动应用程序的经验,我们当时唯一的移动开发者Sujith通过反复试验,在几个月的时间里开发了它。这也是他发明臭名昭著的苏吉特运动的时候™ 算法。第一次公开发布于2016年初。虽然与我们今天的应用相比,它是一款非常基础的应用,但它仍然远远好于当时业界流行的应用。

我们没有立即计划开发原生iOS应用程序,因为我们没有专业知识,而且市场上对iOS应用程序的需求非常少。虽然这一点现在已经发生了很大变化,但这一趋势仍在继续,我们只有约10%的用户使用iOS。然而,为了保持应用商店的基本状态,我们发布了一个iOS版本,将我们的响应式web应用包装在一个webview中。

随着交易平台的发展,原生安卓应用经历了大量的变化和功能添加。我们将首先在web应用程序上发布新功能,收集web用户的反馈,消除漏洞并稳定API,然后将其添加到移动应用程序中。我们从一开始就遵循这一策略,以弥补移动发布流程中增加的开发和测试工作。

随着变化的速度加快,我们开始构建更复杂的功能,我们的开发和测试过程开始放缓,同时出现了越来越多的设备特定问题和OEM问题。这也是我们开始看到iOS用户数量增加的时候,我们认为我们需要一个实际的应用程序,而不是webview框架。

我在2017年初加入了这个团队,大约在这个时候,我们在0.42版本的React Native上开始了我们的实验。与其他基于web的跨平台框架相比,我们选择了它,因为它的最终结果是“原生的”。带有遵循操作系统的UX的本机UI是React native的USP。最终用户确实感觉到了原生用户体验,但只有在它不幸奏效的时候!我们还希望能够重用Kite web中的大量逻辑,尽管当时它是用AngularJS编写的。但在现实中,重用的潜力被证明是非常低的,除了一些稍作修改的数据处理库。

由于我们的iOS应用落后,我们决定首先在React Native for iOS中重写Kite,并最终替换已经开始获得吸引力的本机Android应用。经过5个月的开发,我们于2017年年中发布了用React Native编写的iOS版Kite。与原生安卓应用相比,开发时间大大缩短,这让我们非常惊讶。虽然它是Android应用程序的一个端口,但我们添加了一系列针对iOS特定元素的UI改进。然而,不久之后,React Native开始展示其公平的问题份额。

让我们从JavaScript开始。除了众所周知的语言怪癖,我们还对大量节点模块依赖关系带来的混乱感到失望。时不时会有事情突然发生,唯一的解决办法就是用核弹炸掉整个node_模块缓存并重新安装它们。事情是如何或为什么破裂的往往是个谜。每次我们这样做,macOS Spotlight,一个全系统的搜索服务,就会开始索引node_modules文件夹中的数十万个文件。这个索引问题(纱线#6453,npm#15346)仍然是用户每次手动修复的责任。

一个晴朗的日子,整个Android构建开始失败(react native#19259)。结果是,有人把一个假的React-Native版本上传到了JCenter,Gradle更喜欢它而不是官方版本(React-Native#13094)。这打破了每个人的项目,这些项目的版本与这个假库不同。这很可怕,我们很幸运,因为我们被困在了一个较低的版本上。想象一下,那些拥有相同版本的人,他们会用假库构建自己的应用程序,并在没有意识到的情况下发布。

另一个问题是第三方图书馆的质量问题。由于React Native与本机SDK紧密相连,因此如果存在未内置的API或UI元素,我们别无选择,只能寻找第三方LIB。例如,要使用原生Android复选框,需要第三方库。为了升级Android build和SDK版本(react native Android),不得不放弃每一个依赖项,这是一种痛苦-checkbox@07ac303)或者该项目无法使用更新的React原生版本。

如果我们要从头开始构建其中一个lib,那么在项目中导航和编写本机模块时糟糕的IDE支持会让我们非常困难。它真的让我想起了通过net2ftp编辑PHP文件。希望现在情况有所改善。此外,我们缺乏Objective方面的专业知识,无法将跨越多个Swift版本的Swift代码片段从StackOverflow中转换出来,以及破坏性的语言更改,也导致了这种糟糕的体验。

说到升级,这几乎从未发生过。每次我们尝试时,都会遇到数不清的神秘错误。为了修复商标React本机红色错误屏幕,我们通常必须通过逐步删除应用程序的部分来进行消除,直到我们获得无错误启动,这通常归结为一个错误库,该库使用了一些内部API,该API经历了突破性的更改。如果堆栈跟踪中有某种线索可以精确定位错误就好了。相反,堆栈跟踪通常指向React native的平台本机代码中的某些内容,因此无法追溯到代码或库的部分。

回到iOS上的Kite,尽管开发过程中遇到了困难,但它在用户中取得了成功。React Native在iOS上运行得相当好,这要归功于简洁稳定的朴素UI组件、与其他操作系统一致的用户体验,以及使用React Native的iOS原生JavaScript引擎JavaScriptCore的稳定性能。

然而,当我们试图将其应用到安卓系统时,我们对其性能非常失望,尤其是在中端和廉价智能手机上。最明显的方面是股市行情的更新。作为一款交易应用程序,Kite订阅大量股票行情,每秒多次解析从Websocket连接接收到的二进制数据,进行多次计算,最后在屏幕上呈现这些数字。再加上同时发生的反应、用户交互和UI转换的setState批处理、缺少同步的、低延迟的、延迟呈现的listview,以及JS本机网桥的性能问题,这导致了非常糟糕的用户体验。滴答声从未与本机Android应用程序同步,有时会延迟一秒钟,有时会由于设置状态批处理而以无法理解的速度闪烁。

我们尝试了很多方法来修复它。将渲染范围缩小到文本元素,调整与shouldComponentUpdate的协调,将更新限制到固定的时间范围,使用setNativeProps和只读TextInput显示刻度,以避免setState。我们设法把问题解决到了可以接受的程度。

但是,这只是冰山一角。下一个大挑战是导航。我们使用的是Wix的导航库,虽然它在iOS上工作得很完美,但在Android上却有问题。在经历了许多类似的痛苦之后,最终,我们放弃了用React原生版本替换原生Android应用程序的计划,并将React原生版本仅用于iOS。

与此同时,为了减少大量依赖项带来的持续问题,我们编写了一个高度固执己见、重量极轻的lib,用于状态管理、网络调用、数据解包、,存储持久性等。我们还构建了一个导航库,它只使用硬件加速的转换,而不是由JavaScript控制的转换。后来,我们在React Native中为Coin构建了一个移动应用程序,这些LIB帮助我们轻松构建了一个简洁的UI。由于Coin上的屏幕不像Kite上的实时屏幕那样更新,所以性能没有太大的提升。

尽管React Native存在所有问题,但我们欣赏它的代码推送支持。在某些紧急情况下,你可以通过无线方式更新应用程序的代码包,这一点非常有用,因为在这些情况下,你真的需要推送一个热补丁。虽然这在绝大多数设备上有效,但在一定比例的设备上,代码推送中的回滚系统在更新失败的情况下恢复捆绑包,结果证明是一个问题。有太多无法解释的回滚(react native code push#1488)无法调试。因为我们将此作为修复关键漏洞的最后手段,所以无论发生什么情况,更新都是至关重要的。

其中一些问题现在似乎已经解决了。但是,React Native的新变化似乎非常有希望,比如Hermes取代了JavaScriptCore和新的升级工具。最近在React Native中开发的Varsity应用程序运行良好。因此,对于许多类型的应用程序来说,它仍然是一个可行的选择,只是它没有为Kite做出正确的权衡。

2018年初,我们偶然发现了弗利特。这是一个阿尔法版本,从第一眼看,它类似于闪亮、Nuklear和NanoVG等项目。在尝试过一次之后,它确实感觉像是一个更成熟的项目,而且开发体验也感觉优于React Native。我立即开始在我的个人项目中使用它来掌握诀窍。我花了6天时间回到Visual Basic,在开发过程中,我可以立即看到UI的变化。

但是,有一些摩擦。它需要一种全新的语言,Dart,而这在一开始并不舒服。“hello world”应用的捆绑包大小可能高达5MB。考虑到一款类似的原生安卓应用可以低至6KB,这是一个巨大的挑战。当时可用的Webview插件不是很好。使用Flatter构建的最大的开源应用程序是演示库应用程序,它展示了框架的所有小部件。这不是一个关于如何组织一个严肃项目的好例子。唯一的州管理库是scoped_模型,它与我们以前使用的完全不同。

与此同时,我在Flatter中创建了几个个人项目。一个无限嵌套的待办事项列表,一个用于我的Raspberry Pi路由器的控制用户界面,一个用于我父亲监控从ESP8266 WiFi模块提供的屋顶水箱水位数据的用户界面,一个重写Olam字典应用程序的用户界面,以及其他自动化工具。Flatter为文件IO/HTTP/Raw套接字提供了干净的API,并且易于编写本机插件,因此它可以轻松构建UI,同时也可以轻松构建外部集成。

几个月后,事情开始变得有意义了。我们对Dart、其语法、类型检查和代码组织更加熟悉。结果比我们预想的要容易。IDE支持和文档在语言和框架方面都是例外。使用pub工具进行的包管理也是结构良好且可靠的。多亏了全局包缓存,它没有不必要地占用磁盘空间。

颤振的一个重要方面是其布局机制与我们在React Native中已经熟悉的flexbox的相似性。这使得构思和编写UI布局变得轻而易举。如果我们想创建一些非常定制的东西,我们可以使用图形、布局和物理原语来编写自己的。

在编译器方面,快速启动和调试友好的JIT模式很不错。用于生产构建的AOT模式减少了二进制大小,一致且最佳的性能令人印象深刻。不过,最好的特性是热重新加载,它只是对Dart VM的运行时源代码重新加载机制的一个重新命名。比较React Native的热模块更换(HMR)和Flatter的热重新加载就像是日以继夜。捆绑包的大小并没有像我们最初担心的那样,随着应用程序的大小和复杂性的增加而从基本大小显著增加。基本规模的大部分只是用于国际化的图形引擎、颤振框架和Unicode ICU数据。

然而,在从现有代码库移植序列化程序时,颤振中缺少Dart的运行时反射(镜像)是一个问题。这显然是为了在编译过程中简化树结构,以减少二进制大小。如果有一种方法可以使用某种编译器注释为特定声明启用反射,那就太好了。官方的解决方案是使用代码生成,这也避免了反射对性能的影响。情况仍然如此。

随着时间的推移,Dart语言的频繁改进对于颤振来说是极好的。例如,在类构造函数之前删除new关键字可以减少冗长。默认情况下不可空(NNBD)类型显著减少了空指针异常。外部函数接口(FFI)支持允许Dart与使用C/C++/Rust/Go等构建的本机二进制文件通信。Dart 2.15中的隔离(Dart相当于线程)重用堆,在隔离之间交换数据时减少内存拷贝。这使得它们在应用程序需要进行大量处理的情况下更加有用,这些处理可能会导致渲染延迟,从而导致帧丢失。

然而,在2018,我们考虑使用颤动不是一个简单的决定,更不用说改写风筝了。这将是一项重大的长期承诺,它将在我们的用户群不断增长、我们提供的功能越来越多的时候,占用我们开发人员的大量带宽。更不用说,只有我们两个人在开发移动应用。事实上,今天,仍然只有我们两个人。

与此同时,我们的两个完全不同的代码库,native和React native,正在扩展,维护起来越来越痛苦。如果我们想快速发布功能,并且不落后于web应用程序,我们真的需要统一这些代码库。我们想集中精力,把大部分时间花在应用程序的开发上,而不是担心框架中的问题和外部依赖性。而弗利特开始看起来像是一个可行的选择。

尽管如此,由于Kite是一款关键的金融应用程序,即使是最小的决定和改变也会带来巨大的风险。所以,我们讨论和商议了很多杯乏味的办公室咖啡。我们考虑了所有可能的权衡。我们还认真考虑了押注前沿技术的长期影响,这种技术可能会停止使用。多亏了它是开源的,以及它当时作为alpha软件的良好状态,我们认为即使它被杀了,我们仍然能够在几年内有意义地使用它。

由于这些原因,尽管颤动是阿尔法,我们决定考虑它风筝改写。谢天谢地,我的个人项目也给了我很大的信心。我们已经花了几个月的时间断断续续地试验潜在的项目结构,将一些旧的ReactNative JS库移植到Dart,并构建用于代码生成、调试等的帮助工具,以便更好地理解它。为了进行最后的通话,我们构建了一个功能齐全的Kite用户界面原型,包括正确的导航、加载模拟数据的屏幕,当然还有只花了大约一周时间的模拟市场行情,然后我们进行了压力测试。而且,它一点也不冒汗。那一个被选中了。

我们在2018年年中开始重写。我们的计划是首先替换原生安卓应用,因为这开始成为维护的噩梦,也因为它拥有最大的用户群。我们已经编写了一些用于代码生成和调试的辅助工具,它们加快了重写过程,并使Flatter应用程序在架构上尽可能接近我们的React原生应用程序。只花了大约3个月的时间就可以与我们的生产应用程序实现功能对等。

为了帮助重写,我们构建了一个代码生成器(序列化程序、字符串枚举映射、静态资产嵌入等),以移植React本机应用程序的许多状态管理行为,如果没有这些行为,我们将编写大量样板代码。我们曾尝试使用官方的生成器系统build_runner来实现这一点,但没有简单明了的例子,也没有关于使用它编写我们自己的生成器的指南。此外,它的某些方面还不太理想,比如可怕的嵌套楼梯状YAML配置,我们担心这需要部署我们的常驻YAML忍者@karan“k3n”Sharma来处理。

所以我们回到了第一原则。我们的生成器使用@hints(注释)生成助手函数,比如Java IDE如何为类插入getter和setter。它使用官方的analyzer包,可以解析最新的Dart语法。早期版本使用的正则表达式开始失控。由于代码生成器是Dart中的一个外部CLI工具,因此运行它需要完全冷却Dart VM,这是一个缓慢的过程。为了在构建Dart CLI时提供热重新加载,我们编写了充值。

因为我们正在重写整个应用程序,所以我们认为我们也可以刷新一下用户界面。考虑到Flatter的UI构建功能是多么强大,我们能够在不依赖任何外部LIB的情况下构建我们想要的UI/UX元素。如果我们不喜欢内置小部件的工作方式,我们只需复制它的源代码,调整它,修复导入路径,替换一些变量,就可以了。该框架的源代码可读性强,易于理解,并有大量注释。我们做了几个有趣的补丁:

tab view的一个修改版本,物理特性略有不同,允许在水平页面滚动之后快速进行垂直滚动。在快速浏览某些屏幕时可以观察到这一点。

对许多内置小部件的定制,随着新版本的Flatter的改进,我们逐渐删除了这些小部件。

对Dart标准库中的WebSocket实现进行了修改,以添加对连接超时的支持,这对于预期用于脆弱互联网连接的移动应用程序至关重要。不幸的是,这个问题(web_socket_channel#61)似乎没有得到应有的关注。

此外,@knadh轻视React Native中iOS的默认刷新指示器。我们既不能确认也不能否认这是否是切换到颤振的实际原因。我们很高兴我们现在有一个漂亮的纺纱机。

任何投资平台最重要的特征之一就是财务图表。我们使用WebView提供了两种不同的基于web的财务图表系统。然而,由于整个颤振应用程序都是OpenGL/金属表面上的2D图形,所以它是

......