Flutter性能优化-WebView使用姿势

avatar
@智云健康

本文作者:陶涛,未经授权禁止转载。

背景

​ Flutter从诞生到现在,已经成为了跨端开发的领跑者,国内外越来越多的公司走上了flutter探索之路,我们智云大前端团队从2018年就已经开始尝试使用flutter开发少量页面,经过一年时间的技术探索和沉淀,2019年前端组发起了智云健康app的全量flutter化(基于flutter sdk 1.9.0),经过flutter组小伙伴3个月的努力,智云健康app flutter版本稳定上线。

​ 本文记录了我们在全量flutter化app中遇到的问题,并说明我们是怎么解决这些问题的。

Flutter WebView使用问题

简介

​ 由于flutter 通过将Widget Tree转换为纹理,然后通过Skia实现控件绘制的,从原理上就决定了flutter UI无法直接使用Android/IOS原生的控件,也就无法使用原生已经成熟稳定的WebView、MapView等了。对于这种情况,我们有两种处理方案:

  • 开发一个Plugin,在Plugin里打开一个Activity/Controller,通过Activity/Controller加载并展示原生WebView,这种方案可以完全复用原生WebView的所有能力,也可以根据需求高度定制化WebView,不过存在一个严重的缺陷,无法实现从一个打开的原生WebView再打开flutter页面,重新打开flutter页面被盖在原生WebView下面了,相信使用此方案的小伙伴都遇到过这个问题。(如flutter_webview_plugin就是使用的此方案)

  • 在flutter sdk 1.20之前,官方提供了一个叫PlatformView的东西,Android端叫AndroidView,IOS端叫UIKitView,通过这两个Widget,开发者可以将Android/IOS Native组件嵌入到Flutter的Widget Tree中。(官方的webview_flutter就是使用这种方式实现)

基于业务场景选择Flutter WebView方案

  • 第一种方案,只适用于点开一个新页面打开webview,且webview不需要再打开flutter页面的情况,这种方案的好处就是能避免flutter使用原生控件会遇到的所有的坑。

  • 第二种方案,算是官方对flutter通过PlatformView使用原生控件的一种实践,flutter sdk1.22之前,还只是处于预览状态,不建议上生产,遗留问题很多。

    回到我们具体的业务场景,智云健康app主界面存在四个tab,其中社区、商城为h5页面,需要使用webview加载,其中有一个流程是这样:商场首页->商品购买页->客服聊天页,而客服聊天页为flutter开发的页面。第一种方案明显无法满足我们的需求,所以我们采用了官方webview_flutter方案。

    而选择官方预览版的webview_flutter有大量的问题亟待解决,由此我们开启了漫长的flutter webview问题治理之路。。。

官方webview_flutter原理解析

  • AndroidView(Android)

    方式:在flutter sdk 1.20之前,flutter通过AndroidView将需要渲染的内容绘制到VirtualDisplay的内存中,然后通过对应textureId获取绘制数据渲染在Surface中。VirtualDisplay类似一个虚拟显示区域,通过VirtualDisplay输出纹理,Android端可以将原生控件直接添加到Flutter Widget Tree的层次结构中。

    缺陷

    1. 因为AndroidView是被渲染在VirtualDisplay内存中,当用户点击看到的AndroidView时,其实真正点击到是渲染的Flutter纹理(Texture),用户触摸事件是发送到Flutter View中的,而不是AndroidView,所以这种方式实现的AndroidView是需要特殊处理触摸事件的,而特殊处理造成的问题就是触摸信息存在发送错误或者丢失的情况。

    2. 因为VirtualDisplay所在的位置始终是no focus的,而Flutter中focused的Window通常是实际持有的且并且对于用户是可见的Flutter纹理和Widget,所以这种方式需要特殊处理焦点问题。

    3. 由于前面两点缺陷,造成AndroidView在处理触摸、焦点是非常脆弱,而AndroidView中加入WebView的使用,由于Android WebView具有自己内部的逻辑来创建和设置输入连接,这使得前两个问题变得更加复杂了,出现各种奇怪问题,软键盘弹出异常、文本复制、共享对话框不可用等等

      AndroidView
  • UIKitView(IOS):

    方式:在IOS端,实现方式和Android端不同,采用的是图层混合的方式:flutter有两个透明纹理:一个在IOS原生视图之下,一个在IOS原生视图之上,需要展示在IOS原生视图下面的Flutter UI绘制在下方的透明纹理上,而需要展示在IOS原生视图上面的Flutter UI绘制在上方的透明纹理上,最后将所有图层组合起来就我们需要的界面了,这种方式IOS端同样也可以将原生控件直接添加到Flutter Widget Tree的层次结构中。

    优势:使用的都是纹理组合,仍处于flutter widget渲染逻辑范围,不存在像Android端那边需要特殊处理的问题。

    那为什么Android端不使用这种方式呢?

    原因是在 iOS 上框架渲染后系统会有回调通知,例如:当 iOS 视图向下移动 1px 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 1px

    然而在Android系统上没有任何有关渲染回调的系统API,无法实现同步输出渲染。

  • Hybird Composition(Android)

    方式:从flutter sdk 1.20官方开始推出和IOS PlatformView类似的新的Hybird Composition模式,在此模式下,官方新增了一个FlutterImageView,FlutterImageView本身是一个普通的原生View,它具有混合图层的能力。PlatformView通过把原生控件添加到FlutterView上,然后再通过FlutterImageView去实现图层的混合。

    改进:类似IOS图层组合实现,无VirtualDisplay那种需要特殊处理情况,能解决之前的触摸、软键盘、文本功能。

    hybirdcomposition

