前一阵应用中接入了Flutter
,使用的是官方的Multiple Flutter
Engine管理方案,目前线上运行良好,这里整理一下遇到的问题。
将 Flutter 集成到现有应用整体来说没有什么问题,按照文档的说明结合demo操作就行。接入后多语言,深色模式也可以和原生部分一样正常运行。但还是遇到了一些实际开发中的细节问题。
首屏展示优化
在官方文档中有提到,即使使用了预热的
FlutterEngine
,第一次展示Flutter
的内容仍然需要一些时间。为了更进一步提升用户体验,Flutter
支持在第一帧渲染完成之前展示闪屏页。
我这里遇到的问题是这样,首页有四个Tab,其中第三个Tab是Flutter页面。所以当切换到它时,第一次加载会先白屏一下。
我这里提供两种优化方法。第一种可以使用 CachedEngineFragmentBuilder
中的 shouldDelayFirstAndroidViewDraw()
方法。它表示是否延迟 Android 绘制过程,直到 Flutter UI 显示完毕。
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
.shouldDelayFirstAndroidViewDraw(true)
.build();
使用这个方法有两点需要注意:
- 渲染模式必须使用
RenderMode.surface
。 - 这个时间延迟并不是消失了,只是显示的策略不同。如果是首次点击tab切换,那么点击后会等待Flutter显示完毕后再切换。在低端机上这里会很明显感到停顿。
第二种方法可以使用SplashScreen
来显示一个闪屏页。我们让承载FlutterFragment
的Activity实现SplashScreenProvider
接口的provideSplashScreen
方法。
@Override
public SplashScreen provideSplashScreen() {
return new DrawableSplashScreen(this.getResources().getDrawable(R.drawable.xxx),
ImageView.ScaleType.CENTER_CROP, 300);
}
- 上面的参数是设置闪屏图片,图片的裁剪方式,以及Flutter UI出现时过渡动画的时间。
- 过渡动画生效需要渲染模式必须使用
RenderMode.texture
。
这两个方法各有适用场景,大多数情况下可以使用第一种。由于我们这个页面背景是一张图片,所以可以使用第二种方法。先展示图片,再用动画过渡显示页面内容。可以避免使用第一种方法首次切换时的等待时间。
另外,使用RenderMode.surface
渲染模式目前(Flutter 3.10)有个bug,就是在承载页面onResume
时。FlutterFragment
会突然显示,因为 SurfaceView
在视图层级最顶层。所以覆盖了其他的Fragment页面。目前使用RenderMode.texture
无此问题。
问题跟进具体可以看这里。
Activity与Fragment基本一致,默认使用RenderMode.surface
渲染模式。所以等到页面第一帧渲染完成后才会打开Activity。如果flutter页面背景是张图片,那么首次进入页面时,因为图片的加载有一定时间,所以会闪一下(release相对好一些)。所以也可以考虑闪屏的方案。
以上问题对应源码位置FlutterActivityAndFragmentDelegate
的onCreateView
:
View onCreateView(
LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState,
int flutterViewId,
boolean shouldDelayFirstAndroidViewDraw) {
Log.v(TAG, "Creating FlutterView.");
...
SplashScreen splashScreen = host.provideSplashScreen();
if (splashScreen != null) {
FlutterSplashView flutterSplashView = new FlutterSplashView(host.getContext());
flutterSplashView.setId(ViewUtils.generateViewId(FLUTTER_SPLASH_VIEW_FALLBACK_ID));
flutterSplashView.displayFlutterViewWithSplash(flutterView, splashScreen);
return flutterSplashView;
}
if (shouldDelayFirstAndroidViewDraw) {
delayFirstAndroidViewDraw(flutterView);
}
return flutterView;
}
private void delayFirstAndroidViewDraw(FlutterView flutterView) {
if (host.getRenderMode() != RenderMode.surface) {
throw new IllegalArgumentException(
"Cannot delay the first Android view draw when the render mode is not set to"
+ " `RenderMode.surface`.");
}
if (activePreDrawListener != null) {
flutterView.getViewTreeObserver().removeOnPreDrawListener(activePreDrawListener);
}
activePreDrawListener =
new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (isFlutterUiDisplayed && activePreDrawListener != null) {
flutterView.getViewTreeObserver().removeOnPreDrawListener(this);
activePreDrawListener = null;
}
return isFlutterUiDisplayed;
}
};
flutterView.getViewTreeObserver().addOnPreDrawListener(activePreDrawListener);
}
异常处理
FlutterFragment, IllegalStateException
IllegalStateException: The requested cached FlutterEngine did not exist in the FlutterEngineCache: 'my_engine_id'
at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.setupFlutterEngine (FlutterActivityAndFragmentDelegate.java:280)
at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onAttach (FlutterActivityAndFragmentDelegate.java:189)
at io.flutter.embedding.android.FlutterFragment.onAttach (FlutterFragment.java:1046)
at androidx.fragment.app.Fragment.performAttach (Fragment.java:2922)
at androidx.fragment.app.FragmentStateManager.attach (FragmentStateManager.java:464)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState (FragmentStateManager.java:275)
at androidx.fragment.app.FragmentStore.moveToExpectedState (FragmentStore.java:112)
at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:1647)
at androidx.fragment.app.FragmentManager.dispatchStateChange (FragmentManager.java:3128)
at androidx.fragment.app.FragmentManager.dispatchCreate (FragmentManager.java:3061)
at androidx.fragment.app.FragmentController.dispatchCreate (FragmentController.java)
at androidx.fragment.app.FragmentActivity.onCreate (FragmentActivity.java:276)
异常位置:
void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");
// First, check if the host wants to use a cached FlutterEngine.
String cachedEngineId = host.getCachedEngineId();
if (cachedEngineId != null) {
flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId);
isFlutterEngineFromHost = true;
if (flutterEngine == null) {
throw new IllegalStateException(
"The requested cached FlutterEngine did not exist in the FlutterEngineCache: '"
+ cachedEngineId
+ "'");
}
return;
}
...
}
分析原因应该是页面在后台被回收后,重新打开页面时,在获取FlutterEngine
时发现不存在于FlutterEngineCache
中。因为cachedEngineId
是通过getArguments()
获取的,而FlutterEngine
在onDetach
已经移除。
@Override
public String getCachedEngineId() {
return getArguments().getString(ARG_CACHED_ENGINE_ID, null);
}
所以处理方法就是在FragmentActivity.onCreate
前就判断FlutterEngine
是否存在,不存在时创建。
@Override
protected void onCreate(Bundle savedInstanceState) {
if (!FlutterEngineCache.getInstance().contains("my_engine_id")) {
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
super.onCreate(savedInstanceState);
...
}
- 个别设备出现
UnsatisfiedLinkError
Caused by java.util.concurrent.ExecutionException: java.lang.UnsatisfiedLinkError: No implementation found for
void io.flutter.embedding.engine.FlutterJNI.nativeUpdateRefreshRate(float) (tried Java_io_flutter_embedding_engine_FlutterJNI_nativeUpdateRefreshRate and Java_io_flutter_embedding_engine_FlutterJNI_nativeUpdateRefreshRate__F)
at java.util.concurrent.FutureTask.report(FutureTask.java:123)
at java.util.concurrent.FutureTask.get(FutureTask.java:193)
at io.flutter.embedding.engine.loader.FlutterLoader.ensureInitializationComplete(FlutterLoader.java:221)
Caused by java.lang.UnsatisfiedLinkError: No implementation found for void io.flutter.embedding.engine.FlutterJNI.nativeUpdateRefreshRate(float) (tried Java_io_flutter_embedding_engine_FlutterJNI_nativeUpdateRefreshRate and Java_io_flutter_embedding_engine_FlutterJNI_nativeUpdateRefreshRate__F)
at io.flutter.embedding.engine.FlutterJNI.nativeUpdateRefreshRate(FlutterJNI.java)
at io.flutter.embedding.engine.FlutterJNI.updateRefreshRate(FlutterJNI.java:7)
at io.flutter.embedding.engine.loader.FlutterLoader$1.call(FlutterLoader.java:27)
at io.flutter.embedding.engine.loader.FlutterLoader$1.call(FlutterLoader.java)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:923)
或:
Caused by java.util.concurrent.ExecutionException: java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/xx/base.apk"],nativeLibraryDirectories=[/data/app/xx/lib/x86, /system/lib, /vendor/lib]]] couldn't find "libflutter.so"
一般是在创建FlutterEngine
或FlutterEngineGroup
时出现的,目前也没有较好的方法,但是可以捕获一下此类异常,做一些兜底操作。避免直接崩溃影响用户使用其他功能,这类问题占比很少,目前仅有两个用户上报了此异常。
此问题跟进可以看这里。
热重载
在调试混合开发模块时(Flutter版本3.10.x),发现当存在多个Flutter页面时(使用FlutterEngineGroup
创建),热重载会使App卡死。我找到了相关问题,我尝试使用beta 3.13.0版本发现此问题已解决。等待stable的发布。
打包
众所周知Flutter的debug模式性能表现一般,所以在交给测试时,为了避免一些体验问题。我们可以将Flutter模块打包成release。
如果使用依赖 Android Archive方式集成,可以直接使用flutter_release
包。如果直接依赖模块的源码,可以在直接修改flutter/packages/flutter_tools/gradle/flutter.gradle
的源码:
/**
* Returns a Flutter build mode suitable for the specified Android buildType.
*
* The BuildType DSL type is not public, and is therefore omitted from the signature.
*
* @return "debug", "profile", or "release" (fall-back).
*/
private static String buildModeFor(buildType) {
if (buildType.name == "profile") {
return "profile"
} else if (buildType.debuggable) {
return "debug"
}
return "release"
}
将上面的"debug"改为"release"就好。iOS在flutter/packages/flutter_tools/bin/xcode_backend.dart
中修改。
当然直接修改显得不是很优雅,所以可以写个打包脚本处理这一操作。例如用Dart实现如下:
// 读取文件内容
File file = File('xxx\flutter\packages\flutter_tools\gradle\flutter.gradle');
String content = file.readAsStringSync();
// 修改文件内容
String newContent = content.replaceAll('return "debug"', 'return "release"// weilu');
// 将修改后的内容写回文件
file.writeAsStringSync(newContent);
执行完后,还原即可。
其他
- BUG in [v3.10.0] FlutterViewController memory leak when add an existing app, Multiple Flutter instances
- [Android, SystemChrome, FlutterActivity] Status bar becomes semi translucent when using a pre-warmed Flutter engine
后面如果有遇到新的问题,也会同步记录到这里。
参考
- multiple_flutters
- 文档 - 展示闪屏页