漫读flutter.gradle脚本。
项目中,通过implementation project(':flutter')的方式,在主工程的build.gradle目录下,以模块的方式引入了一个flutter工程。
而在flutter工程的build.gradle中,直接通过apply from引入了flutter.gradle脚本:
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
因此,一般你可以在这个目录找到它:
flutterRoot对应的就是flutter SDK的目录,而在Android项目中,集成、编译Flutter工程,自然而然地离不开flutter.gradle的参与。
我们可以在上述的路径中,找到flutter.gradle文件,并拖到idea或者Android Studio中打开。
一、目录
整个脚本文件的全貌还是比较简单的:
一共七个部分,乍一看我们不难看出:
- FlutterExtensions是一个配置类,一些省略的数据可以更加灵活地去设置;
- buildScript、android闭包就更不用说了,几乎每个Android开发都知道它们的含义;
- Apply plugin,显然就是应用了下面的FlutterPlugin;
- FlutterPlugin显然就是我们Flutter嵌入Android工程(Android的Gradle工程)的核心;
- BaseFlutterTask,显然就是一个Flutter的GradleTask的抽象表达。
- 而FlutterTask就是一个对应的实现,其中实现了类似于获取输入/输出目录、获取assets资源目录等等功能的具体实现。
显然,FlutterPlugin就是flutter.gradle运行的核心。
二、FlutterPlugin
有尝试过学习Gradle的同学一定对这串代码不陌生:
class FlutterPlugin implements Plugin<Project>{//...}
这是用来声明一个自定义Gradle插件的Gradle类,因为它类似Java的语法使得本身能够被Java/Kotlin开发者所读懂,一个完整的Gradle插件的定义应该如下:
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
// ....
}
}
我们需要对传入的apply对象进行配置,自然我们会想要去阅读apply相关的方法,其实不多,大概就283-121,大约162行。
三、addFlutterDeps
3.1 isFlutterAppProject
apply方法一开始就通过isFlutterAppProject方法对当前的项目文件结构进行判断,如果当前的Gradle Properties中,含有applicationVariants参数,那么就会去执行项目根目录下的gradlew程序:
def gradlew = (OperatingSystem.current().isWindows()) ?
"$ { rootProject.projectDir } /gradlew.bat" : "$ { rootProject.projectDir } /gradlew"
rootProject.exec {
workingDir rootProject.projectDir
executable gradlew
args ":$ { subproject.name } :dependencies", "--write-locks"
}
workingDir指定了工作目录,类似于Shell的cd操作,切换到项目的根目录,然后执行Windows下的批处理程序gradlew.bat或者是其它平台的gradlew程序,然后再其后拼接了args参数。
至此我们其实已经有所收获了,我们知道如何在Gradle中,通过指定工作目录、程序和参数去执行一个程序了,def gradlew本身是在声明一个路径,一个脚本文件的路径,然后.exec函数可以帮助我们去执行一些东西。
3.2 FLUTTER_STORAGE_BASE_URL与镜像仓库
在flutter官方文档中,通常会建议我们将Flutter的镜像地址,替换为国内的镜像地址:
在这里我通过在系统的环境变量里面设置了一个FLUTTER_STORAGE_BASE_URL变量,在flutter.gradle脚本中,会通过System.env.FLUTTER_STORAGE_BASE_URL把这个环境变量读出来,然后通过为rootProject统一配置一个基于该域名的仓库。
3.3 FlutterExtensions
接下来的一段代码是:
project.extensions.create("flutter", FlutterExtension)
即向工程注册了一个FlutterExtensions扩展,并命名为flutter,这里的FlutterExtension,其实就是flutter.gradle开头声明的第一份部分,包括如下的内容:
class FlutterExtension {
static int compileSdkVersion = 31
static int minSdkVersion = 16
static int targetSdkVersion = 31
String source
String target
}
3.4 Flutter相关的Task1:compileTask
我们知道,Gradle通过构建Task与Task间的先后关系,生成一个有向无环图(DAG)来完成有序的构建,如果项目嵌入了Flutter,那么Flutter的构建本身,也会作为Android工程构建的一个或者多个步骤,在众多task之间,按既定顺序排列,显然,接下来就是整个FlutterGradle构建的关键。
接下来的100行左右都是在从project.property中取一些配置值,例如:
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')
}
……
这里获取到的一些数值,例如fileSystemRootsValue、fileSystemSchemeValue等等,会统一在后续使用。
接下来,会通过如下的代码完成对Task的创建:
String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask){
...
}
仅通过阅读我们可能很难看出taskName最终的取值是什么,这个时候println大法就派上用场了:
println "(from_source_code)-> $taskName"
我们在运行自己的Flutter项目之后,这里就会输出:
(from_source_code)-> compileFlutterBuildDebug
这里的compile和FLUTTER_BUILD_PREFIX(常量:flutterBuild)的值都是固定的,而最后的debug,则取自你的variant.name,即Android工程配置的变体的名称,如果你在这里选择构建的是debug模式的包,那么就是debug;如果选择构建release模式,这里就会是complileFlutterBuildRelease。
project.tasks.create的一次完整调用其实后面还跟着一个闭包,大概30多行:
FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
flutterRoot this.flutterRoot // / Users / yzyi / sdk / flutter
flutterExecutable this.flutterExecutable // / Users / yzyi / sdk / flutter / bin / flutter
buildMode variantBuildMode // debug
localEngine this.localEngine // null
localEngineSrcPath this.localEngineSrcPath // null
targetPath getFlutterTarget() // lib/main.dart
verbose isVerbose() // false
fastStart isFastStart() // false
fileSystemRoots fileSystemRootsValue // null
fileSystemScheme fileSystemSchemeValue // null
trackWidgetCreation trackWidgetCreationValue // true
targetPlatformValues = targetPlatforms // [android - arm, android - arm64, android - x64]
sourceDir getFlutterSourceDirectory() //Users/yzyi/workdir/{your_project_path}
intermediateDir project.file("$ { project.buildDir } /$ { AndroidProject.FD_INTERMEDIATES } /flutter/$ { variant.name } /")
//Users/yzyi/workdir/{your_project_path}/.android/Flutter/build/intermediates/flutter/debug/
extraFrontEndOptions extraFrontEndOptionsValue // null
extraGenSnapshotOptions extraGenSnapshotOptionsValue // null
splitDebugInfo splitDebugInfoValue // null
treeShakeIcons treeShakeIconsOptionsValue // false
dartObfuscation dartObfuscationValue // false
dartDefines dartDefinesValue // null
bundleSkSLPath bundleSkSLPathValue // null
performanceMeasurementFile performanceMeasurementFileValue // null
codeSizeDirectory codeSizeDirectoryValue // null
deferredComponents deferredComponentsValue // false
validateDeferredComponents validateDeferredComponentsValue // true
doLast {
project.exec {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine('cmd', '/c', "attrib -r $ { assetsDirectory } /* /s")
} else {
commandLine('chmod', '-R', 'u+w', assetsDirectory)
}
}
}
}
执行一次debug构建的参数大致上已经写上去了,其实就是对这一次的任务创建的配置构建。
最后,在doLast闭包中,向构建的尾部加入了一个commandLine语句,chmod本意是在修改一个文件的访问权限:chmod -R u+w该指令表示-R:递归地向下,u+w表示给用户加上写入的权限,而对应的目录,就是assetsDirectory,它是一个在FlutterTask中定义的内容,而它的构成,则又是通过上面提到的**intermediateDir/flutter_assets**构成的:
在doLast阶段为flutter_assets的输出目录为当前用户加上写权限。
3.5 Flutter相关的Task2:packFlutterAppAotTask
到这一步flutter.gradle会再创建一个新的Task,首先会找到一个Jar包:
File libJar = project.file("$ { project.buildDir } /$ { AndroidProject.FD_INTERMEDIATES } /flutter/$ { variant.name } /libs.jar")
它的文件路径是:
/Users/yzyi/workdir/{your_project_path}/.android/Flutter/build/intermediates/flutter/debug/libs.jar
我们可以猜测,这部分就是Flutter中Java部分的代码,随后创建了一个新的task:
Task packFlutterAppAotTask = project.tasks.create(name: "packLibs$ { FLUTTER_BUILD_PREFIX } $ { variant.name.capitalize() } ", type: Jar) {
destinationDir libJar.parentFile
archiveName libJar.name
dependsOn compileTask
targetPlatforms.each { targetPlatform ->
String abi = PLATFORM_ARCH_MAP[targetPlatform]
from("$ { compileTask.intermediateDir } /$ { abi } ") {
include "*.so"
// Move `app.so` to `lib/<abi>/libapp.so`
rename { String filename ->
return "lib/$ { abi } /lib$ { filename } "
}
}
}
}
首先,我们可以看到它的顺序是依赖于complieTask的,其次,这是一个jar类型的task,结合它的名字:packFlutterAppAotTask,我们可以猜它的作用就是把指定的内容打入对应的jar包,而destinationDir则指定了jar文件的地址,archiveName则指定了jar产物的名称。
而最后一个targetPlatforms.each所做的事情,就是遍历所有的目标平台,以上面的内容为例,我们可以看到目标平台的取值如下:
targetPlatformValues = // [android - arm, android - arm64, android - x64]
首先会从PLATFORM_ARCH_MAP这个map中拿到对应平台的abi名称,然后再将对应平台的地址:intermediateDir/${abi}`取出所有*.so文件,复制到目的地址:lib/{fileName}"。正如注释中所说的那样:
// Move `app.so` to `lib/<abi>/libapp.so`
就是一个将app.so复制到目的文件地址的过程。
但是,这个任务并不是和compileTask本身一样,它并不是任何时候都触发的,我们知道,debug模式下的libapp.so其实并不存在,转而被一种特殊的二进制文件所替代(为了能够支持JIT实现快速的热重载),正如这个Task名字描述的那样:packFlutterAppAotTask,这其实是一个用于移动位于临时地址的对应/libapp.so到目的lib//libapp.so地址的任务。
如果你的Flutter工程以Release、Profile模式执行,那么你的Flutter代码就会以libapp.so二进制的程序存在,这里会对对应的libapp.so进行拷贝。
> Task :flutter:packLibsflutterBuildProfile
copy:/Users/yzyi/{your_project_dir}/flutter_module/.android/Flutter/build/intermediates/flutter/profile/armeabi-v7a/app.so to lib/armeabi-v7a/libapp.so
> Task :flutter:packLibsflutterBuildProfile
copy:/Users/yzyi/{your_project_dir}/flutter_module/.android/Flutter/build/intermediates/flutter/profile/arm64-v8a/app.so to lib/arm64-v8a/libapp.so
> Task :flutter:packLibsflutterBuildProfile
copy:/Users/yzyi/{your_project_dir}/flutter_module/.android/Flutter/build/intermediates/flutter/profile/x86_64/app.so to lib/x86_64/libapp.so
我们可以看到,这个方法将三个对应平台、ABI的libapp.so从flutter_module中的临时文件夹(intermediates)复制到Jar包中,最终他们都会进入到我们之前提到的libs.jar当中:
最后这个Jar包在Gradle中会被我们的主工程所引用,这些so文件也会被添加到我们最终的apk当中。
但是,即使debug模式下也会对对应的libs进行打包,但是debug模式下Flutter并不依赖于libapp.so进行运行,因此,我们看看打出来的libs.jar中含有什么内容:
也没啥有用的内容,那么我们的Flutter代码呢?我们之前在juejin.cn/post/718953… "kernel_blob.bin" 的形式存在,这也是一个二进制文件。
其实平时我们对这一类非常规文件,我们一般都把他扔在assets里面。比如一些存储在本地的.json、字体.ttf等等,我们直接看intermediates/flutter/debug/flutter_assets中的文件,就能找到这个
kernel_blob.bin
相比profile版本的flutter_assets,debug版本的flutter_assets文件夹中多了这些内容:
-rw-r--r--@ 1 yzyi staff 4.8M 3 24 2022 isolate_snapshot_data
-rw-r--r--@ 1 yzyi staff 66M 4 24 14:18 kernel_blob.bin
-rw-r--r--@ 1 yzyi staff 11K 3 24 2022 vm_snapshot_data
hot reload的实现逻辑,就是将新编译出来的二进制文件推送到设备上以实现快速的热重载,显然,如果我们手动去替换assets的内容也能实现手动的替换。
3.6 构建FlutterPlugin插件(构建AAR)
boolean isBuildingAar = project.hasProperty('is-plugin')
首先,properties中的属性:is-plugin用来标记是否将本Flutter工程构建为一个AAR包,给其他Android 工程引入,由于这里是直接引入了一个Flutter工程,这里的Flutter_module就不会去打成AAR包了,因此isBuildingAar的取值会是false。
接下来声明了两个task:
- packageAssets
- cleanPackageAssets
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
isUsedAsSubproject这个变量主要用于甄别当前的Flutter工程,是否被用作一个子工程。
我们知道,一般来说Android工程会有一个主工程,并且它的工程名称一定是:app。这样一来:app就会是主工程,而:flutter则会变成子工程。
原文注释是:
We know that `:flutter` is used as a subproject when these tasks exists and we aren't building an AAR.
大致上就是说,当我们在构建AAR并且:app和:flutter同时存在的时候,:flutter目录才会是子工程。
根据这个点,我们创建了第三个Task:
Task copyFlutterAssetsTask = project.tasks.create(
name: "copyFlutterAssets$ { variant.name.capitalize() } ",
type: Copy,
){
// ......
}
它的类型是Copy,名字也是Copy,就是一个搬运文件的Task,在debug模式下,它的名称是:copyFlutterAssetsDebug,省略号构成的Closure中,同样对闭包进行了配置:
{
dependsOn compileTask
with compileTask.assets
if (isUsedAsSubproject) {
dependsOn packageAssets
dependsOn cleanPackageAssets
into packageAssets.outputDir
return
}
// `variant.mergeAssets` will be removed at the end of 2019.
def mergeAssets = variant.hasProperty("mergeAssetsProvider") ?
variant.mergeAssetsProvider.get() : variant.mergeAssets
dependsOn mergeAssets
dependsOn "clean$ { mergeAssets.name.capitalize() } "
mergeAssets.mustRunAfter("clean$ { mergeAssets.name.capitalize() } ")
into mergeAssets.outputDir
}
首先会依赖于compileTask任务,也就是Flutter核心的构建任务。
其次会连接到compileTask的assets目录:
@Internal
CopySpec getAssets() {
return project.copySpec {
from "$ { intermediateDir } "
include "flutter_assets/**" // the working dir and its files
}
}
接下来会根据是否子工程构建(isUsedAsSubProject变量)走两条不同的构建路径:
- 子工程构建:
if (isUsedAsSubproject) {
dependsOn packageAssets
dependsOn cleanPackageAssets
into packageAssets.outputDir
return
}
比较明显就是依赖于上面提到的两个子工程构建相关的任务,完成后输出到AAR包对应的outputDir目录中。
- 非子工程构建:
// `variant.mergeAssets` will be removed at the end of 2019.
def mergeAssets = variant.hasProperty("mergeAssetsProvider") ?
variant.mergeAssetsProvider.get() : variant.mergeAssets
dependsOn mergeAssets
dependsOn "clean$ { mergeAssets.name.capitalize() } "
mergeAssets.mustRunAfter("clean$ { mergeAssets.name.capitalize() } ")
into mergeAssets.outputDir
和子工程构建比较相似,区别就在于构建的产物最终会写到mergeAsset相关的目录中。
至此copyFlutterAssetsTask就算完成了,其实就是一个Copy Flutter Asset的过程。
接下来的代码会从variant的输出中拿到processResourcesProvider,并将它依赖到copyFlutterAssetsTask上。
至此本文最大的一个部分:addFlutterDeps的分析就完成了。但是addFlutterDeps本身也只是一个Closure,它会在未来的某个时间去执行,上述的内容都只是定义罢了:
...上文
def addFlutterDeps = { variant ->
/// 上面的分析
} // end def addFlutterDeps
下文...
其实关于compileTask章节还有一个很重要的内容没有提到,compileTask是如何完成
app.so文件的构建的?
四、FlutterApk的构建相关
最终将compileTask添加到构建的代码其实直到这里才开始。
Apk的构建,同样划分了两种情况,如果是满足isFlutterAppProject的项目,最后会走一套Flutter为主工程的流程(单独工程);而其余情况则会走Flutter作为子工程的构建流程,需要和主工程(:app)做交互(作为子工程进行嵌入)。
4.1 主工程
首先会通过project.android.applicationVariants.all去遍历当前Flutter主工程中的所有Application变体。拿到变体之后,会作为参数,输入到addFlutterDeps当中去:
Task copyFlutterAssetsTask = addFlutterDeps(variant)
def variantOutput = variant.outputs.first()
def processResources = variantOutput.hasProperty("processResourcesProvider") ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
然后将资源处理程序依赖到copyFlutterAssetsTask任务执行完成之后。
最终,再通过variant.outputs.all去遍历该变体下的所有输入文件,在对应的assembleTask的末尾插入一个Closure,然后就是一个根据ABI、变体等等对APK文件进行重命名的逻辑,完成之后将文件拷贝到outputs目录。
4.2 嵌入工程
嵌入工程中,会先尝试通过findProject方法去拿到主工程的Gradle Project对象:
String hostAppProjectName = project.rootProject.hasProperty('flutter.hostAppProjectName') ? project.rootProject.property('flutter.hostAppProjectName') : "app"
Project appProject = project.rootProject.findProject(":$ { hostAppProjectName } ")
然后在appProject的afterEvaluate阶段做如下的操作:
project.android.libraryVariants.all { libraryVariant ->
Task copyFlutterAssetsTask
appProject.android.applicationVariants.all { appProjectVariant ->
Task appAssembleTask = getAssembleTask(appProjectVariant)
if (!shouldConfigureFlutterTask(appAssembleTask)) {
return
}
String variantBuildMode = buildModeFor(libraryVariant.buildType)
if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
return
}
if (copyFlutterAssetsTask == null) {
copyFlutterAssetsTask = addFlutterDeps(libraryVariant)
}
Task mergeAssets = project
.tasks
.findByPath(":$ { hostAppProjectName } :merge$ { appProjectVariant.name.capitalize() } Assets")
assert mergeAssets
mergeAssets.dependsOn(copyFlutterAssetsTask)
}
}
这里和构建主工程的代码逻辑上还是比较相似的,步骤也是优先获取assembleTask、执行addFlutterDeps方法最后通过mergeAssets完成资源合并。
4.3 嵌入式Flutter项目和主工程构建模式分离
如何让Android的主工程以Debug模式运行,而内嵌的Flutter工程以Release/Profile模式运行?
官方的计数器demo可以使用flutter run --release指令执行run一个release模式的包,但是我们在Android主工程中以Gradle的形式,直接引入一个flutter项目工程,我们似乎没有直接指定--release的地方。
其实观察compleTask提供的参数,我们不难发现,它的第三行就指定了构建模式为:variantBuildMode,而variantBuildMode又是根据variant.buildType得到的:
private static String buildModeFor(buildType) {
if (buildType.name == "profile") {
return "profile"
} else if (buildType.debuggable) {
return "debug"
}
return "release"
}
其实关键之处,就在于variant.buildType,如果它的name == "profile"那么最终的产物就会以profile模式执行;如果它是可以debug的(debuggable的),那么最终的产物就会是debug,其余场景都会是release模式。
为了速度的话,可以直接注释掉其他行,只保留最后一行,这样打包出来的产物就可以是单独地以Release模式运行的Flutter模块了。
正确的做法,自然是去修改variant.buildType的值。我们知道,Variant,变体是在主工程的build.gradle中通过android闭包声明、指定的:
android{
buildTypes {
debug {
debuggable true
}
release {
debuggable false
}
......
}
这里的debug和release,其实就是我们主工程的两个变体,name就是他们的名称本身:debug和release,我们并没有一个名为profile的变体名称,因为一般我们的需求就只需要两个变体版本debug和release。
因此,结合**buildModeFor** 的实现,我们其实选择不多,如果我们想要主工程debuggable,那么我们就不能修改debuggable的值, 显然主工程也会依赖这个值来判断是否加入debug信息,代码中的原文注释如下:
// This mapping is based on the following rules:
// 1. If the host app build variant name is
profilethen the equivalent// Flutter variant is
profile.// 2. If the host app build variant is debuggable
// (e.g.
buildType.debuggable = true), then the equivalent Flutter// variant is
debug.// 3. Otherwise, the equivalent Flutter variant is
release.
因此,我们只能新增一个variant:profile,它的配置和debug一模一样:
android{
buildTypes {
profile {
debuggable true
}
debug {
debuggable true
}
release {
debuggable false
}
......
}
但是由于**buildModeFor** 会优先判断名称,profile 这个变体能够忽略debuggable属性的判断,从而达到主工程是debuggable的,而flutter工程是采用AOT编译的profile模式。
五、compileTask与FlutterTask
我们前面虽然已经介绍了compileTask,但是你会发现一个事情,我们从compileTask几乎是毫无征兆地就跳到了libs.jar的包装过程Task:packFlutterAppAotTask,而完全没有提到libs.jar中的app.so是如何构建的,这个其实已经被定义在FlutterTask当中了,这也是本文的最后一个部分:BaseFlutterTask与FlutterTask。
BaseFlutterTask已经定义了很多的属性内容,在FlutterTask实现它之后就可以对这些属性内容进行重写以实现多态,但是真正和构建相关的,其实是buildBundle方法,整体内容不多,也就70多行,逻辑也比较简单,大致如下:
- 创建
intermediateDir文件夹; project.exec去执行构建脚本;
这里的关键点其实在project.exec,我们之前在调用Gradlew的时候已经走过这个方法了,核心就三个点:
- 工作目录:workingDir
- 可执行文件/脚本文件:executable
- 参数:arg
它基本上等价于我们直接在shell中做如下操作:
cd $workingDir
$executable $arg // 用可执行完成执行程序,以args为参数
flutter的构建使用的就是flutter.bat或者flutter脚本文件,不难想想它的参数格式就是:
cd flutter_sdk_dir
flutter $args
我们只需要将参数打印出来,我们就可以自己完成flutter源码->app.so或者kernel_blob.bin的构建过程:
println "(from_source_code)-> plzRunExecute:$ { flutterExecutable.absolutePath } $ { args } ,$ { ruleNames.toString() } in $ { sourceDir } "
然后我们打开一个终端,然后手动CD到sourceDir的目录,然后执行上述的指令:
/Users/yzyi/sdk/flutter/bin/flutter\
assemble\
--no-version-check\
--quiet\
--depfile /Users/yzyi/{your_flutter_module_dir}/.android/Flutter/build/intermediates/flutter/release/flutter_build.d\
--output /Users/yzyi/{your_flutter_module_dir}/.android/Flutter/build/intermediates/flutter/release\
-dTargetFile=lib/main.dart\
-dTargetPlatform=android\
-dBuildMode=release\
-dTrackWidgetCreation=true\
android_aot_bundle_release_android-arm android_aot_bundle_release_android-arm64 android_aot_bundle_release_android-x64
其中的参数:android_aot_bundle_release_android-arm android_aot_bundle_release_android-arm64 android_aot_bundle_release_android-x64**部分其实是ruleNames的参数,其余的参数绝大多数都是在对assemble进行配置。
经过这个步骤之后,你就可以拿到在packFlutterAppAotTask中被用来出来的app.so了,也就是我们程序对应的libapp.so。