使用官方webview_flutter遇到的问题

IOS WebView滚动操作的时候,会出现卡住的情况

现象:在webview_flutter0.0.7版本之前,在IOS端,使用webview_flutter使用WebView加载h5页面,滑动多 次,会出现页面卡住的情况。issues

当时解决方案

  • fork webview_flutter源码,在IOS端增加了一个定时器,每隔几秒去连接并唤醒gesture。

推荐解决方案

  • 将webview_flutter升级到1.0.7,flutter sdk升级到1.22.3,即可解决问题。

Android WebView软键盘弹出不正常、复制文本不可用的问题

现象

​ 在webview_flutter1.0.0版本之前,在Android端,使用webview_flutter使用WebView加载h5页面,在h5页 面点击输入框会出现软键盘无法弹出、无法收起,h5页面中长按文本辅助功能控件不可用的问题。

当时解决方案

  • 在flutter sdk1.20之前,这个问题官方一直没有修复,我们通过fork webview_flutter代码修改了InputConnection的逻辑,暂时解决了这个问题,不过某些文本功能仍然没有解决。
  • 基于此,我们做了另一件事,将主界面tab页的h5页面全部使用flutter重构,首页不再有h5页面,临时规避了webview_flutter一些无法解决的问题。
  • 从flutter sdk1.20开始,官方提供了hybrid composition方案,我们通过修改webview_flutter源码,使其hybrid composition方案,并解决了Android端WebView软键盘弹出问题、文本功能不可用问题。

推荐解决方案

  • 如果使用的是webview_flutter1.0.0之前的版本,建议将flutter sdk升级到1.20,然后修改官方webview_flutter源码,使其支持hybrid composition方案,不过使用此方案会遇到切换后台再重新进入app会闪退的问题,我会在下面说明怎么解决这个问题
  • 如果使用的是webview_flutter1.0.3版本,将flutter sdk升级到1.22.3,即可解决此问题。

Android WebView切换为Hybird Composition方式时,打开WebView页面,应用切换到后台再重新进入app,会闪退的问题

现象

​ 如果使用的是webview_flutter1.0.0之前的版本,然后修改了官方webview_flutter源码,使其支持hybrid composition方案,并且flutter sdk为1.20,那在Android端会遇到打开WebView页面,应用切换后台再重新 进入app,会造成app闪退的问题。issues

当时解决方案

  • 官方开始在Flutterimage中使用两个引用来存储Image对象,这会导致在释放image时丢失对其的引用,最终造成image始终没有被合理释放 官方随后添加了一个list来持有不同的image对象 在清理时遍历list保证历史上存在的image都被合理释放。
  • 过早的在视图树中移除surfaceview 触发了PlatformView::NotifyDestroyed()->Shell::OnPlatformViewDestroyed() 在上一个问题的基础商行会导致app在后台时,Image存储的帧不能被合理地释放并且更新为新的一帧,最后会触发帧队列过载在java层抛出异常崩溃。
  • 根据官方方案,修改FlutterImageView源码后,我们重新编译flutter engine,并使用编译后到engine产物打包app,问题确实解决了。

推荐解决方案

  • 将flutter sdk升级到1.22.3,即可解决此问题。

Android WebView切换为Hybird Composition方式时,反复打开WebView页面,WebView不会回收的问题

现象

​ 使用hybrid composition方案后,虽然解决了交互方面的很多问题,不过却出现了内存泄露,最明显的表现 就是在我们反复打开webview页面时,如果使用chrome://inspect查看webview加载情况,会出现很多个 webview没回收。issues

当时解决方案

  • 从官方issues找到解决方案,不过没有官方只合并到1.22中。
  • 我们根据官方的解决方案,将其改动合并到了我们本地fork的flutter engine中,重新编译flutter engine,并使用编译后到engine产物打包app,问题确实解决了。

推荐解决方案

  • 将flutter sdk升级到1.22.3,即可解决此问题。

Android WebView切换为Hybird Composition方式时,反复打开WebView页面,内存泄露的问题

现象

​ 使用hybrid composition方案后,除了官方本身的bug外,由于对hybrid composition图层混合原理理解不 深,我们在WebView Widget上面覆盖了一个LinearProgressIndicator用来填补WebView加载h5的空白时 间,这也造成了FlutterImageView图层混合时的内存泄露。

当时解决方案

  • 在LinearProgressIndicator展示期间,采用一个Placeholder截图WebView。

推荐解决方案

  • 将flutter sdk升级到1.22.3,即可解决此问题。

webview_flutter 1.0.7打开webview,然后退出webview页面,手机屏幕旋转,app闪退问题

现象

​ 将webview_flutter升级到1.0.7后,在flutter sdk1.22.1之前,打开一个webview页面,然后退出到前一页,然后将手机屏幕旋转,app闪退。issues

当时解决方案

  • 从官方issues找到解决方案,不过没有官方只合并到1.22中。
  • 我们根据官方的解决方案,将其改动合并到了我们本地fork的flutter engine中,重新编译flutter engine,并使用编译后到engine产物打包app,问题确实解决了。

推荐解决方案

  • 将flutter sdk升级到1.22.3,即可解决此问题。

    总结

    ​ 以上是我们在flutter中使用原生webview遇到的所有的问题了,从flutter sdk1.9一直到1.22.3,我们追踪且解决了所有flutter webview使用的问题。不管是自己分析源码临时解决,还是从官方issues找到官方解决方案,我们趟过所有的坑,一直到现在flutter sdk1.22.3,终于迎来好消息,经过验证,我们之前遇到的所有坑官方目前已经全部解决,我们也计划将之前本地定制化的engine回归到官方stable分支上来!