揭開Flutter工程編譯的面紗(Android篇)

一、引言

本文主要對Flutter工程編譯時如何把Flutter生成的產物打包進入Android工程中進行分析。在Flutter工中打包過程中涉及到了local.properties、settings.gradle、build.gradle、flutter.gradle這幾個腳本文件的參與,與傳統的Android工程相比這幾個腳本文件中的內容也不相同,接下來我們通過一層層解析,解開Flutter工程編譯的面紗。 同時也建議大家看的時候搭配Flutter工程一起食用效果更佳。

二、工程結構分析

首先我們創建了一個最普通的Flutter工程flutter_new,創建后整個工程的目錄結構如下:


image

Flutter工程下包括了Android和IOS兩個目錄,分別用于運行在Android和IOS平臺上。其中android目錄結果與Android工程的目錄結構是一樣的。Flutter工程中的android目錄下包含了兩個工程:第一個是android根工程,第二個是app子工程

image

稍微有點不同的地方在于這兩個工程的的輸出目錄搬到了Flutter工程根目錄build下:

image

Flutter工程中android工程與傳統的Android工程相比,都有.gradle、gradle、 setting.gradlew、gradlew等目錄和文件,文件基本上都是一樣的,在但是在local.properties、settings.gradle、build.gradle文件內容上又有所不同,接下來我們我們會一一做對比。

三、local.properties

image

在根工程下的local.properties文件中了多了Flutter SDK相關的配置信息,包括SDK的路徑、版本名、版本號,這些信息在構建工程的過程自動從環境變量中獲取的,我們無需手動配置。
image

四、根工程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目錄再建立屬于自己名稱的輸出目錄。可以看下面這張圖:

image

第二點:所有子工程都配置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中,接下來我們就揭開它的神秘面紗:

image

flutter.gradle代碼分為了有兩大核心部分:FlutterPluginFlutterTask

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中拷貝過來的。

image

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 庫文件:

image

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/目錄下:

image

這個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工程結構:

image

第三分部如下:

      //指定自己的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 { } 閉包來配置sourcetarget兩個屬性。

  • 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
        }

該部分主要做的操作如下:

  1. 創建名為flutterBuild[構建類型] 的task任務,該任務的執行邏輯位于FlutterTask類當中的build方法中
  2. 同時查找是否存在package[構建類型]Assets 和 cleanPackage[構建類型]Assets 這兩個task。
  3. 創建copyFlutterAssets[構建類型]的task用于assert資源的拷貝操作,拷貝邏輯位于FlutterTask的getAssets方法。 同時該task依賴flutterBuild[構建類型] task 和 上面兩個task,由于上面兩個task在只要在flutter庫作為依賴的時候才存在,flutter工程模式下這兩個task都為null。因此,轉而依賴variant.mergeAssets( merge[構建類型]Assets )cleanMerge[構建類型]Assets 這兩個task進行構建。
  4. 讓構建類型process[構建類型]Resources 依賴 copyFlutterAssets[構建類型] 。

可見在整個Gradle構建過程中插入很多Flutter自行的Task,因此上面task整個依賴關系如下:
process[構建類型]Resources 【Android任務】 -> copyFlutterAssets[構建類型] 【Flutter任務】 -> flutterBuild[構建類型] 【Flutter任務】 merge[構建類型]Assets 【Android任務】、 cleanMerge[構建類型]Assets 【Android任務】。

通過運行debug構建類型后的關系圖如下: (使用了 cz.malohlava.visteg插件)

image

運行時候任務的構建順序:
image

也就是說當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類型的拷貝對比:


image

image

那么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目錄中:

image

接著,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的資源目錄在目錄中:

image

上面通過兩個命令最終生成的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

執行后生成的文件資源如下:


image

注意執行上面的命令的時候要保證 intermediateDir文件目錄已經創建,既...intermediates/flutter/debug/目錄已經創建。命令執行的開頭代碼也顯示的進行mkdir了。否則跑上面的命令會出現找不到目錄的異常:


image

可以對比Release和Dubug模式下,上面的命令生成資源的對比:

image

在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資源的編譯中!


image

merged_assets中的這些文件就是最后都會打包到apk的assets目錄下。下面對兩個構建版本做了下簡單的對比:


image

八、總結

總結下 Flutter工程混編進入Android工程整個流程大概如下:

  1. 讀取根工程下local.properties獲取Flutter SDK路徑和版本信息。
  2. 添加三種構建模式:dynamicProfile、dynamicRelease、profile
  3. 為工程添加flutter.jar包的依賴,該包包含flutter類文件、icudtl.dat資源文件和lib/libflutter.so 庫文件
  4. 如果是debug版本,那么還把引擎庫下的android-x64/libflutter.so 和 android-x86/libflutter.so 這兩so打包成一個flutter-x86.jar,同時該jar包也作為依賴。
  5. 執行Flutter命令構建Flutter產物,把生成的產物通過拷貝任務拷貝至merged_assets下。生成的產物包括flutter_asserts下等資源文件 和 snapshot 程序數據段
  6. 最后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的工程名:

image

默認情況下主proejct的工程名為app,如果用于擅自修改了工程名,那么就會出現編譯異常的情況,見issues 26948 。并且在pull request 27154 修復了該問題,如果主工程名變更,那么只要在setBinding中傳入主app名即可:

2、解決了當Flutter工程作為aar的依賴時,沒有把icudtl.dat文件引入到aar中的問題,見issues18025

image

創建一個名為 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

歡迎關注我的公眾號【不喝咖啡的程序員】,最新的文章會在上面發布:


image
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容