闲鱼基于Flutter的移动端跨平台应用实践
IsobelCovin
6年前
<h2>闲鱼为什么使用 Flutter</h2> <p>Flutter 作为 Google 新一代的跨平台框架,有较多的优点,但跟其他跨平台解决方案相比,最吸引我们的是它的高性能,可以轻松构建更流畅的 UI。虽然各跨平台方案都有各自的特点,但 Flutter 的出现,给闲鱼、给大家都提供了一种新的可能性。</p> <p>那么,Flutter 为什么会有高性能呢?</p> <p><img src="https://simg.open-open.com/show/d264b527d08bacaffee5e23c3d6a6323.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2336" height="1266"></p> <p>首先,Flutter 自建了一个绘制引擎,底层是由 C++ 编写的引擎,负责渲染,文本处理,Dart VM 等;上层的 Dart Framework 直接调用引擎。避免了以往 JS 解决方案的 JS Bridge、线程跳跃等问题。</p> <p>第二,引擎基于 Skia 绘制,操作 OpenGL、GPU,不需要依赖原生的组件渲染框架。</p> <p>第三,Dart 的引入,是 Flutter 团队做了很多思考后的决定,Dart 有 AOT 和 JIT 两种模式,线上使用时以 AOT 的方式编译成机器代码,保证了线上运行时的效率;而在开发期,Dart 代码以 JIT 的方式运行,支持代码的即时生效(HotReload),提高开发效率。</p> <p>第四,Flutter 的页面和布局是基于 Widget 树的方式,看似不习惯,但这种树状结构解析简单,布局、绘制都可以单次遍历完成计算,而原生布局往往要往复多次计算,“simple is fast”的设计效果。</p> <p>下面截图是目前闲鱼已经上线的商品详情页面:</p> <p><img src="https://simg.open-open.com/show/22f269f9ca906322f4824505a1aa8d02.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2266" height="1252"></p> <p>商品详情页包含混合栈、视频、动画、原生组件、多图、留言盖楼等功能,页面较复杂,有代表性,也是闲鱼最重要的页面之一。选择商品详情页做为第一个 Flutter 页面,是闲鱼能成功快速使用起 Flutter 的重要因素。</p> <p>接下来介绍一下,闲鱼的实践过程和总结。</p> <h2>Flutter 与 Native 混合开发实践</h2> <h3>Flutter Hybrid 工程实践(研发时)</h3> <p>我们把 Flutter 和闲鱼现有的 APP 做渐进式的整合,App 中会同时有 Native、Flutter 和 H5 页面。现有的 Flutter Demo 和应用,都是独立的 Flutter 应用,而当把它和 Native 混合的时候,会碰到很多的困难。</p> <p>首先是研发时的问题,怎么让 Flutter 在现有的 Native 工程中开发起来。这个要从这张图说起:</p> <p><img src="https://simg.open-open.com/show/6d0b0f6b58ec0efb9954aff8617c5c38.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2296" height="1296"></p> <p>闲鱼 Flutter 工程结构如图,三个蓝色背景的目录分别是安卓工程、iOS 工程和 main.dart 入口。编译产物中以 iOS 为例,APP Framework 是 Flutter 应用页面代码,Flutter Framework 是 Flutter 引擎。</p> <p>这个过程,需要重点考虑几个问题:如何基于现有工程搭建混合工程?如何支持过渡期的 Flutter 开发及纯 Native 开发的双开发模式?如何让 Flutter 与现有持续集成、构建工具集成?</p> <p><img src="https://simg.open-open.com/show/3539ccdce51bfe9a2ecb4489ab03d47d.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="1998" height="908"></p> <p>首先,现有的 Native 工程并不符合 Flutter 默认的规范,两者不能完全匹配,需要修改打包脚本,甚至修改 Flutter 的打包 Tool 来解决。另外,我们通过 Submodule 将现有⼯程引入到 Flutter 父工程中。</p> <p><img src="https://simg.open-open.com/show/f825d632200097bbc13c851ed43b7d7e.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2356" height="1234"></p> <p>纯 Native 开发同学,不需要引入 Flutter 工程,直接在 iOS 或 Android 工程下开发,Flutter 以产物的方式集成到 Native 中运行,Flutter 的开发同学引入 Submodule。</p> <p><img src="https://simg.open-open.com/show/e1cb361a2a6c9e2fedd7a47545ce5b48.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2068" height="1336"></p> <p>上图是工程上的修改点。绿色虚线部分是 Flutter 默认的结构,红色虚线是闲鱼在 Flutter 基础上做的定制。Flutter 的构建工具 gen_snapshot,会把业务代码,Flutter 框架、引擎编译成中间产物,以 so 或 Framework 的方式变成 Native 的一部分。</p> <p>几个主要的改动点:</p> <p>第一,构建私有的仓库,用来管理阿里私有包,如 CDN、无线网关等中间件适配 Package。</p> <p>第二,构建工具和引擎的优化。</p> <p>第三,跟现有的构建工具打通,混合调试等。</p> <p><img src="https://simg.open-open.com/show/cd813acd73ee749b80e5a481dc85c773.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2336" height="1328"></p> <p><img src="https://simg.open-open.com/show/d3bdd79c904b6fbd57b44b4bd943165d.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2304" height="1266"></p> <h2>Flutter Hybrid 栈管理</h2> <p>除了上述的研发时问题,接下来就是让它跑起来,解决运行时问题。其中最重要的是实现混合栈。</p> <h3>混合栈的定义</h3> <p><img src="https://simg.open-open.com/show/2d4f136ace3eeecf667ae576b6cd3c72.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2292" height="1322"></p> <p>在混合工程中,Native 页面,Flutter 页面之间会以多种可能的顺序混合入栈,出栈。要怎么去做?先看一下 Flutter 内部栈的管理默认下是怎么做的:</p> <p><img src="https://simg.open-open.com/show/cca382a5f732dcf46a851520eb6e01cc.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2282" height="1342"></p> <p>整个 Flutter 运行在一个单例的 Activity 容器里(用安卓举例),Flutter 内部的所有页面都在这个容器中管理。 对安卓来说,怎样把这样容器里面的栈与 Native 栈混合起来,直接的一个想法就要把栈自己托管起来,把这个容器在 Android 的栈中来回移动。但 Android 里想这样操作非常难。</p> <p>所以解决这个事情,就主要有两个问题要考虑,首先就是混合栈要在哪里管理?是在 Hybrid 栈管理,还是在 Flutter 管理,第二个就是关于实例剥离的问题,既然移动单例很复杂,那就把单例剥离出来,在上面 Wrap 出多个实例,这样就方便管理了。 下面是两个对比方案。</p> <p><img src="https://simg.open-open.com/show/f491b5ea5f7ea690b6ece3cf8749b695.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2272" height="1316"></p> <p>这两种方案都是可选的,方案一就是把 Flutter 直接变成多例,每个 Flutter 页面重新启动一个 Flutter 的容器,每个 Flutter 页面就像通常使用 WebView 一样,这个方便我们做了实测,发现它的启动速度有影响,能感觉到一些卡顿,另外,还有一个问题,当我想在两个页面之间去复用数据的时候,那两个引擎之间是完全隔离的,最后数据不好复用。 这个方案的好处是很简单,如果喜欢隔离性,也可以变成优点。</p> <p>第二种方案,就是做浅层的单例剥离,尽量多的遵守 Flutter 的标准运行方式,以最小的影响把单例剥离出来,Wrap 成多例。</p> <p>这种方案是在 Flutter View 这一层剥离,关于 Flutter View 的概念看一下源码很容易理解。</p> <p>这种解决方案的好处是可以实现多页面复用,因为不用每次都取一个新的实例,加载速度会更快,因为对闲鱼来讲,我们追求的就是性能,最后我们的选择就是方案二。</p> <p>这个是具体的实现方式:</p> <p><img src="https://simg.open-open.com/show/832e653026530ea96af5c18a82903334.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2320" height="1308"></p> <p>把下面的 View 复用,在多个 Activity 之间移动,切换到下一个页面的时候,把这个可复用的 View 从前一个 Activity 移走,放到下一个 Activity,这是它的主要的思路。</p> <p>在这个思路下也会遇到一些需要解决的问题:</p> <ul> <li>两个页面转场动画由于 View 在 Activity 间移动,会有一个短暂的白色闪屏,体验不好,解决闪屏的办法,就是做一个截图,从 A 页面到 B 页面的时候,对 A 页面做个截图,同时把 Flutter 自带栈的转场动画禁止掉,有这个截图,转场时就不会有闪屏的感觉了。</li> <li>考虑对统一 OpenUL 支持,把 Flutter 和 Native 的 URL 统一。</li> <li>由于 Flutter 容器内部有个栈管理,对这个栈需要与 Native 做同步的跟随。</li> </ul> <p>到此,混合栈的方案就简单介绍完了。</p> <h2>基于 Texture 的自定义视频播放器</h2> <p>接下来,如果 Flutter 页面中想复用已有的 Native 组件,怎么办?</p> <p>一种情况是视频播放器,Native 中我们做过很多优化的播放器,希望能复用到 Flutter 页面中。</p> <p>首先,还是先看原理:</p> <p><img src="https://simg.open-open.com/show/2317a0649ffebb6186d6220895f06c22.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2330" height="1276"> Flutter 内部的渲染,与通常的做法一样,有 layer。其中一种 Layer 叫 Texture Layer,可以把任何其他地方计算出来的纹理直接贴到 Flutter 的 Texture Layer 上。不管是视频,还是图片,如果有需要,都可以用 Texture Layer。</p> <p><img src="https://simg.open-open.com/show/d1d1fbf8234ffb644f11c29a842fe575.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2344" height="1274"></p> <p>在这个实现的方式中,Flutter 侧负责展示这个播放器 UI,接收对播放器做控制交互,而 Native 侧负责视频的渲染,通过 TextureLayer 展示到 Flutter 侧。而控制协议,通过 Flutter 特有的 MethodChannel 来控制。</p> <p>除了视频,还有没有其他类型的 Native 组件能复用到 Flutter 中?像下图这样,把 Native 控件放在 View/Window 中与 Flutter 混合,是可以的。但截止演讲时,Flutter 还无法做到在 Flutter 中挖个小天窗嵌入 Native 组件。不过这个方式 Google Flutter 团队已经在做尝试,未来可能做有办法支持,大家可以关注。</p> <p><img src="https://simg.open-open.com/show/6339df227bc072f79c7d90697e6656eb.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2348" height="1342"></p> <h2>Flutter 通用问题实践</h2> <p>接下来,介绍一下 Flutter 商品详情页的页面的开发框架。</p> <h3>页面框架</h3> <p><img src="https://simg.open-open.com/show/55d29448fe824097158cc8949fca7c17.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2316" height="1266"></p> <p>右边边绿色的这一部分,就是整个页面的结构,整个详情页面是一个大列表,由商品的描述、图片,评论,个性化推荐等组成。这里简单概括几个特点:</p> <ul> <li>通过 Server 端返回的数据驱动 UI 界面,可以一定程度上获得页面内容的动态能力。Flutter 本身不支持动态更新,无法像 JS 那样,所以这种设计方式可以一定程度上弥补这方面的短板。</li> <li>Widget 树结点间(或者说页面的不同组件间)的数据如何共享?这里大家知道 InheritedWidget 这个类就好了,这是解决数据共享的很有用的类。</li> <li>如果页面再复杂些,有很多交互,希望将视频、交互、数据等分离怎么办?也可以考虑引入 Redux 框架。</li> </ul> <h3>统一协议</h3> <p><img src="https://simg.open-open.com/show/7dc2aa95f06b52a02ea9196e7da2626b.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2350" height="1328"></p> <p>Flutter 不支持 Dart 的反射(mirror),所以在开发 Flutter 页面时,解析服务端返回的数据,生成 Flutter 对象时,可能会很不习惯,需要有较多的硬编码。 Flutter 不支持反射,请大家理解,这样可以获得 tree shaking 能力,减少 Flutter 包的大小。</p> <p>既然不支持反射,怎么去解决刚才说的数据转换问题?我们实现了一个统一协议层,把 Serve 端和客户端的请求接口和数据模型,都通过协议统一生成代码,避免了手工编码。</p> <h3>图片缓存方案</h3> <p><img src="https://simg.open-open.com/show/887cd091d059de7c29152f009f7c0bcd.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2288" height="1272"></p> <p>闲鱼的页面中有大量图片,但 Flutter 默认的图片缓存策略比较简单,截止演讲时,如上图所示,默认图片缓存策略是按照图片数量,以 1000 为上限,LRU 的方式置换。当大图片较多时,这会占用过多的内存,容易造成 Crash 或 Abort。</p> <p><img src="https://simg.open-open.com/show/6919baa0339a3cd04c55acbc72773d4e.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2296" height="1312"></p> <p>在我们只有详情页一种页面时,解决这个问题可以用简单粗暴的方式,首先把 1000 这个数量调小。一种修改方式如图所示,通过 WidgetsFlutterBinding 来修改(WidgetsFlutterBinding 是 Flutter 中很重要的一个机制,有兴趣可以深入了解)。</p> <p>此外,还要注意图片尺寸自适应剪裁,支持 WebP 等,这些对节省图片内存和网络流量都很关键。</p> <p>第二种解决方案,是官方正在做的优化,按照整个空间的大小来做缓存策略,具体可以关注图中的链接。</p> <p>第三种方案,更加完善,加一层持久层的缓存,以实际的经验来看,闲鱼的场景下,持久层缓存时,通常可以提高缓存命中率 10% 到 30% 。</p> <h2>上线效果</h2> <h3>线上 Crash 率</h3> <p><img src="https://simg.open-open.com/show/77bd599360133acb3370dcba4da06624.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2300" height="1316"></p> <p>大家可能会关心 Flutter 在生产环境的稳定性,兼容性等表现。闲鱼使用 Flutter 的前期阶段,这方面确实有很大的问题。前期在真实环境中发现了很多问题,第一次灰度测试时 Crash 率有百分之一的量级,主要的 Crash 问题包括内存、GPU、icu data、视频播放、截图接口、armv7、字体缺失等。</p> <p>我们和 Google 团队一起,通过几个版本的灰度迭代,用了一个半月的时间,把问题逐步解决了,目前 Crash 率收敛稳定,达到万分之一的量级,已经达到了生产标准。</p> <h3>Flutter 与 Native 详情页性能对比</h3> <p>我们对 Flutter 与 Native 的详情页做了简单的性能对比,并不严谨,仅供参考。</p> <p>测试场景:进入宝贝详情页后快速浏览到页面底部,从猜你喜欢进入第二个宝贝,重复进行访问 10 个不同宝贝详情。对比 Native 版详情页和 Flutter 版详情页。</p> <p>测试机型,以低端机型为主(高端机型区分不明显):</p> <p>Android 4.x, 5.x...</p> <p>iPhone 5c, 6s...</p> <p>安卓的对比:</p> <p><img src="https://simg.open-open.com/show/8fd9d28fb123ad856435206c19b73d91.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2296" height="1274"></p> <p>下面两行是体现流畅度的,LPS 或者 MS 是腾讯提出的一种流畅度的表达方式,在流畅度是 OK 的,比 Native 详情页做的好,在技术的指标上也还不错。</p> <p>iOS 的结果:</p> <p><img src="https://simg.open-open.com/show/abad0ddc38d988031839b696408b84f9.png" alt="闲鱼基于Flutter的移动端跨平台应用实践" width="2282" height="1218"></p> <p>iOS 上,也是 Flutter 会更流畅一些,测下来,发现在 GPU 的使用率上,Flutter 会更高一些,Flutter 在这上有更进一步的优化空间。</p> <p>说到这里,可能大家也会疑惑,这个对比结果,是不是因为以前 Native 写的详情页太复杂了? 确实有这种可能。但主要分享的是,两种页面是相同团队成员开发的,并且没有针对 Flutter 做专门的性能优化,这个性能测试可以确定的结论是,使用 Flutter 还是比较容易就能开发出与 Native 性能相近的页面。</p> <p>最后,说一下大家可能会关于的成本问题。对于混合开发,初期接入成本是有的。如果是全新的 Flutter 独立应用,接入成本会很低。首次接入完成后,后面开始会顺利很多,可以享受跨端统一编程,一套代码带来的效率快感。另外,关于学习成本,还好,因为 Dart 语言跟 Java 很像,跟 JS 也很像,另外 Flutter 的 UI 框架遵循响应式,声明式设计原则,个人感觉,较容易上手。谢谢大家,由于水平有限,可能会有错误,请大家指正。篇幅有限本文无法对每个细节深入探讨,关于细节的深入分享,欢迎大家关注“闲鱼技术”的公众号。</p> <h2>作者简介</h2> <p>王树彬,阿里巴巴闲鱼无线技术专家,毕业于浙江大学,2009 年加入阿里巴巴,现任阿里巴巴闲鱼架构负责人,负责闲鱼从端到云的整体架构升级。有十余年互联网研发经验。曾负责移动端 LBS 技术,是淘宝位置归一、地理围栏等技术的开拓者,为个性化、O2O 等业务提供基础能力。也曾负责淘宝的商家系统,建立商家十亿级大数据下的实时在线查询、挖掘服务。</p> <p>感谢覃云对本文的审校。</p> <p> </p> <p>来自:http://www.infoq.com/cn/articles/xianyu-cross-platform-based-on-flutter</p> <p> </p>