一、引言
本文主要對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 <buildType>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 )
歡迎關注我的公眾號【不喝咖啡的程序員】,最新的文章會在上面發布: