在Flutter&Native(本文针对Android与iOS)混开中,FlutterEngine是十分重要的概念,了解其作用与流程灰常重要的。
本篇笔记以直接看注释、选择性看源码的方式来了解FlutterEngine,这样子不会太枯燥,知识获取快;但是不够深入,各有取舍吧。
笔记中的FlutterSDK版本是v1.12.13+hotfix.8
,从Android集成FlutterModule的角度去学习源码,并且没有使用AndroidX框架,因为现在集成FlutterModule有bug:即使flutter的yaml文件设置使用AndroidX,但是调试生成的依赖包依然是support库。特别是使用kotlin1.3.71
版本插件会报错,即使不影响运行,也会影响开发。对应issue在这里https://github.com/flutter/flutter/issues/52077#event-3204382141。
本文以Android混合Flutter Module的角度出发,对于iOS朋友来说,流程应该也是一样的。
小结
后文直接上注释与代码比较枯燥,这里先上小结,如果本文对你有帮助或喜欢本文请帮忙点赞哦,谢谢鼓励ღ( ´・ᴗ・` )比心
。如有错漏,还烦请指出,十分感谢哈!
- 一个Native进程只有一个DartVM。
- 一个DartVM(或说一个Native进程)可以有多个FlutterEngine。
- 多个FlutterEngine运行在各自的Isolate中,他们的内存数据不共享,需要通过Isolate事先设置的port(顶级函数)通讯。
- FlutterEngine可以后台运行代码,不渲染UI;也可以通过FlutterRender渲染UI。
- 初始化第一个FlutterEngine时,DartVM会被创建,之后不会再有其他DartVM环境被创建。
- FlutterEngine可以通过FlutterEngineCache管理缓存,建议使用阿里闲鱼的flutter_boost来管理Native&Flutter页面混合的项目。
- 我们可以手动改动Flutter项目的入口函数、flutter_assets资源路径、flutter项目初始Route等参数。涉及到的API有FlutterLoader、DartExecutor、FlutterJNI、Host等等。简单描述下,就是使用BinaryMessager传输数据,在修改入口函数、初始化Route参数之后在调用DartExecutor的执行代码
/** * 创建一个新的engine */ fun createEngine(context: Application, dartVmArgs: Array<String>?) { if (flutterEngine != null) { throw IllegalStateException("Already has a unrelease engine!") } flutterEngine = FlutterEngine(context, dartVmArgs) flutterEngine?.navigationChannel?.setInitialRoute("/page/me") flutterEngine?.dartExecutor?.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) FlutterEngineCache .getInstance() .put(FlutterConst.UNIQUE_ENGINE_NAME, flutterEngine) }
- FlutterEngine创建之后需要手动启动,调用
FlutterEngine.destory()
之后,该Engine就不能再使用了,并且需要清空FlutterEngineCache中的缓存。/** * 释放flutter引擎 */ fun releaseFlutterEngine() { flutterEngine?.let { engine -> FlutterEngineCache.getInstance().remove(FlutterConst.UNIQUE_ENGINE_NAME) engine.destroy() } flutterEngine = null }
再说一句,如有错漏,烦请指出,十分感谢哈!
1 FlutterActivity
我之前分享过一篇关于FlutterActivity的笔记,但是现在的FlutterSDK的API改动很大,我们这里直接看最新版本的Flutter版本的FlutterActivity部分注释,注意package是io.flutter.embedding.android
,不是io.flutter.embedding.app
。这里直接贴原文
Activity which displays a fullscreen Flutter UI.
FlutterActivity is the simplest and most direct way to integrate Flutter within an Android app.
Dart entrypoint, initial route, and app bundle path
The Dart entrypoint executed within this Activity is “main()” by default. To change the entrypoint that a FlutterActivity executes, subclass FlutterActivity and override getDartEntrypointFunctionName().
The Flutter route that is initially loaded within this Activity is “/”. The initial route may be specified explicitly by passing the name of the route as a String in FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE, e.g., “my/deep/link”.
The initial route can each be controlled using a FlutterActivity.NewEngineIntentBuilder via FlutterActivity.NewEngineIntentBuilder.initialRoute.
The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass of FlutterActivity by overriding their respective methods:
- getAppBundlePath()
- getDartEntrypointFunctionName()
- getInitialRoute()
The Dart entrypoint and app bundle path are not supported as Intent parameters due to security concerns. If such configurations were exposed via Intent, then a FlutterActivity that is exported from your Android app would allow other apps to invoke arbitrary Dart entrypoints in your app by specifying different Dart entrypoints for your FlutterActivity. Therefore, these configurations are not available via Intent.
Using a cached FlutterEngine
FlutterActivity can be used with a cached FlutterEngine instead of creating a new one. Use withCachedEngine(String) to build a FlutterActivity Intent that is configured to use an existing, cached FlutterEngine. io.flutter.embedding.engine.FlutterEngineCache is the cache that is used to obtain a given cached FlutterEngine. An IllegalStateException will be thrown if a cached engine is requested but does not exist in the cache.
When using a cached FlutterEngine, that FlutterEngine should already be executing Dart code, which means that the Dart entrypoint and initial route have already been defined. Therefore, FlutterActivity.CachedEngineIntentBuilder does not offer configuration of these properties.
It is generally recommended to use a cached FlutterEngine to avoid a momentary delay when initializing a new FlutterEngine. The two exceptions to using a cached FlutterEngine are:
When FlutterActivity is the first Activity displayed by the app, because pre-warming a FlutterEngine would have no impact in this situation.
When you are unsure when/if you will need to display a Flutter experience.
The following illustrates how to pre-warm and cache a FlutterEngine:
// Create and pre-warm a FlutterEngine. FlutterEngine flutterEngine = new FlutterEngine(context); flutterEngine .getDartExecutor() .executeDartEntrypoint(DartEntrypoint.createDefault()); // Cache the pre-warmed FlutterEngine in the FlutterEngineCache. FlutterEngineCache.getInstance().put(“my_engine”, flutterEngine);
Alternatives to FlutterActivity
If Flutter is needed in a location that cannot use an Activity, consider using a FlutterFragment. Using a FlutterFragment requires forwarding some calls from an Activity to the FlutterFragment.
If Flutter is needed in a location that can only use a View, consider using a FlutterView. Using a FlutterView requires forwarding some calls from an Activity, as well as forwarding lifecycle calls from an Activity or a Fragment.
FlutterActivity responsibilities
FlutterActivity maintains the following responsibilities:
- Displays an Android launch screen.
- Displays a Flutter splash screen.
- Configures the status bar appearance.
- Chooses the Dart execution app bundle path and entrypoint.
- Chooses Flutter’s initial route.
- Renders Activity transparently, if desired.
- Offers hooks for subclasses to provide and configure a FlutterEngine.
Launch Screen and Splash Screen
FlutterActivity supports the display of an Android “launch screen” as well as a Flutter-specific “splash screen”. The launch screen is displayed while the Android application loads. It is only applicable if FlutterActivity is the first Activity displayed upon loading the app. After the launch screen passes, a splash screen is optionally displayed. The splash screen is displayed for as long as it takes Flutter to initialize and render its first frame.
Use Android themes to display a launch screen. Create two themes: a launch theme and a normal theme. In the launch theme, set windowBackground to the desired Drawable for the launch screen. In the normal theme, set windowBackground to any desired background color that should normally appear behind your Flutter content. In most cases this background color will never be seen, but for possible transition edge cases it is a good idea to explicitly replace the launch screen window background with a neutral color(后文省略)…
原文比较长,提取一下。
- FlutterActivity全屏显示Flutter的UI,是Android显示FlutterUI最简单的方式。
- 默认情况下,Flutter项目的入口函数是顶级函数
main()
,如果需要启动函数请重写FlutterActivity.getDartEntrypointFunctionName()
函数。 - 初始路由路径是’/’,可通过重写
Flutter.getInitialRoute()
函数来改变FlutterActivity入口,但是不支持使用intent传值的方式来改变入口,注释中说道是为了安全问题,避免其他App改变当前项目的入口页面。 - 可以通过重写
getAppBundlePath()
函数来指定Android项目运行时加载的Flutter项目bundle路径,这个我还没有尝试过,不赘述。getInitialRoute
、getAppBundlePath
、getDartEntrypointFunctionName
的初始值如下:super.getInitialRoute() = null super.getAppBundlePath() = flutter_assets super.getDartEntrypointFunctionName() = main
- 指定FlutterEngine缓存,这是本文的重点。 我们可以通过
FlutterEngineCache
来管理FlutterEngine。 - 除了FlutterActivity,我们也可以使用FlutterFragment、FlutterView来展示Flutter的UI。
Splash Screen
也就是闪屏,FlutterActivity可以在Mianfest中设置一个闪屏theme与常驻theme。其中闪屏theme只会在FlutterActivity启动时显示,启动之后会切换回常驻theme。并且也可以通过重写SplashScreenProvider.provideSplashScreen
来展示一个Flutter初始化时展示的第一帧,warmFrame。
对FlutterActivity的学习暂时停下,先跳跃到FlutterEngine,毕竟这才是主角,FlutterActivity只是一个载体。当对于FlutterEngine有一定了解之后回头再来看FlutterActivity就很简单了。
2 FlutterEngine
A single Flutter execution environment. 这是FlutterEngine的首行注释:一个独立、最小的flutter执行环境。Single在这里不太好直译。
The FlutterEngine is the container through which Dart code can be run in an Android application.
Dart code in a FlutterEngine can execute in the background, or it can be render to the screen by using the accompanying FlutterRenderer and Dart code using the Flutter framework on the Dart side. Rendering can be started and stopped, thus allowing a FlutterEngine to move from UI interaction to data-only processing and then back to UI interaction.
Multiple FlutterEngines may exist, execute Dart code, and render UIs within a single Android app.
To start running Dart and/or Flutter within this FlutterEngine, get a reference to this engine’s DartExecutor and then use DartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint). The DartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint) method must not be invoked twice on the same FlutterEngine.
To start rendering Flutter content to the screen, use getRenderer() to obtain a FlutterRenderer and then attach a RenderSurface. Consider using a io.flutter.embedding.android.FlutterView as a RenderSurface.
Instatiating the first FlutterEngine per process will also load the Flutter engine’s native library and start the Dart VM. Subsequent FlutterEngines will run on the same VM instance but will have their own Dart Isolate when the DartExecutor is run. Each Isolate is a self-contained Dart environment and cannot communicate with each other except via Isolate ports.
同样的,简单概括一下
- FlutterEngine提供了Dart代码在Android上运行的容器。
- Dart代码可以在Android后台运,或者通过
FlutterRender
与FlutterFrameWord配合渲染到屏幕上。 - Render可以被启动与停止,所以FlutterModule的作用不再只是渲染UI了,当然在有必要时才让Dart代码后台运行。
- 在一个Android项目中,多FlutterEngine是存在的。每个FlutterEngine运行在不同的isolate中,只能通过IsolatePort进行通讯。
- 初始化第一个FlutterEngine时,会加载FlutterEngine的native libs并且启动DartVM,DartVM是单例!
- 创建FlutterEngine之后,使用
DartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint)
启动Engine。- DartExecutor:*Configures, bootstraps, and starts executing Dart code.*其实就是一个
BinaryMessenger
实现类,通过DartEntrypoint
指定FlutterModule的顶级函数入口与flutter打包资源。public static class DartEntrypoint { @NonNull public static DartEntrypoint createDefault() { return new DartEntrypoint( FlutterMain.findAppBundlePath(), "main" ); } /** + The path within the AssetManager where the app will look for assets. */ @NonNull public final String pathToBundle; /** + The name of a Dart function to execute. */ @NonNull public final String dartEntrypointFunctionName; public DartEntrypoint( @NonNull String pathToBundle, @NonNull String dartEntrypointFunctionName ) { this.pathToBundle = pathToBundle; this.dartEntrypointFunctionName = dartEntrypointFunctionName; } @Override @NonNull public String toString() { return "DartEntrypoint( bundle path: " + pathToBundle + ", function: " + dartEntrypointFunctionName + " )"; } }
- DartExecutor:*Configures, bootstraps, and starts executing Dart code.*其实就是一个
到了这里,关于DartVM的相关知识并没有在FlutterEngine的注释中出现,在dart (Dart VM)中也没有一个全面的介绍,那这里就先酱紫吧。
2.1 更多关于FlutterEngine
回到FlutterActivity,该类的注释提到了FlutterEngine的缓存问题,那么该部分是如何实现的呢?
这里提几个重要的类,并简单介绍下:
- FlutterEngine:主角,没啥好说的额
- FlutterMain:*A class to intialize the Flutter engine.*其实就是帮助FlutterEngine初始化的。有
startInitialization
与ensureInitializationComplete
等函数,最新的v1.12.13+hotfix.8
版本用不到这2个函数了,这里提一下,不需要在手动调用了,替换API为FlutterLoader。 - FlutterLoader:
Finds Flutter resources in an application APK and also loads Flutter's native library.
查找Flutter资源的类。 - FlutterEngineCache:*Static singleton cache that holds FlutterEngine instances identified by Strings.*使用String类型key来管理FlutterEngine实例的静态单例。
- FlutterJNI:*Interface between Flutter embedding’s Java code and Flutter engine’s C/C++ code.*定义了Java与C/C++库之间互调函数接口函数。
2.2 手动管理FlutterEngine
经过前文的描述,我们对于Flutter的工作流程已经有些许了解了,再啰嗦下:
- 一个Native进程只有一个DartVM环境。
- 一个Native进程可以有多个FlutterEngine,初始化第一个FlutterEngine会开始运行DartVM。
- FlutterEngine可以后台运行代码,也可以通过FlutterRender渲染UI。
- 多个FlutterEngine单独运行在各自的Isolate中,互不干涉。
对于DartVM,我们知道就行了,改不了,也就切换个FlutterSDK版本能影响下它。而FlutterEngine是我们需要着重关心的对象了,这里根据业务场景对Flutter混开项目做下分类:
- 纯Flutter项目,整个项目的主页面或者成为栈顶就只有一个FlutterActivity(FlutterController)。所有页面跳转都单独一个FlutterEngine中完成,这种情况不需要管理FlutterEngine。
- Flutter独立模块,打开一个Flutter页面之后,接下来的页面都是Flutter页面,除非退出当前模块。就和WebView中跳转页面一样,这种情况下管理一个FlutterEngine单例即可。
- **Flutter混合模块,Flutter页面与原生页面交错打开,这种情况要是一个Flutter页面一个FlutterEngine那将是灾难啊,并且对于运行内存数据也不好共享。**在这种情况下就需要对FlutterEngine进行管理,建议使用flutter_boost,阿里闲鱼出品,牛比!。
flutter_boost对于Flutter&Native页面混合应该是目前最好的坚决方案了,我这里介绍下FlutterEngine在Native端的管理API,大家如果在使用flutter_boost也会得心应手啦。废话不多说,上步骤:
- 创建FlutterEngine对象,其构造函数全参数如下:
public FlutterEngine( @NonNull Context context, @NonNull FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI, @Nullable String[] dartVmArgs, boolean automaticallyRegisterPlugins ){ //context建议使用Application的Context //FlutterLoader替换了之前的FlutterMain,用于初始化FlutterFramework,FlutterLoader使用单例获取 //flutterJNI,提供了Native与FlutterFramework交互的API。 //dartVmArgs可以为空,具体配置与作用不清楚诶。 //automaticallyRegisterPlugins是否自动注册插件,默认值为true,不建议修改。 }
- 定义一个Key,以键值对的形势存到FlutterEngineCached中,在FlutterEngineCache中使用HashMap<String, FlutterEngine>来存储FlutterEngine。
FlutterEngineCache .getInstance() .put(String, FlutterEngine)
- 启动与停止FlutterEngine:
- 使用
flutterEngine?.dartExecutor?.executeDartEntrypoint(DartEntrypoint)
来启动引擎。 - 使用
Flutter.destroy()
销毁FlutterEngine,记得调用FlutterEngineCache.getInstance().remove(String)
移除缓存哦。
- 使用
- 在FlutterActivity或者FlutterFragment中使用FlutterEngine缓存,据我所知有2种方式可以实现
- 在FlutterActivity或者FlutterFragment创建时,使用其
EngineFragmentBuilder
、NewEngineIntentBuilder
或者其他Builder构造。 - 重写FlutterActivity或者FlutterFragment的
getCachedEngineId()
方法,直接指定FlutterEngine缓存对象,这是最为直接的办法。
我推荐使用方法2。
- 在FlutterActivity或者FlutterFragment创建时,使用其
- 打住,由于篇幅关系后面的内容就不展开了。
3 再结
本文从FlutterSDK生成的源码中学习了关于FlutterEngine的相关知识,简单的提取概括一些简单但却重要的知识点。
对于一些展开的内容由于篇幅与精力等其他原因,留待之后学习记录哈。