背景
今年九月初,王者人生Android端及iOS端正式接入flutter跨平台方案来提升开发效率。
接入flutter之后,我们成功使用flutter上线了首页一起玩赢福利,上线之后,我们的优化工作也一直紧锣密鼓的进行着,其中最为突出的三个问题是【flutter热修复,flutter单引擎,flutter利用原生图片缓存来减少整体内存占用】。
flutter的热更新
着手研究flutter热更新是为了应对现网出现flutter相关的bug好紧急修复,这个在我前面的文章《带你不到80行代码搞定Flutter热更新》中已经提到,这个问题我们目前已经解决了,这里不再啰嗦。
flutter单引擎
着手研究flutter单引擎,是因为对于以原生接入flutter这种形式的项目来说,因为并不是一个纯粹的flutter项目,因此,可能会出现以下这样的导航方式中间过程,而且可能会存在多次。
而且,出现flutter
通过调用原生jsbridge
在开一个flutter
也是有可能的发生的,当出现这样一种情况时,很明显,flutter
会有多个实例,那么我们的flutter引擎
的内存占用是否会有多份呢?
带着这个问题,我研究了一下flutter的启动流程,也记录了一下过程《flutter启动流程简析》,而这个过程让我明白了我们起初的接入方式做不到单引擎,但是如果我们换另外一种方式,可以很巧妙的做到单引擎,下面来探索这个过程。
通过下图,可以看到,FlutterView
存在两个版本,这还是在一个flutter版本中,如图所示:
而我们最初的接入方式是采用的io.flutter.view
包下的FlutterView
。
它是通过Flutter.createView()
创建的,相关部分的省略代码可以参考:
public static FlutterView createView(@NonNull final Activity activity, @NonNull Lifecycle lifecycle, String initialRoute)
。。。
if (nativeView == null) {
this.mNativeView = new FlutterNativeView(activity.getApplicationContext());
} else {
this.mNativeView = nativeView;
}
this.dartExecutor = this.mNativeView.getDartExecutor();
this.flutterRenderer = new FlutterRenderer(this.mNativeView.getFlutterJNI());
this.mIsSoftwareRenderingEnabled = FlutterJNI.nativeGetIsSoftwareRenderingEnabled();
可以看到dartExecutor
,FlutterRenderer
,NativeView
等都是耦合在FlutterView
中的。所以这个FlutterView
在的话,引擎就会一直在。然后,我们关注一下Flutter.createView()
的时候,flutter给加上的生命周期回调函数。
这里重点关注一下onPause
,onStop
,这里为了便于理解,我特意补充了fragment生命周期图。
fragment生命周期
很明显,单我们栈里面出现两个flutter
模块的时候,被覆盖的,就是前一个flutter模块会走
onPause
onStop
注意,这里并没有走onDestroyView
,和onDetach
,往下翻可以看到我贴的日志。
回过头来看看我们之前的接入方式,在生命周期回调中看看,跟进去,你会发现,并没有对DartExecutor
等做释放操作。
所以,以**io.flutter.view
** 包下的**FlutterView
**接入flutter的方式,在有多个**flutter
**实例的情况下,是会出现多分引擎内存占用的,而且因为引擎**代码耦合
**在**FLutterView
**中的原因,我们很难做到单引擎。
所以,我们另外一种接入方式可以做到单引擎吗?
那么,对比使用另外一个FlutterView的接入方式来看,我们发现有一个代理类FlutterActivityAndFragmentDelegate
中,代理的生命周期函数:
有释放引擎的操作,这就意味着,这意味着,如果这个代理类派上用场,那么,如果我们出现两个flutter模块,前一个的引擎是否会释放呢?
用图说话最简单的了,如图所示
所以,biubiubiu
,我将接入方式进行了重构,当然,用上了这个代理类,然后,走走上面的导航流程,用Profiler工具
看看FLutterEngin
实例:
恩,切换到第二个flutter模块,gc一下,看看发现确实只有一个引擎了。做法其实很简单。
- 继承
io.flutter.embedding.android.FlutterFragment
- 写一个引擎提供者
EngineProvider
- 串联起来就OK啦。 部分代码可以参考:
//引擎提供者
public class TipEngineProvider {
public static FlutterEngine obtain() {
return new FlutterEngine(IGameApplication.getIGameApplicationContext());
}
}
。。。。。。
//fragment中-
@Override
public void onAttach(@NonNull Context context) {
FlutterMain.startInitialization(IGameApplication.getIGameApplicationContext());
FlutterMain.ensureInitializationComplete(IGameApplication.getIGameApplicationContext(), (String[]) null);
super.onAttach(context);
}
public FlutterEngine provideFlutterEngine(@NonNull Context context) {
return TipEngineProvider.obtain();
}
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
ShimPluginRegistry pluginRegistry = new ShimPluginRegistry(flutterEngine);
GeneratedPluginRegistrant.registerWith(pluginRegistry);
new TipMethodChannelProxy().init(this, flutterEngine);
}
图片优化,利用原生缓存
第一次爬坑,利用flutter平台提供的PlatFromView,
包装原生的ImageView,做到了利用原生图片缓存,详情可以参考我写的这篇文章
Flutter利用原生控件加载图片,馋原生的图片缓存
在图片较少时,这种方式固然可以,但是一旦出现像列表加载图片的场景,性能问题就出现了,当使用列表加载多张图片时,滑动起来会非常卡。所以PlatformView不适合用于列表,仅仅适合用户页面呈现单一控件的情景,比如地图,比如单个的视频播放器,有很多引用列表展示视频,使用PlatformView实现的那些视频播放插件很显然不适合,我们可以发现,flutter团队视频播放器https://pub.dev/packages/video_player的实现就不是platfomView,是使用的外接纹理。
第二次爬坑,利用Texture 外接纹理。
Google了一下,很不幸,flutter外接纹理渲染图片的demo非常少,仅仅找到了官方的VideoPlayer可以看看源码中联系texture和原生的代码,这里贴出比较重要的部分。
代码语言:txt复制 @override
Widget buildView(int textureId) {
return Texture(textureId: textureId);
}
EventChannel _eventChannelFor(int textureId) {
return EventChannel('flutter.io/videoPlayer/videoEvents$textureId');
}
代码语言:txt复制private void setupVideoPlayer(
EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) {
....
surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);
setAudioAttributes(exoPlayer);
....
Map<String, Object> reply = new HashMap<>();
reply.put("textureId", textureEntry.id());
result.success(reply);
我们可以看到最重要的两行代码是这个:
代码语言:txt复制surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);
参考其原理,那我们要将bitmap渲染上去,是不是只要想办法把bitmap扔给surface,然后在合适的时机手动触发surface的一些回调,比如unlockCanvasAndPost
就可以将bitmap渲染出来,既然视频都可以做样做不卡,一张bitmap应该不会存在性能问题才是,恩,这是理论上的,但是,这方面的这些方面的demo没有找到,不知道如何推进,可以留着后面继续研究。
那么还有其他方式吗?
继续在Google汪洋大海中寻找,发现讲原理倒是一堆一堆的,真正比较关键的地方缺没给出,直到我发现了这篇文章提到了如何去使用flutter的外接纹理,但是其实对于我来说,离贴bitmap有一定的距离,虽然只是贴了一个背景色而已,但是意义非凡,剩下的就是将原生缓存库中的bitmap拿到,并且贴上去就OK了。
所干就干,但是真的那么容易吗?经过研究,发现越陷越深,我不得不去了解一下**OpenGL
**。
Android OpenGLES2.0(一)——了解OpenGLES2.0
Android OpenGLES2.0(八)——纹理贴图之显示图片
大致了解到 纹理映射是将2D的纹理映射到3D场景中的立体物体上 ,然后,OpenGL ES
的世界是3D的,但是手机屏幕能够给我展示的终究是一个平面,只不过是在绘制的过程中利用色彩和线条让画面呈现出3D的效果。OpenGL ES
将这种从3D到2D的转换过程利用投影的方式使计算相对使用者来说变得简单可设置。
textureId->绘制->初始化glcontext->生成贴图->flutter
最终可以看看效果:
其中勾选导航栏按钮,表示使用flutter提供的image来加载图片,不勾选表示使用了外接纹理。
可以看到,这次使用texture外接纹理,渲染图片,在列表加载多图情况下,滑动也非常流畅。
另外,这里也对比一下两种情况下帧率,发现在滑动列表时,外接纹理这种和flutter原生表现一致,基本上是可以满足性能要求的。:
使用外接纹理的方式:
使用flutter原生的Image
目前,插件仅仅实现了Android版本,已经开源了,目前支持webp,gif解析。iOS版本在开发中,相信很快就能出来。
在这个方案的实现过程中,请教过踩过这些坑的同事,特别鸣谢raymondguo,azraellong 。当然,我们的优化之路还将继续进行着,我知道我们并没将这个工作做到极致,只是目前可用而已,遇到新的问题,肯定还需要继续想办法突破。