在实际工程场景中,通常我们是在现有工程的基础上增加Flutter的业务模块。因此我们将重点讲解如何在已有的工程中集成Flutter、Flutter的编译原理、打包流程等。
2.1 工程集成
2.1.1 集成原理
通过第一章的学习,我们知道Flutter通过嵌入层集成进原生应用中,对于底层操作系统而言,Flutter应用程序与本地其他应用程序一样,外壳是Android或iOS的原生工程,Flutter代码是一个模块集成到现有的应用程序中,以Android为例,就是将Flutter工程作为一个独立的AAR,通过gradle脚本,集成进现有工程中。另外,我们知道要想Flutter在不同平台运行起来,除了本身代码的编译产物之外,还需要Dart VM和Skia渲染引擎,这些内容也需要一起打包进应用中。了解了原理后,我们以Android为例来看一下具体的集成方法。
2.1.2 集成方法
1 创建Flutter模块
在Android工程同级目录下执行命令flutter create -t module demo_flutter,也可以在创建时指定语言和是否使用androidx。
$ flutter create -t module -i objc -a java --no-androidx demo_flutter
我们也可以在Android Studio打开现有的Android项目并点击菜单按钮File>New>NewModule…,这样就可以创建出一个可以集成的新Flutter模块,或者选择导入已有的Flutter模块。生成的目录结构如下:
其中lib/main.dart是业务的入口代码,pubspec.yaml为项目配置文件,.android和.ios工程目录,用于存放对应平台生成的编译产物。
2 Android工程集成
接下来,将Flutter模块添加为宿主应用程序的依赖项。
1.将Flutter模块作为子项目添加到宿主应用的settings.gradle中:
include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))
2.在app目录下的build.gradle,在依赖中加上Flutter模块的依赖:
implementation project(':flutter')
3.引入Java8
Flutter Android引擎需要使用到Java 8中的新特性。
在尝试将Flutter模块项目集成到宿主Android应用之前,需要在宿主Android应用的 build.gradle文件的android{ }块中声明了以下属性:
android {
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
3 创建Flutter页面
与Web容器、MRN容器类似,Flutter模块在原生工程中也有对应的原生容器FlutterActivity和FlutterViewController。
1.在AndroidManifest.xml中添加FlutterActivity
Flutter提供了FlutterActivity,用于在Android应用内部展示一个Flutter的交互界面。和其他的Activity一样,FlutterActivity必须在项目的AndroidManifest.xml文件中注册。
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
2.加载FlutterActivity
在AndroidManifest.xml中注册之后,我们就可以通过startActivity打开FlutterActivity页面了。
myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(currentActivity)
);
}
});
上述的例子假定了Dart代码入口是调用main(),并且Flutter初始路由是‘/’。Dart代码入口不能通过Intent改变,但是初始路由可以通过Intent来修改。下面的例子是打开一个自定义Flutter初始路由的FlutterActivity。
myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("/my_route")
.build(currentActivity)
);
}
});
FlutterActivity默认会创建它自己的FlutterEngine,每个FlutterEngine会有一个明显的预热时间,这意味着加载一个标准的FlutterActivity时,会有一个短暂的延迟,想要最小化这个延迟时间,可以在启动FlutterActivity之前,初始化一个FlutterEngine,然后用这个已经预热好的FlutterEngine。例如在Application中先初始化一个FlutterEngine。
public class MyApplication extends Application {
public FlutterEngine flutterEngine;
@Override
public void onCreate() {
super.onCreate();
// 初始化一个FlutterEngine.
flutterEngine = new FlutterEngine(this);
// 配置初始路由
flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
// Dart代码会在预热FlutterEngine时就开始执行
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);
// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
}
传给FlutterEngineCache的ID可以是任何名称,在使用缓存FlutterEngine时,传递同样的ID即可。
myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(currentActivity)
);
}
});
运行时的性能考量并不是预热和缓存一个FlutterEngine的唯一原因。一个预热的FlutterEngine会独立于FlutterActivity执行Dart代码,即一个FlutterEngine可以在任意时刻用于执行任意代码,非UI的应用逻辑可以在FlutterEngine中执行,例如网络请求和数据缓存。
另外,配置导航通道内的初始路由,会让关联的FlutterEngine在runApp()方法首次执行后,展示已配置的路由页面。想在不同的Activity之间使用同一个FlutterEngine,并且在展示时切换不同的路由,需要通知Dart代码切换Navigator路由。 4 Flutter配置
Flutter的配置文件是pubspec.yaml。
# 此处定义flutter module name,一般以 {业务名}_flutter 命名
name: demo_flutter
description: A new flutter module project.
# 此处定义flutter module 的编译产物(对应 pod 和 aar)版本号
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
module:
androidX: false
# 此处配置 Android package id(用于AAR package id)
androidPackage: com.example.demo_flutter
ohosPackage: com.example.ohos.demo_flutter
# 此处配置 iOS Bundle Identifier
iosBundleIdentifier: com.example.demoFlutter
2.2 编译原理
2.2.1 编译产物
集成进工程后,我们来看一下flutter的编译产物
1 Android产物
Android侧flutter产物主要包括两大块assets和libflutter.so 1.libflutter.so:flutter引擎的C++代码编译产物。
2.libapp.so:flutter业务与框架的Dart代码编译产物,内部由四部分组成。
(1)isolate_snapshot_data:表示isolate堆存储区的初始状态和特定的信息。和vm_snapshot_data配合,更快的启动Dart VM;
(2)isolate_snapshot_instr:包含由Dart isolate执行的AOT代码;
(3)vm_snapshot_data:表示isolates之间的共享的Dart堆存储区的初始状态,用于更快的启动Dart VM;
(4)vm_snapshot_instr:包含VM中所有的isolates之间共享的常见例程的指令;
flutter 1.7.8之后不再产生isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data、vm_snapshot_instr四部分,转而将其打包成一个so。
3.flutter.jar:flutter引擎的Java代码编译产物。
4.flutter_assets:包括图片、字体、LISENSE等静态资源。
2 iOS产物
iOS侧flutter产物主要分为两部分App.framework和Flutter.framework,具体组成如下图所示:
Flutter.framework:
Flutter:即为Flutter Engine,是由Dart代码和C++代码编译而成的动态链接库。
icudtl.dat:国际化支持相关数据文件,大小固定为883KB。
App.framework:
App:其为一个动态链接库、主要来源是dart代码AOT编译的产物,其主要包括_kDartIsolateSnapshotData、_kDartVmSnapshotData、_kDartIsolateSnapshotInstructions、_kDartVmSnapshotInstructions四部分。
flutter_assets::包括图片,字体、LICENSE等静态资源。
2.2.2 打包流程
在讲集成时我们说到需要修改build.gradle,将flutter的编译加入,这个小节我们将详细讲解脚本是如何将flutter必需的产物打包进Android工程中的,这个过程需要由flutter tool和gradle脚本来配合完成。
Flutter工中打包过程中涉及到了local.properties、settings.gradle、build.gradle、flutter.gradle这几个脚本文件的参与。
1 Android工程集成Flutter
在local.properties中增加了Flutter SDK的路径
sdk.dir=/Users/Library/Android/sdk
flutter.sdk=/Users/.flutter_sdk
Android工程中的build.gradle主要的功能是:
1.从local.properties文件中获取Flutter SDK路径;
2.把该路径下的Flutter Gradle插件导入到当前工程中;
3.配置Flutter工程的路径。
//获取flutter sdk路径、versionCode、VersionName
def flutterRoot = localProperties.getProperty('flutter.sdk')
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
//导入flutter.gradle,脚本位于flutter_tools目录下
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
flutter {
source '../..'
}
2 加入Flutter依赖
flutter.gradle的作用主要是Flutter工程编译和将flutter工程依赖的jar和so,打包到APK中。flutter.gradle有两大核心部分:FlutterPlugin和FlutterTask。
apply plugin: FlutterPlugin
class FlutterPlugin implements Plugin<Project> {
@Override
void apply(Project project) {}
}
abstract class BaseFlutterTask extends DefaultTask {}
class FlutterTask extends BaseFlutterTask {}
FlutterPlugin的入口是apply方法,从Android根工程的local.properties文件下获取Flutter SDK的路径和版本号信息。并且根据当前系统的设置运行Flutter默认程序:Linux/Mac OS执行的是Flutter SDK/bin/flutter,Windows执行的是Flutter SDK/bin/flutter.bat。
@Override
void apply(Project project) {
//从根工程下local.properties文件中 获取SDK Flutter路径信息
String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)
//根据操作环境的不同选择执行Flutter SDK/bin/flutter文件 还是FlutterSDK/bin/flutter.bat文件
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
//获取Flutter引擎文件路径Flutter SDK/bin/cache/artifacts/engine
Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
//根据不同的配置 选择不同的flutter.jar包,这个jar包包含了Android代码所依赖的Flutter类文件和lib/libflutter.so
//android-arm/flutter.jar文件 (debug专用)
debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile()
}
此外我们通过copyFlutterAssetsTask方法把Flutter根工程flutter_assets/目录中所有的内容都拷贝到Flutter根工程//build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下。如果是release或者profile版本的话,还包含拷贝了Dart的二进制产物snapshot或app.so merged_assets中的这些文件就是最后都会打包到apk的assets目录下。
//查找 :flutter:package[构建类型]Assets的Task
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
//查找:flutter:cleanPackage[构建类型]Assets的Task
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
//创建 copyFlutterAssets[构建类型] 的task的拷贝操作.
Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
dependsOn flutterTask
dependsOn packageAssets ? packageAssets : variant.mergeAssets
dependsOn cleanPackageAssets ? cleanPackageAssets : "clean${variant.mergeAssets.name.capitalize()}"
variant.mergeAssets.outputDir == /build/app/intermediates/merged_assets/debug/mergeDebugAssets/out
into packageAssets ? packageAssets.outputDir : variant.mergeAssets.outputDir
//运行FlutterTask的getAssets方法执行拷贝操作
with flutterTask.assets
}
3 Flutter编译
Flutter的编译流程在FlutterTask中,FlutterTask继承自BaseFlutterTask,BaseFlutterTask是一个自定义的Task( DefaultTask),因此入口就在@TaskAction注解的build方法中,build方法直接调用BaseFlutterTask的buildBundle方法。如果当前的构建类型是profile或者release,需要先编译AOT的snapshot,Dart的snapshot相当于Java的jar。
class FlutterTask extends BaseFlutterTask {
@TaskAction
void build() {
buildBundle()
}
void buildBundle() {
intermediateDir.mkdirs()
if (!sourceDir.isDirectory()) {
throw new GradleException("InvalidFluttersource directory: ${sourceDir}")
}
intermediateDir.mkdirs()
//如果当前的构建类型 是profile和release 则先执行下面的操作
if (buildMode == "profile" || buildMode == "release") {
project.exec {
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
args "build", "aot"
args "--suppress-analytics"
args "--quiet"
args "--target", targetPath //Flutter启动的程序入口,默认为lib/main.dart
args "--target-platform", "android-arm"
args "--output-dir", "${intermediateDir}" //输出目录
if (trackWidgetCreation) {
args "--track-widget-creation"
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"
}
if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
}
if (buildSharedLibrary) {
args "--build-shared-library"
}
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
args "--${buildMode}"
}
}
}
实际的编译命令是flutter build aot,这个脚本转化为启动一个Dart虚拟机,并执行flutter_tool.snapshot。
flutter build aot --suppress-analytics --quiet --target
lib/main.dart --target-platform android-arm --output-dir
工程路径/build/app/intermediates/flutter/release --release
在flutter_tools.snapshot中 定义了一些子命令。
class BuildCommand extends FlutterCommand {
BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand());
addSubcommand(BuildFlxCommand());
addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
}
这里对应的是BuildAotCommand,这里面其实调用了两个Dart虚拟机命令,主要做了两件事:编译kernel和生成AOT snapshot。这两种产物都是Dart代码生成的程序文件。
AOT可执行文件是在编译期间根据kernel生成的二进制的文件,在运行的时候已经是平台的机器码了,因此不再需要Dart虚拟机解释执行了,执行速度更快。release模式下,打包进APK的Dart源码都是以AOT文件的形式存在的。
class BuildAotCommand extends BuildSubCommand {
Future<FlutterCommandResult> runCommand() async {
String mainPath = findMainDartFile(targetFile);
final AOTSnapshotter snapshotter = AOTSnapshotter();
// 编译kernel.
mainPath = await snapshotter.compileKernel(
platform: platform,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
trackWidgetCreation: false,
outputPath: outputPath,
extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions],
);
// 生成AOT snapshot.
if (platform == TargetPlatform.ios) {
... ...
} else {
// Android AOT snapshot.
final int snapshotExitCode = await snapshotter.build(
platform: platform,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
buildSharedLibrary: argResults['build-shared-library'],
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
);
if (snapshotExitCode != 0) {
status?.cancel();
throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode');
}
}
}
生成kernel文件具体是执行了以下命令:
frontend_server.dart.snapshot --target=flutter --aot --tfa
-Ddart.vm.product=true --packages .packages
--output-dill app.dill
可以看到,这里是通过Dart虚拟机启动了fronted_server.dart.snapshot把Dart代码文件编译成为名为app.dill的kernel文件。接着,就可以根据上一步得到的app.dill通过gen_snapshot来生成AOT可执行文件了。
flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot
--causal_async_stacks
--packages=.packages
--deterministic
--snapshot_kind=app-aot-blobs
--vm_snapshot_data=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/vm_snapshot_data
--isolate_snapshot_data=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/isolate_snapshot_data
--vm_snapshot_instructions=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/vm_snapshot_instr
--isolate_snapshot_instructions=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/isolate_snapshot_instr
--no-sim-use-hardfp
--no-use-integer-division /path-to-project/flutter_hello/build/app/intermediates/flutter/release/app.dill
gen_snapshot不是之前常见的Dart命令,而是一个Native二进制可执行文件。其对应的是Dart虚拟机中的C++代码:dart/runtime/bin/gen_snapshot.cc。这里主要做了两件事。首先,根据传入的参数,初始化出一个Dart运行环境加载kernel文件,然后把所有Dart类都加载到运行环境中。接着根据已有的运行环境,直接编译生成二进制可执行文件snapshot。
最后我们需要build bundle,将一些文件放进flutter_assets目录下,这些文件分别是:
packages/cupertino_icons/assets/CupertinoIcons.ttf
fonts/MaterialIcons-Regular.ttf
AssetManifest.json
FontManifest.json
LICENSE
到这里,flutter编译release包的完整流程就全部分析完了。我们以一张图再来归纳一下整个编译流程:
2.2.3 包大小优化
1 打包阶段
了解了Flutter产物之后,我们就可以分析下如何优化包的大小,包大小优化的核心思想就是三个:删减、压缩、动态下发。
1.删减就是删除无用代码,无用资源。
Flutter引擎中包括了Dart、skia、boringssl、icu、libpng等多个模块,其中Dart和skia是必须的,其他模块如果用不到可以考虑裁掉,能够带来几百k的收益。
Flutter的业务代码采用了Tree Shaking机制,Tree Shaking是一种死代码消除技术,思想是,一个程序所有可能的执行流程都可以用函数调用的树来表示,这样就可以消除那些从未被调用的函数。所以,Flutter的业务产物从代码的角度已经是精简过的了,要想继续精简只能从业务的角度去分析。Java代码本身占比很少,而且会被App中的代码所依赖,移除难度比较大,性价比很低。
flutter_assets中的LICENSE许可证文件在运行时用不到,可以删除,删除后可以节省包体积约60KB。删除过程统一可以通过自定义一个gradle plugin来完成。
在官方的Flutter打包流程中,flutter_assets是在copyFlutterAssets的Task中复制到assets目录的。我们可以在该Task之后新增一个Task,将指定的文件删除掉。
2.压缩是把一些图片之类的资源压缩,在使用时再解压。
3.动态下发,就是放到服务端,等到APK启动后再按需下载。Flutter中的so文件我们可以只保留一种架构,通过abifilters将无用的so移除,然后将剩下的架构采用动态下发。flutter_assets文件中可以移除其中的无用LICENSE文件,其他文件动态下发。libflutter.so、libapp.so和flutter_assets/bundle.zip从APK中移除。
2 运行阶段
在APK运行阶段,首先要对Flutter路由进行拦截,确保在进入到Flutter页面之前,so和flutter_assets都已经下载完成。在第一次进到Flutter页面,需要先初始化Flutter引擎,主要是将libflutter.so和libapp.so的路径改为动态下发的路径。Flutter引擎的初始化由FlutterMain中的startInitialization和ensureInitializationComplete方法完成,一般在Application初始化时调用startInitialization(懒加载模式会延迟到启动Flutter页面时再调用),然后在Flutter页面启动时调用ensureInitializationComplete确保初始化的完成。
在startInitialization方法中,会加载libflutter.so,在ensureInitializationComplete中会构建shellArgs参数,然后将shellArgs传给FlutterJNI.nativeInit方法,由jni侧完成引擎的初始化。其中shellArgs中有个参数AOT_SHARED_LIBRARY_NAME可以用来指定libapp.so的路径。
我们可以继承FlutterMain类,重写startInitialization和ensureInitializationComplete的逻辑,让业务方使用我们的自定义类来初始化引擎。当自定义类完成引擎的初始化后,通过反射的方式修改sSettings和sInitialized,从而使得原有的初始化逻辑不再执行。
引擎初始化完成后,开始执行Dart代码的逻辑,此时需要加载资源,例如字体或者图片,原有的资源加载器是通过method channel调用AssetManager的方法,从APK中的assets中进行加载,我们需要改成从动态下发的路径中加载。
其中字体是一种特殊的资源,有两种加载方式:静态加载和动态加载。静态加载是在pubspec.yaml中声明字体,当引擎初始化的时候,自动从AssetManager中加载静态注册的资源。动态资源是通过Flutter提供的FontLoader类来完成字体的动态加载。当资源动态下发后,我们要改为动态加载的方式来加载资源。
2.3 Flutter启动流程
Flutter启动流程,先从Application开始,FlutterApplication是Flutter提供的Application,在onCreate方法中调用了FlutterMain.startInitialization(this)。
public class FlutterApplication extends Application {
private Activity mCurrentActivity = null;
public FlutterApplication() {
}
@CallSuper
public void onCreate() {
super.onCreate();
FlutterMain.startInitialization(this);
}
}
通过调用,跟踪到FlutterLoader的startInitialization,在这个方法里加载了libflutter.so库。在initConfig方法,初始化了相关资源的路径,以便进行资源查找
public void startInitialization(@NonNull Context applicationContext, @NonNull FlutterLoader.Settings settings) {
if (this.settings == null) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("startInitialization must be called on the main thread");
} else {
applicationContext = applicationContext.getApplicationContext();
this.settings = settings;
long initStartTimestampMillis = SystemClock.uptimeMillis();
this.initConfig(applicationContext);
this.initResources(applicationContext);
// 加载libflutter.so库
ReLinker.loadLibrary(applicationContext, "flutter");
VsyncWaiter.getInstance((WindowManager)applicationContext.getSystemService("window")).init();
long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
FlutterJNI.nativeRecordStartTimestamp(initTimeMillis);
}
}
}
private void initConfig(@NonNull Context applicationContext) {
Bundle metadata = this.getApplicationInfo(applicationContext).metaData;
if (metadata != null) {
this.aotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, "libapp.so");
this.flutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, "flutter_assets");
this.vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, "vm_snapshot_data");
this.isolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, "isolate_snapshot_data");
}
}
我们再看一下FlutterActivity,创建了FlutterActivityDelegate的对象,onCreate等生命周期方法实际都是调用的FlutterActivityDelegate。
public class FlutterActivity extends Activity implements FlutterView.Provider, PluginRegistry, ViewFactory {
private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this);
private final FlutterActivityEvents eventDelegate = delegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
eventDelegate.onCreate(savedInstanceState);
}
}
在FlutterActivityDelegate中创建了FlutterView,并将FlutterView setContent到Activity全屏显示。
public void onCreate(Bundle savedInstanceState) {
String[] args = getArgsFromIntent(this.activity.getIntent());
// 确保已经初始化完成
FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
this.flutterView = this.viewFactory.createFlutterView(this.activity);
if (this.flutterView == null) {
// 创建了FlutterView
FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
// 设置FlutterView全屏显示
this.flutterView.setLayoutParams(matchParent);
// acitivity的显示内容
this.activity.setContentView(this.flutterView);
this.launchView = this.createLaunchView();
if (this.launchView != null) {
this.addLaunchView();
}
}
// 确认Bundle并运行
if (!this.loadIntent(this.activity.getIntent())) {
String appBundlePath = FlutterMain.findAppBundlePath();
if (appBundlePath != null) {
this.runBundle(appBundlePath);
}
}
}
private boolean loadIntent(Intent intent) {
String action = intent.getAction();
if ("android.intent.action.RUN".equals(action)) {
String route = intent.getStringExtra("route");
String appBundlePath = intent.getDataString();
if (appBundlePath == null) {
appBundlePath = FlutterMain.findAppBundlePath();
}
if (route != null) {
this.flutterView.setInitialRoute(route);
}
this.runBundle(appBundlePath);
return true;
} else {
return false;
}
}
private void runBundle(String appBundlePath) {
if (!this.flutterView.getFlutterNativeView().isApplicationRunning()) {
FlutterRunArguments args = new FlutterRunArguments();
args.bundlePath = appBundlePath;
args.entrypoint = "main";
this.flutterView.runFromBundle(args);
}
}
FlutterView继承了SurfaceView,SurfaceView使用的是绘图线程而不是UI线程,我们平时使用的摄像头、游戏之类的要求图形性能比较高的场景使用的就是SurfaceView。它的内部有两个线程即主线程和渲染线程,使用渲染线程中向屏幕上绘图可以避免主线程阻塞,从而提高了程序的反应速度。此外FlutterView还实现BinaryMessenger接口和TextureRegistry接口,BinaryMessenger接口定义了Android代码和Flutter代码直接通信的方式,此接口还有2个内部接口。FlutterView实现了这个接口,拥有了和Flutter通信的的能力。TextureRegistry接口接口主要是创建一个SurfaceTexture对象,使用SurfaceTextureEntry包装起来,拥有唯一的ID。
最终调用FlutterJNI让Engine来运行Flutter bundle。Flutter就会开始执行Dart程序,在FlutterView的SurfaceView绘制UI。
public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry {
public void runFromBundle(FlutterRunArguments args) {
if (args.entrypoint == null) {
throw new AssertionError("An entrypoint must be specified");
} else {
this.assertAttached();
if (this.applicationIsRunning) {
throw new AssertionError("ThisFlutter Engineinstance is already running an application");
} else {
this.mFlutterJNI.runBundleAndSnapshotFromLibrary(args.bundlePath, args.entrypoint, args.libraryPath, this.mContext.getResources().getAssets());
this.applicationIsRunning = true;
}
}
}
}
整个流程的调用序列图:
我们总结一下上面的流程,要完成Flutter的启动,需要以下五个步骤:
1.查找Flutter资源
Flutter引擎运行时应用已编译的Dart代码都被打包进Android和iOS。加载Flutter的第一步是在.apk、.ipa或.app中查找这些资源,例如图像、字体、JIT代码。
2.加载Flutter库
找到后,引擎会将这些资源加载进内存。
3.启动Dart VM
在Android上首次构建FlutterEngine,以及在iOS上首次运行Dart入口时,将完成一次Dart VM启动,Dart VM负责运行Dart代码。
此时,Dart代码的snapshot也将从应用程序的文件加载到内存中。
4.创建并运行一个Dart Isolate
Isolate是Dart的内存和线程容器,每个FlutterEngine都存在一个Isolate,同一个Dart VM可以承载多个Isolate。在Android上,调用DartExecutor.executeDartEntrypoint()时,Dart代码就会执行默认的入口点方法(默认是main.dart文件的main()方法),如果你在main()方法中调用Flutter的runApp()方法,则你的Flutter应用或库的Widget树将会创建并构建。
5.将UI挂载到Flutter引擎
在Android上使用FlutterActivity.withCachedEngine()方法构建的Intent,调用startActivity() 时,会将 FlutterEngine挂载到UI组件。这两个平台都为FlutterEngine提供了UI组件,在Android上是Surface,在iOS上是CAEAGLLayer或CAMetalLayer。此时,Flutter程序生成的Layer树将会转换为OpenGL(或Vulkan或Metal)GPU指令。
2.4 HotReload原理
Flutter亚秒级的热重载,是他非常大的一个优势,它可以在无需重新启动的情况下,快速,轻松地构建页面、测试功能,那Flutter是如何实现热重载的呢?
我们知道,Flutter提供两种编译模式,AOT和JIT。AOT是静态编译,即编译成设备可直接执行的二进制码,而JIT是动态编译,即将Dart代码编译成中间代码,在运行时需要解释执行。热重载只能在Debug模式下使用,Debug模式下采用的是JIT动态编译,JIT编译器将Dart代码编译成可以运行在Dart VM上的Dart Kernel,而Dart Kernel是可以动态更新的,这就实现了代码的实时更新功能。
1.代码更新。热重载模块会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的Dart代码。
2.增量编译。热重载模块会将发生变化的Dart代码,通过编译转化为增量的Dart Kernel文件。
3.推送更新。热重载模块将增量的Dart Kernel文件通过HTTP端口,发送给正在移动设备上运行的Dart VM。
4.代码合并。Dart VM会将收到的增量Dart Kernel文件,与原有的Dart Kernel文件进行合并,然后重新加载新的Dart Kernel文件。
5.Widget重建。在确认Dart VM资源加载成功后,Flutter会将其UI线程重置,通知Flutter Framework重建Widget。
Flutter提供的亚秒级热重载一直是开发者的调试利器。通过热重载,我们可以快速修改UI、修复Bug,无需重启应用即可看到改动效果,从而大大提升了UI调试效率。
但是,也有一些场景是不支持热重载的,例如全局变量和静态属性的修改、main方法的修改、initState方法的修改和枚举泛型的修改等。