一、引言
本文主要对Flutter工程编译时如何把Flutter生成的产物打包进入Android工程中进行分析。在Flutter工中打包过程中涉及到了local.properties、settings.gradle、build.gradle、flutter.gradle这几个脚本文件的参与,与传统的Android工程相比这几个脚本文件中的内容也不相同,接下来我们通过一层层解析,解开Flutter工程编译的面纱。 同时也建议大家看的时候搭配Flutter工程一起食用效果更佳。
二、工程结构分析
首先我们创建了一个最普通的Flutter工程flutter_new,创建后整个工程的目录结构如下:
Flutter工程下包括了Android和IOS两个目录,分别用于运行在Android和IOS平台上。其中android目录结果与Android工程的目录结构是一样的。Flutter工程中的android目录下包含了两个工程:第一个是android根工程,第二个是app子工程:
稍微有点不同的地方在于这两个工程的的输出目录搬到了Flutter工程根目录build下:
Flutter工程中android工程与传统的Android工程相比,都有.gradle、gradle、 setting.gradlew、gradlew等目录和文件,文件基本上都是一样的,在但是在 local.properties、settings.gradle、build.gradle文件内容上又有所不同,接下来我们我们会一一做对比。
三、local.properties
在根工程下的local.properties文件中了多了 Flutter SDK相关的配置信息,包括SDK的路径、版本名、版本号,这些信息在构建工程的过程自动从环境变量中获取的,我们无需手动配置。
四、根工程settings.gradle
如果你有配置Flutter工程根目录下.flutter-plugins这个文件,那么下面的操作就会把flutter插件用到的第三方工程include到当前的工程中,并为其配置工程的路径 projectDir:
include ':app'
//1、根工程的父目录,既Flutter工程目录
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
//2、把Flutte工程下.flutter-plugins文件内容读取到内存中
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
//3、把.flutter-plugins文件中配置的flutter插件工程包含到当前工程中
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}
然,我们新创建的Flutter工程默认是没有.flutter-plugins这个文件的,所以上面的代码基本不走。
五、根工程build.gradle
根工程中的build.grandle主要的工作是重新配置了工程的输出目录 和 工程间配置执行时的依赖关系:
buildscript {//...}
allprojects {//...}
//上面的代码是基本一样的
//第一点
rootProject.buildDir = '../build' //根工程输出路径
subprojects { //所有子工程输出路径
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
//第二点
subprojects {
project.evaluationDependsOn(':app') //为所有子工程配置app的依赖
}
//第三点
task clean(type: Delete) {
delete rootProject.buildDir
}
第一点:配置了根工程 和 其所有子工程的输出路径。把所有的输出路径都搬到了Flutter根目录下的build目录中,如果是子工程则在build目录再建立属于自己名称的输出目录。可以看下面这张图:
第二点:所有子工程都配置app子工程的依赖,既让所有子工程运行配置阶段开始之前都要保证app工程的配置阶段都已经运行完毕。这样做的好处就是保证app工程的配置属性优先导入,防止其他子工程出现属性找不到的问题发生。
第三点: 为根工程添加clean任务用来删除build目录下所有文件。
关于Project#evaluationDependsOn方法
evaluationDependsOn用于配置Project对象之间的依赖关系,跟Task的dependsOn原理一样。
举个例子,比如有两个工程app工程和lib工程,其中app依赖lib工程。在lib工程的build.gradle 添加如下的属性:
rootProject.ext.producerMsg = "Hello"
在app工程的build.gradle 添加如下的代码,既app工程使用lib工程的动态属性:
def msg = rootProject.ext.producerMsg
如果在在配置阶段app 工程先运行,这样就会导致app会导致producerMsg属性没有找到!因为此lib工程还未运行。
所以要解决这个问题 就要在app project运行配置之前,先运行lib project的配置,那么就可以用evaluationDependsOn来解决依赖,在app的build.gradle中添加如下依赖即可:
evaluationDependsOn(':lib') //运行app配置之前,先运行lib依赖
那么添加依赖之后,每次在运行app配置阶段之前,都会保证lib配置阶段先被执行。
六、APP工程build.gradle
build.gradle的内容如下,与原工程一样的地方就省略了:
//第一点:读取local.properties文件中内容到内存
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
//第二点:获取flutter sdk路径、versionCode、VersionName
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
//第三点:导入flutter gradle插件
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
//省略...基本一样
}
//第四点
flutter {
source '../..'
}
dependencies {
//可见App工程并没有依赖support包
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
第一点:读取根工程下local.properties的内容到内存中(Properties), 该内容就是上面所介绍的Flutter SDK相关信息。
第二点:获取flutter sdk路径、versionCode、VersionName等信息。
第三点:从Flutter SDK目录下导入flutter gradle插件到当前工程中运行。
第四点:配置flutter插件的source属性,该属性指定了Flutter工程的路径。
该build.gradle最主要的功能从local.properties文件中获取Flutter SDK路径,并把该路径下的Flutter Gradle插件导入到当前工程中运行,接下来我们要看看该插件到底做了哪些工作。
七、flutter.gradle
Flutter代码打包到Android工程中秘密其实就是发生在flutter.gradle脚本中。该gradle脚本位于Flutter SDK/packages/flutter_tools/gradle/flutter.gradle中,接下来我们就揭开它的神秘面纱:
flutter.gradle代码分为了有两大核心部分: FlutterPlugin和 FlutterTask。
FlutterPlugin核心代码
apply方法
FlutterPlugin实现了Plugin接口,它是一个标准的gradle plugin。因此它的主入口在apply方法中,首先我们看看第一部分:
@Override
void apply(Project project) {
// Add custom build types
println "==== apply:" + project.getName() //app
//1、新增profile、dynamicProfile、dynamicRelease 三种构建类型
//在当前project下的android.buildTypes进行配置
project.android.buildTypes {
profile {
initWith debug //initWith:复制所有debug里面的属性
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
dynamicProfile {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
dynamicRelease {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
}
//从根工程下local.properties文件中 获取SDK Flutter路径信息
String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)
if (flutterRootPath == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
}
println '===System.env.FLUTTER_ROOT: ' + System.env.FLUTTER_ROOT //默认为null
println '===flutterRootPath: ' + flutterRootPath // /Users/chenrongyi/Develop/flutter/flutter
flutterRoot = project.file(flutterRootPath)//仍然是Flutter SDK路径
if (!flutterRoot.isDirectory()) {
throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
}
println '===flutterRoot: ' + flutterRoot // /Users/chenrongyi/Develop/flutter/flutter
println '===Os.FAMILY_WINDOWS: ' + Os.isFamily(Os.FAMILY_WINDOWS) // 判断当前的系统环境
//根据操作环境的不同选择执行 Flutter SDK/bin/flutter文件 还是 Flutter SDK/bin/flutter.bat文件
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
}
第一部分主要的操作如下:
1、为当前工程新增 profile、dynamicProfile、dynamicRelease 这三种构建类型,而且大部分属性都是从debug中拷贝过来的。
2、从Android根工程的local.properties文件下获取Flutter SDK的路径和版本号信息。并且根据当前系统的设置运行Flutter默认程序: Linux/Mac OS执行的是Flutter SDK/bin/flutter,Windows执行的是Flutter SDK/bin/flutter.bat。
第二部分代码如下:
//当前工程是否有localEngineOut属性,默认为false,因此下面代码暂时不分析
if (project.hasProperty('localEngineOut')) {
//...
} else {
//获取Flutter引擎文件路径 Flutter SDK/bin/cache/artifacts/engine
Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
println "=== 'target-platform :"+project.hasProperty('target-platform')//默认为false
//根据target-platform的配置选项,选择arm平台,默认为arm
String targetArch = 'arm'
if (project.hasProperty('target-platform') &&
project.property('target-platform') == 'android-arm64') {
targetArch = 'arm64'
}
//2、根据不同的配置 选择不同的jar包
//android-arm/flutter.jar文件 (debug专用)
debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile()
//android-arm-profile/flutter.jar文件
profileFlutterJar = baseEnginePath.resolve("android-${targetArch}-profile").resolve("flutter.jar").toFile()
//android-arm-release/flutter.jar文件
releaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-release").resolve("flutter.jar").toFile()
//android-arm-dynamic-profile/flutter.jar文件
dynamicProfileFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-profile").resolve("flutter.jar").toFile()
//android-arm-dynamic-release//flutter.jar文件
dynamicReleaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-release").resolve("flutter.jar").toFile()
println "===debugFlutterJar.isFile():"+debugFlutterJar.isFile() //true
//如果android-arm/flutter.jar非文件或不存在,则运行Flutter SDK/bin/flutter脚本。
//默认情况下debugFlutterJar文件都是存在的。
if (!debugFlutterJar.isFile()) {
project.exec {
executable flutterExecutable.absolutePath
args "--suppress-analytics"
args "precache"
}
if (!debugFlutterJar.isFile()) {
throw new GradleException("Unable to find flutter.jar in SDK: ${debugFlutterJar}")
}
}
//定位当前工程下输出目录中的intermediates/flutter/flutter-x86.jar文件
//注意,这里输出目录在根工程的build.gralde已经发生了改变,移动至Flutter工程的build/project下
// Add x86/x86_64 native library. Debug mode only, for now.
flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar")
println "====flutterX86Jar: " + flutterX86Jar// .../flutter_new/flutter_new/build/app/intermediates/flutter/flutter-x86.jar
//创建了一个任务,该任务的作用是把引擎目录下的x86 x64两个libflutter.so 打包成flutter-x86.jar包
//该jar包生成于.../flutter_new/flutter_new/build/app/intermediates/flutter/flutter-x86.jar
Task flutterX86JarTask = project.tasks.create("${flutterBuildPrefix}X86Jar", Jar) {
destinationDir flutterX86Jar.parentFile //压缩包生成的路径
archiveName flutterX86Jar.name //生成压缩包flutter-x86.jar的名称
//下面是要拷贝的两个so路径 和 拷贝后的位置
from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") {
into "lib/x86"
}
from("${flutterRoot}/bin/cache/artifacts/engine/android-x64/libflutter.so") {
into "lib/x86_64"
}
}
// Add flutter.jar dependencies to all Api configurations, including custom ones
// added after applying the Flutter plugin.
//重要:遍历buildTypes中的构建类型,并把根据当前构建类型,添加对应的jar包依赖
project.android.buildTypes.each {
println "====buildType:"+it.name //debug、dynamicProfile、dynamicRelease、profile、release
addFlutterJarApiDependency(project, it, flutterX86JarTask)
}
//设置监听,当新的构建类型加入的时候执行的依赖操作
project.android.buildTypes.whenObjectAdded {
addFlutterJarApiDependency(project, it, flutterX86JarTask)
}
}
第二部分的主要操作如下:
1、获取Flutter引擎文件路径Flutter SDK/bin/cache/artifacts/engine,该目录下存在按构建环境目录分类的flutter.jar文件,该jar包含了Android代码的所以依赖的Flutter类文件 和 asserts/icudtl.dat 资源文件 和 lib/libflutter.so 库文件:
2、如果构建环境的debug,那么还会把Flutter SDK//bin/cache/artifacts/engine/android-x86/libflutter.so 和Flutter SDK/bin/cache/artifacts/engine/android-x64/libflutter.so 这两个so生成一个 flutter-x86.jar,该jar包生成在Flutter根工程下的build/app/intermediates/flutter/目录下:
这个jar包的目录结构大概是这样的:
lib
-x86
-libflutter.so
-x86_64
-libflutter.so
生成该jar包后,就会该jar包加入到该工程的依赖环境中,见如下的依赖代码:
/**
*根据构建类型 添加指定的flutter.jar 包依赖
*/
private void addFlutterJarApiDependency(Project project, buildType, Task flutterX86JarTask) {
project.dependencies {
String configuration;
if (project.getConfigurations().findByName("api")) {
//plugin 3.0以上用api依赖
configuration = buildType.name + "Api";
} else {
//plugin 3.0以下的用compile依赖
configuration = buildType.name + "Compile";
}
//根据buildType的不同添加对应的jar包依赖,只依赖对应的构建类型jar包
add(configuration, project.files {
String buildMode = buildModeFor(buildType)
//这里~ 如果是debug构建类型,还添加了flutter-x86.jar的依赖
if (buildMode == "debug") {
[flutterX86JarTask, debugFlutterJar]
} else if (buildMode == "profile") {
profileFlutterJar
} else if (buildMode == "dynamicProfile") {
dynamicProfileFlutterJar
} else if (buildMode == "dynamicRelease") {
dynamicReleaseFlutterJar
} else {
releaseFlutterJar
}
})
}
}
因此可见如果是debug类型,相对于其他构建类型还增添了flutter-x86.jar的依赖,既多了两个so库。见编译后的apk工程结构:
第三分部如下:
//指定自己的DSL扩展,FlutterExtension包含source和code两个属性
project.extensions.create("flutter", FlutterExtension)
//工程配置完毕后执行addFlutterTask方法,该方法的作用是把Flutter的代码和资源工程加入到Android工程中
project.afterEvaluate this.&addFlutterTask
//.flutter-plugins默认情况下是没有配置的,因此忽略
File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
Properties plugins = readPropertiesIfExist(pluginsFile)
plugins.each { name, _ ->
....
}
第三步主要做的操作如下:
1、添加了Flutter插件的DSL扩展flutter{},其扩展的类是FlutterExtension,包含下面两个属性:
class FlutterExtension {
String source
String target
}
也就是说你可以用在工程的build.gradle中 使用 flutter { } 闭包来配置source和 target两个属性。
- source:用来配置当前Flutter工程的根路径,注意不是Android工程,如果没有配置抛出Must provide Flutter source directory异常。
- target:用来指定Flutter代码的启动入口,如果没有配置默认为lib/main.dart
2、在工程配置阶段结束后,执行addFlutterTask方法,该方法很重要,它的作用是把Flutter的代码和资源进行编译和处理并加入到Android工程中。
addFlutterTask方法
addFlutterTask方法仍然比较多,分为两部分进行讲解。
第一部分如下:
if (project.state.failure) {
return
}
if (project.flutter.source == null) {
throw new GradleException("Must provide Flutter source directory")
}
//target属性指定了,Flutter启动的程序入口,如果没有配置默认为lib/main.dart
String target = project.flutter.target
if (target == null) {
target = 'lib/main.dart'
}
if (project.hasProperty('target')) {
target = project.property('target')
}
Boolean verboseValue = null
if (project.hasProperty('verbose')) {
verboseValue = project.property('verbose').toBoolean()
}
String[] fileSystemRootsValue = null
if (project.hasProperty('filesystem-roots')) {
fileSystemRootsValue = project.property('filesystem-roots').split('\\|')
}
String fileSystemSchemeValue = null
if (project.hasProperty('filesystem-scheme')) {
fileSystemSchemeValue = project.property('filesystem-scheme')
}
Boolean trackWidgetCreationValue = false
if (project.hasProperty('track-widget-creation')) {
trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
}
String compilationTraceFilePathValue = null
if (project.hasProperty('precompile')) {
compilationTraceFilePathValue = project.property('precompile')
}
Boolean buildHotUpdateValue = false
if (project.hasProperty('hotupdate')) {
buildHotUpdateValue = project.property('hotupdate').toBoolean()
}
String extraFrontEndOptionsValue = null
if (project.hasProperty('extra-front-end-options')) {
extraFrontEndOptionsValue = project.property('extra-front-end-options')
}
String extraGenSnapshotOptionsValue = null
if (project.hasProperty('extra-gen-snapshot-options')) {
extraGenSnapshotOptionsValue = project.property('extra-gen-snapshot-options')
}
Boolean buildSharedLibraryValue = false
if (project.hasProperty('build-shared-library')) {
buildSharedLibraryValue = project.property('build-shared-library').toBoolean()
}
String targetPlatformValue = null
if (project.hasProperty('target-platform')) {
targetPlatformValue = project.property('target-platform')
}
第一部分的主要操作很简单,就是从project工程中读取各自配置属性例如:target、verbose、filesystem-roots、target-platform等等,以备后面运行的时候使用。这里尤其对target和source属性做了特殊处理,具体处理方式上面已经说过了。
第二部分如下(关键):
def addFlutterDeps = { variant -> //variant对应的构建类型
//获取当前的构建类型
String flutterBuildMode = buildModeFor(variant.buildType)
if (flutterBuildMode == 'debug' && project.tasks.findByName('${flutterBuildPrefix}X86Jar')) {
//...
}
//根据构建类型,创建任务flutterBuild[构建类型] 任务,该任务是用于编译flutter dart代码
FlutterTask flutterTask = project.tasks.create(name: "${flutterBuildPrefix}${variant.name.capitalize()}", type: FlutterTask) {
//下面这些都是属性赋值
flutterRoot this.flutterRoot
flutterExecutable this.flutterExecutable
//...中间忽略
//Flutter根工程路径
sourceDir project.file(project.flutter.source)
//Flutter编译的中间产物输出路径
intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
// ... /FlutterProject/flutter_new/flutter_new/build/app/intermediates/flutter/[debug,dynamicProfile,release..] 根据构建类型的不同
//...
}
// We know that the flutter app is a subproject in another Android app when these tasks exist.
//查找 :flutter:package[构建类型]Assets的Task【当flutter作为依赖库的时候,否则工程模式为null 】
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
//查找:flutter:cleanPackage[构建类型]Assets的Task【当flutter作为依赖库的时候,否则工程模式为null 】
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
//创建 copyFlutterAssets[构建类型] 的task的拷贝操作.
//同时依赖flutterTask、variant.mergeAssets (mergeDebugAssets)、 cleanMerge[构建类型]Assets
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
}
if (packageAssets) {
// Only include configurations that exist in parent project.
Task mergeAssets = project.tasks.findByPath(":app:merge${variant.name.capitalize()}Assets")
if (mergeAssets) {
mergeAssets.dependsOn(copyFlutterAssetsTask)
}
} else {
//最后processResources的task依赖copyFlutterAssetsTask
variant.outputs[0].processResources.dependsOn(copyFlutterAssetsTask)
}
}
if (project.android.hasProperty("applicationVariants")) {
//applicationVariants.all对应多种构建类型,添加addFlutterDeps闭包
project.android.applicationVariants.all addFlutterDeps
} else {
project.android.libraryVariants.all addFlutterDeps
}
该部分主要做的操作如下:
- 创建名为flutterBuild[构建类型] 的task任务,该任务的执行逻辑位于FlutterTask类当中的build方法中。
- 同时查找是否存在package[构建类型]Assets 和 cleanPackage[构建类型]Assets 这两个task。
- 创建copyFlutterAssets[构建类型]的task用于assert资源的拷贝操作,拷贝逻辑位于FlutterTask的getAssets方法。 同时该task依赖flutterBuild[构建类型] task 和 上面两个task,由于上面两个task在只要在flutter库作为依赖的时候才存在,flutter工程模式下这两个task都为null。因此,转而依赖variant.mergeAssets( merge[构建类型]Assets ) 和 cleanMerge[构建类型]Assets 这两个task进行构建。
- 让构建类型process[构建类型]Resources 依赖 copyFlutterAssets[构建类型] 。
可见在整个Gradle构建过程中插入很多Flutter自行的Task,因此上面task整个依赖关系如下:
process[构建类型]Resources 【Android任务】 -> copyFlutterAssets[构建类型] 【Flutter任务】 -> flutterBuild[构建类型] 【Flutter任务】 merge[构建类型]Assets 【Android任务】、 cleanMerge[构建类型]Assets 【Android任务】。
通过运行debug构建类型后的关系图如下: (使用了 cz.malohlava.visteg插件)
运行时候任务的构建顺序:
也就是说当 flutterBuild[构建类型] 任务使得flutter编译完成,并且 merge[构建类型]Assets执行完毕、也就是正常Android的assets处理完成后、flutter相应的产物就会被 copyFlutterAssets[构建类型]复制到 Flutter根工程/build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下。
flutter的编译产物,具体是由FutterTask的getAssets方法指定的:
CopySpec getAssets() {
return project.copySpec {
//Flutte根工程/build/app/intermediates/flutter/[debug,dynamicProfile,release..]
from "${intermediateDir}"
include "flutter_assets/**" // the working dir and its files
if (buildMode == 'release' || buildMode == 'profile') {
if (buildSharedLibrary) {
include "app.so"
} else {
include "vm_snapshot_data"
include "vm_snapshot_instr"
include "isolate_snapshot_data"
include "isolate_snapshot_instr"
}
}
}
}
也就是说copyFlutterAssets[构建类型] 任务的作用就是把 Flutte根工程/build/app/intermediates/flutter/[构建类型] 目录下面flutter_assets/目录中所有的内容都拷贝到 Flutter根工程/build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下。如果是release或者profile版本的话,还包含拷贝了Dart的二进制产物snapshot 或 app.so,可以看到,除了默认情况下的snapshot,我们还可以指定Dart产物为还可以编译成的so库形式。
下面贴出了debug类型 和 release类型的拷贝对比:
那么Flutter的这些产物是怎么生成的呢?那么就要看 flutterBuild[构建类型] 任务是如何构建的,也就是我们接下来要讲的FlutterTask类。
FlutterTask核心代码
FlutterTask继承自BaseFlutterTask,BaseFlutterTask是一个自定义的Task( DefaultTask),因此入口就在@TaskAction注解的build方法中,build方法直接调用BaseFlutterTask的buildBundle方法。
buildBundle方法代码也比较多,我们仍然分为两个部分。首先先看它的第一部分:
intermediateDir.mkdirs()
if (!sourceDir.isDirectory()) {
throw new GradleException("Invalid Flutter source 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}"
}
}
//....
这里,由于是release版本,因此会先编译aot的二进制Dart产物,也就是snapshot产物,实际是执行以下命令(release):
flutter build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir 工程路径/build/app/intermediates/flutter/release --release
执行完成后会生成以下的文件在release目录中:
接着,buildBundle方法的后半部分还会调用一次flutter命令,不过这次命令是所有编译模式都会调用:
//执行下面的任务
project.exec {
//Users/chenrongyi/Develop/flutter/flutter/bin/flutter
//执行bin/flutter程序
executable flutterExecutable.absolutePath
workingDir sourceDir
// 当前工程是有localEngineOut属性,才会有localEngine,默认为null
println "===localEngine:"+localEngine
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
args "build", "bundle"
args "--suppress-analytics"
args "--target", targetPath //Flutter启动的程序入口,默认为lib/main.dart
if (verbose) {
args "--verbose"
}
if (fileSystemRoots != null) {
for (root in fileSystemRoots) {
args "--filesystem-root", root
}
}
if (fileSystemScheme != null) {
args "--filesystem-scheme", fileSystemScheme
}
if (trackWidgetCreation) {
args "--track-widget-creation"
}
if (compilationTraceFilePath != null) {
args "--precompile", compilationTraceFilePath
}
if (buildHotUpdate) {
args "--hotupdate"
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"
}
if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
}
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
//注意,debug和release 分别执行的参数不同
if (buildMode == "release" || buildMode == "profile") {
args "--precompiled"
} else {
args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
}
//设置资源输出目录
args "--asset-dir", "${intermediateDir}/flutter_assets"
if (buildMode == "debug") {
args "--debug"
}
if (buildMode == "profile" || buildMode == "dynamicProfile") {
args "--profile"
}
if (buildMode == "release" || buildMode == "dynamicRelease") {
args "--release"
}
if (buildMode == "dynamicProfile" || buildMode == "dynamicRelease") {
args "--dynamic"
}
}
也就是执行了下面的命令(release):
flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --precompiled --asset-dir 工程路径/build/app/intermediates/flutter/release/flutter_assets --release
执行完成后,最终会生成一个flutter_assetss的资源目录在目录中:
上面通过两个命令最终生成的release资源产物与我们平时用release命令生成的结果是一致的。如果用要生成dubg模式下的产物那只要执行最后的命令代码,也就是执行下面的命令(debug):
flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --depfile 工程路径/build/app/intermediates/flutter/debug/snapshot_blob.bin.d --asset-dir 工程路径/build/app/intermediates/flutter/debug/flutter_assets --debug
执行后生成的文件资源如下:
注意执行上面的命令的时候要保证 intermediateDir文件目录已经创建,既...intermediates/flutter/debug/目录已经创建。命令执行的开头代码也显示的进行mkdir了。否则跑上面的命令会出现找不到目录的异常:
可以对比Release和Dubug模式下,上面的命令生成资源的对比:
在flutter_asserts目录中, 在debug模式下多了 isolate_snapshot_data、vm_snapshot_data、kernel_blob.bin文件 。而其中 isolate_snapshot_data、vm_snapshot_data这两个文件在release的外面目录中生成,除此之外还多了 isolate_snapshot_instr、 vm_snapshot_instr这两个文件。因此总的来说realease模式比debug模式多了 isolate_snapshot_instr、 vm_snapshot_instr 这两个文件,这两个文件属于AOT的指令段文件。
生成上面的Flutter构建产物后,就会执行下面的拷贝操作,也就是我们上一小结提到过的FutterTask的getAssets方法 负责把这些文件拷贝到 工程文件路径/build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下 参与assets资源的编译中!
merged_assets中的这些文件就是最后都会打包到apk的assets目录下。下面对两个构建版本做了下简单的对比:
八、总结
总结下 Flutter工程混编进入Android工程整个流程大概如下:
- 读取根工程下local.properties获取Flutter SDK路径和版本信息。
- 添加三种构建模式:dynamicProfile、dynamicRelease、profile。
- 为工程添加flutter.jar包的依赖,该包包含flutter类文件、icudtl.dat资源文件和lib/libflutter.so 库文件
- 如果是debug版本,那么还把引擎库下的android-x64/libflutter.so 和 android-x86/libflutter.so 这两so打包成一个flutter-x86.jar,同时该jar包也作为依赖。
- 执行Flutter命令构建Flutter产物,把生成的产物通过拷贝任务拷贝至merged_assets下。生成的产物包括flutter_asserts下等资源文件 和 snapshot 程序数据段。
- 最后Android的资源处理任务会把merged_assets下所有flutter产物都打包到apk中asserts目录下,最终完成Flutter工程的混编工作。
九、关于Flutter 1.2.1的补充
以上主要是针对Flutter 1.0 gradle脚本进行的分析。不过前些天Flutter推出了1.2.1版本,对flutter.gradle脚本文件也新增了某些的修改,不过总体来说影响并不大,主要是对一些BUG的修复。
1、新增了mainModuleName动态属性,用来指定主project的工程名:
默认情况下主proejct的工程名为app,如果用于擅自修改了工程名,那么就会出现编译异常的情况,见 issues 26948 。并且在 pull request 27154 修复了该问题,如果主工程名变更,那么只要在setBinding中传入主app名即可:
2、解决了当Flutter工程作为aar的依赖时,没有把icudtl.dat文件引入到aar中的问题,见issues18025。
创建一个名为 copySharedFlutterAssets[构建类型] 的task,该task的作用是把flutter.jar包下assets/flutter_shared下所有文件都拷贝出来。因为在Flutter 1.0的版本中当作为aar进行打包的时候,jar包下的assets资源不会打包到aar包中,因此这里做了修复 (不过我发现在升级到1.2.1后,flutter.jar中已经不存在assets目录,icudtl.dat文件已经被移除, 现在已经被嵌入到 libflutter.so文件中了: Remove the flutter_shared assets directory from the Gradle script )
欢迎关注我的公众号【不喝咖啡的程序员】,最新的文章会在上面发布: