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

一、引言

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

二、工程結(jié)構(gòu)分析

首先我們創(chuàng)建了一個(gè)最普通的Flutter工程flutter_new,創(chuàng)建后整個(gè)工程的目錄結(jié)構(gòu)如下:


image

Flutter工程下包括了Android和IOS兩個(gè)目錄,分別用于運(yùn)行在Android和IOS平臺(tái)上。其中android目錄結(jié)果與Android工程的目錄結(jié)構(gòu)是一樣的。Flutter工程中的android目錄下包含了兩個(gè)工程:第一個(gè)是android根工程,第二個(gè)是app子工程

image

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

image

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

三、local.properties

image

在根工程下的local.properties文件中了多了Flutter SDK相關(guān)的配置信息,包括SDK的路徑、版本名、版本號(hào),這些信息在構(gòu)建工程的過程自動(dòng)從環(huán)境變量中獲取的,我們無需手動(dòng)配置。
image

四、根工程settings.gradle

如果你有配置Flutter工程根目錄下.flutter-plugins這個(gè)文件,那么下面的操作就會(huì)把flutter插件用到的第三方工程include到當(dāng)前的工程中,并為其配置工程的路徑 projectDir:

include ':app'

//1、根工程的父目錄,既Flutter工程目錄
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()

//2、把Flutte工程下.flutter-plugins文件內(nèi)容讀取到內(nèi)存中
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插件工程包含到當(dāng)前工程中
plugins.each { name, path ->
    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
    include ":$name"
    project(":$name").projectDir = pluginDirectory
}

然,我們新創(chuàng)建的Flutter工程默認(rèn)是沒有.flutter-plugins這個(gè)文件的,所以上面的代碼基本不走。

五、根工程build.gradle

根工程中的build.grandle主要的工作是重新配置了工程的輸出目錄 和 工程間配置執(zhí)行時(shí)的依賴關(guān)系:

buildscript {//...}

allprojects {//...}
 
//上面的代碼是基本一樣的

//第一點(diǎn)
rootProject.buildDir = '../build'  //根工程輸出路徑
subprojects {  //所有子工程輸出路徑
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
//第二點(diǎn)
subprojects {
    project.evaluationDependsOn(':app')  //為所有子工程配置app的依賴
}

//第三點(diǎn)
task clean(type: Delete) {
    delete rootProject.buildDir
}

第一點(diǎn):配置了根工程 和 其所有子工程的輸出路徑。把所有的輸出路徑都搬到了Flutter根目錄下的build目錄中,如果是子工程則在build目錄再建立屬于自己名稱的輸出目錄。可以看下面這張圖:

image

第二點(diǎn):所有子工程都配置app子工程的依賴,既讓所有子工程運(yùn)行配置階段開始之前都要保證app工程的配置階段都已經(jīng)運(yùn)行完畢。這樣做的好處就是保證app工程的配置屬性優(yōu)先導(dǎo)入,防止其他子工程出現(xiàn)屬性找不到的問題發(fā)生。

第三點(diǎn): 為根工程添加clean任務(wù)用來刪除build目錄下所有文件。

關(guān)于Project#evaluationDependsOn方法

evaluationDependsOn用于配置Project對(duì)象之間的依賴關(guān)系,跟Task的dependsOn原理一樣。

舉個(gè)例子,比如有兩個(gè)工程app工程和lib工程,其中app依賴lib工程。在lib工程的build.gradle 添加如下的屬性:

rootProject.ext.producerMsg = "Hello"

在app工程的build.gradle 添加如下的代碼,既app工程使用lib工程的動(dòng)態(tài)屬性:

def msg = rootProject.ext.producerMsg

如果在在配置階段app 工程先運(yùn)行,這樣就會(huì)導(dǎo)致app會(huì)導(dǎo)致producerMsg屬性沒有找到!因?yàn)榇薼ib工程還未運(yùn)行。

所以要解決這個(gè)問題 就要在app project運(yùn)行配置之前,先運(yùn)行l(wèi)ib project的配置,那么就可以用evaluationDependsOn來解決依賴,在app的build.gradle中添加如下依賴即可:

evaluationDependsOn(':lib') //運(yùn)行app配置之前,先運(yùn)行l(wèi)ib依賴

那么添加依賴之后,每次在運(yùn)行app配置階段之前,都會(huì)保證lib配置階段先被執(zhí)行。

六、APP工程build.gradle

build.gradle的內(nèi)容如下,與原工程一樣的地方就省略了:

//第一點(diǎn):讀取local.properties文件中內(nèi)容到內(nèi)存
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

//第二點(diǎn):獲取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'

//第三點(diǎn):導(dǎo)入flutter  gradle插件
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
  //省略...基本一樣
}

//第四點(diǎn)
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'
}

第一點(diǎn):讀取根工程下local.properties的內(nèi)容到內(nèi)存中(Properties), 該內(nèi)容就是上面所介紹的Flutter SDK相關(guān)信息。
第二點(diǎn):獲取flutter sdk路徑、versionCode、VersionName等信息。
第三點(diǎn):從Flutter SDK目錄下導(dǎo)入flutter gradle插件到當(dāng)前工程中運(yùn)行。
第四點(diǎn):配置flutter插件的source屬性,該屬性指定了Flutter工程的路徑。

該build.gradle最主要的功能從local.properties文件中獲取Flutter SDK路徑,并把該路徑下的Flutter Gradle插件導(dǎo)入到當(dāng)前工程中運(yùn)行,接下來我們要看看該插件到底做了哪些工作。

七、flutter.gradle

Flutter代碼打包到Android工程中秘密其實(shí)就是發(fā)生在flutter.gradle腳本中。該gradle腳本位于Flutter SDK/packages/flutter_tools/gradle/flutter.gradle中,接下來我們就揭開它的神秘面紗:

image

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

FlutterPlugin核心代碼

apply方法

FlutterPlugin實(shí)現(xiàn)了Plugin接口,它是一個(gè)標(biāo)準(zhǔn)的gradle plugin。因此它的主入口在apply方法中,首先我們看看第一部分:

  @Override
    void apply(Project project) {
        // Add custom build types

        println "==== apply:" + project.getName() //app

        //1、新增profile、dynamicProfile、dynamicRelease 三種構(gòu)建類型
        //在當(dāng)前project下的android.buildTypes進(jìn)行配置
        project.android.buildTypes {
            profile {
                initWith debug  //initWith:復(fù)制所有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 //默認(rèn)為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) // 判斷當(dāng)前的系統(tǒng)環(huán)境

        //根據(jù)操作環(huán)境的不同選擇執(zhí)行 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、為當(dāng)前工程新增 profile、dynamicProfile、dynamicRelease 這三種構(gòu)建類型,而且大部分屬性都是從debug中拷貝過來的。

image

2、從Android根工程的local.properties文件下獲取Flutter SDK的路徑和版本號(hào)信息。并且根據(jù)當(dāng)前系統(tǒng)的設(shè)置運(yùn)行Flutter默認(rèn)程序: Linux/Mac OS執(zhí)行的是Flutter SDK/bin/flutter,Windows執(zhí)行的是Flutter SDK/bin/flutter.bat

第二部分代碼如下:

        //當(dāng)前工程是否有l(wèi)ocalEngineOut屬性,默認(rèn)為false,因此下面代碼暫時(shí)不分析
        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')//默認(rèn)為false

            //根據(jù)target-platform的配置選項(xiàng),選擇arm平臺(tái),默認(rèn)為arm
            String targetArch = 'arm'
            if (project.hasProperty('target-platform') &&
                    project.property('target-platform') == 'android-arm64') {
                targetArch = 'arm64'
            }

            //2、根據(jù)不同的配置 選擇不同的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非文件或不存在,則運(yùn)行Flutter SDK/bin/flutter腳本。
            //默認(rèn)情況下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}")
                }
            }

            //定位當(dāng)前工程下輸出目錄中的intermediates/flutter/flutter-x86.jar文件
            //注意,這里輸出目錄在根工程的build.gralde已經(jīng)發(fā)生了改變,移動(dòng)至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

            //創(chuàng)建了一個(gè)任務(wù),該任務(wù)的作用是把引擎目錄下的x86 x64兩個(gè)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的名稱

                //下面是要拷貝的兩個(gè)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中的構(gòu)建類型,并把根據(jù)當(dāng)前構(gòu)建類型,添加對(duì)應(yīng)的jar包依賴
            project.android.buildTypes.each {
                println "====buildType:"+it.name //debug、dynamicProfile、dynamicRelease、profile、release
                addFlutterJarApiDependency(project, it, flutterX86JarTask)
            }
            //設(shè)置監(jiān)聽,當(dāng)新的構(gòu)建類型加入的時(shí)候執(zhí)行的依賴操作
            project.android.buildTypes.whenObjectAdded {
                addFlutterJarApiDependency(project, it, flutterX86JarTask)
            }
        }

第二部分的主要操作如下:
1、獲取Flutter引擎文件路徑Flutter SDK/bin/cache/artifacts/engine,該目錄下存在按構(gòu)建環(huán)境目錄分類的flutter.jar文件,該jar包含了Android代碼的所以依賴的Flutter類文件 和 asserts/icudtl.dat 資源文件 和 lib/libflutter.so 庫(kù)文件:

image

2、如果構(gòu)建環(huán)境的debug,那么還會(huì)把Flutter SDK//bin/cache/artifacts/engine/android-x86/libflutter.so 和Flutter SDK/bin/cache/artifacts/engine/android-x64/libflutter.so 這兩個(gè)so生成一個(gè) flutter-x86.jar,該jar包生成在Flutter根工程下的build/app/intermediates/flutter/目錄下:

image

這個(gè)jar包的目錄結(jié)構(gòu)大概是這樣的:

lib
  -x86
     -libflutter.so
  -x86_64
     -libflutter.so

生成該jar包后,就會(huì)該jar包加入到該工程的依賴環(huán)境中,見如下的依賴代碼:

/**
 *根據(jù)構(gòu)建類型 添加指定的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";
            }
            //根據(jù)buildType的不同添加對(duì)應(yīng)的jar包依賴,只依賴對(duì)應(yīng)的構(gòu)建類型jar包
            add(configuration, project.files {

                String buildMode = buildModeFor(buildType)
                //這里~ 如果是debug構(gòu)建類型,還添加了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類型,相對(duì)于其他構(gòu)建類型還增添了flutter-x86.jar的依賴,既多了兩個(gè)so庫(kù)。見編譯后的apk工程結(jié)構(gòu):

image

第三分部如下:

      //指定自己的DSL擴(kuò)展,F(xiàn)lutterExtension包含source和code兩個(gè)屬性
        project.extensions.create("flutter", FlutterExtension)

        //工程配置完畢后執(zhí)行addFlutterTask方法,該方法的作用是把Flutter的代碼和資源工程加入到Android工程中
        project.afterEvaluate this.&addFlutterTask

        //.flutter-plugins默認(rèn)情況下是沒有配置的,因此忽略
        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
        Properties plugins = readPropertiesIfExist(pluginsFile)
        plugins.each { name, _ ->
            ....
        }

第三步主要做的操作如下:
1、添加了Flutter插件的DSL擴(kuò)展flutter{},其擴(kuò)展的類是FlutterExtension,包含下面兩個(gè)屬性:

class FlutterExtension {
    String source
    String target
}

也就是說你可以用在工程的build.gradle中 使用 flutter { } 閉包來配置sourcetarget兩個(gè)屬性。

  • source:用來配置當(dāng)前Flutter工程的根路徑,注意不是Android工程,如果沒有配置拋出Must provide Flutter source directory異常。
  • target:用來指定Flutter代碼的啟動(dòng)入口,如果沒有配置默認(rèn)為lib/main.dart

2、在工程配置階段結(jié)束后,執(zhí)行addFlutterTask方法,該方法很重要,它的作用是把Flutter的代碼和資源進(jìn)行編譯和處理并加入到Android工程中

addFlutterTask方法

addFlutterTask方法仍然比較多,分為兩部分進(jìn)行講解。

第一部分如下:

     if (project.state.failure) {
            return
        }
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }

        //target屬性指定了,F(xiàn)lutter啟動(dòng)的程序入口,如果沒有配置默認(rèn)為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')
        }

第一部分的主要操作很簡(jiǎn)單,就是從project工程中讀取各自配置屬性例如:target、verbose、filesystem-roots、target-platform等等,以備后面運(yùn)行的時(shí)候使用。這里尤其對(duì)target和source屬性做了特殊處理,具體處理方式上面已經(jīng)說過了。

第二部分如下(關(guān)鍵):

 def addFlutterDeps = { variant ->   //variant對(duì)應(yīng)的構(gòu)建類型
            //獲取當(dāng)前的構(gòu)建類型
            String flutterBuildMode = buildModeFor(variant.buildType)

         
            if (flutterBuildMode == 'debug' && project.tasks.findByName('${flutterBuildPrefix}X86Jar')) {
                //...
            }

            //根據(jù)構(gòu)建類型,創(chuàng)建任務(wù)flutterBuild[構(gòu)建類型] 任務(wù),該任務(wù)是用于編譯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編譯的中間產(chǎn)物輸出路徑
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
                // ... /FlutterProject/flutter_new/flutter_new/build/app/intermediates/flutter/[debug,dynamicProfile,release..]  根據(jù)構(gòu)建類型的不同
               
                //...
            }


            // We know that the flutter app is a subproject in another Android app when these tasks exist.
            //查找 :flutter:package[構(gòu)建類型]Assets的Task【當(dāng)flutter作為依賴庫(kù)的時(shí)候,否則工程模式為null 】
            Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")

            //查找:flutter:cleanPackage[構(gòu)建類型]Assets的Task【當(dāng)flutter作為依賴庫(kù)的時(shí)候,否則工程模式為null 】
            Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
        
            //創(chuàng)建 copyFlutterAssets[構(gòu)建類型] 的task的拷貝操作.
            //同時(shí)依賴flutterTask、variant.mergeAssets (mergeDebugAssets)、 cleanMerge[構(gòu)建類型]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
                //運(yùn)行FlutterTask的getAssets方法執(zhí)行拷貝操作
                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對(duì)應(yīng)多種構(gòu)建類型,添加addFlutterDeps閉包
            project.android.applicationVariants.all addFlutterDeps
        } else {
            project.android.libraryVariants.all addFlutterDeps
        }

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

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

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

通過運(yùn)行debug構(gòu)建類型后的關(guān)系圖如下: (使用了 cz.malohlava.visteg插件)

image

運(yùn)行時(shí)候任務(wù)的構(gòu)建順序:
image

也就是說當(dāng)flutterBuild[構(gòu)建類型] 任務(wù)使得flutter編譯完成,并且merge[構(gòu)建類型]Assets執(zhí)行完畢、也就是正常Android的assets處理完成后、flutter相應(yīng)的產(chǎn)物就會(huì)被copyFlutterAssets[構(gòu)建類型]復(fù)制到 Flutter根工程/build/app/intermediates/merged_assets/[構(gòu)建類型]/merge[構(gòu)建類型]Assets/out目錄下。

flutter的編譯產(chǎn)物,具體是由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[構(gòu)建類型] 任務(wù)的作用就是把 Flutte根工程/build/app/intermediates/flutter/[構(gòu)建類型] 目錄下面flutter_assets/目錄中所有的內(nèi)容都拷貝到 Flutter根工程/build/app/intermediates/merged_assets/[構(gòu)建類型]/merge[構(gòu)建類型]Assets/out目錄下。如果是release或者profile版本的話,還包含拷貝了Dart的二進(jìn)制產(chǎn)物snapshot 或 app.so,可以看到,除了默認(rèn)情況下的snapshot,我們還可以指定Dart產(chǎn)物為還可以編譯成的so庫(kù)形式。

下面貼出了debug類型 和 release類型的拷貝對(duì)比:


image

image

那么Flutter的這些產(chǎn)物是怎么生成的呢?那么就要看 flutterBuild[構(gòu)建類型] 任務(wù)是如何構(gòu)建的,也就是我們接下來要講的FlutterTask類。

FlutterTask核心代碼

FlutterTask繼承自BaseFlutterTask,BaseFlutterTask是一個(gè)自定義的Task( DefaultTask),因此入口就在@TaskAction注解的build方法中,build方法直接調(diào)用BaseFlutterTask的buildBundle方法。

buildBundle方法代碼也比較多,我們?nèi)匀环譃閮蓚€(gè)部分。首先先看它的第一部分:

      intermediateDir.mkdirs()

        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }
        intermediateDir.mkdirs()

        //如果當(dāng)前的構(gòu)建類型 是profile和release 則先執(zhí)行下面的操作
        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啟動(dòng)的程序入口,默認(rèn)為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版本,因此會(huì)先編譯aot的二進(jìn)制Dart產(chǎn)物,也就是snapshot產(chǎn)物,實(shí)際是執(zhí)行以下命令(release):

flutter build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir 工程路徑/build/app/intermediates/flutter/release --release

執(zhí)行完成后會(huì)生成以下的文件在release目錄中:

image

接著,buildBundle方法的后半部分還會(huì)調(diào)用一次flutter命令,不過這次命令是所有編譯模式都會(huì)調(diào)用

        //執(zhí)行下面的任務(wù)
        project.exec {
            //Users/chenrongyi/Develop/flutter/flutter/bin/flutter
            //執(zhí)行bin/flutter程序
            executable flutterExecutable.absolutePath
            workingDir sourceDir

            // 當(dāng)前工程是有l(wèi)ocalEngineOut屬性,才會(huì)有l(wèi)ocalEngine,默認(rèn)為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啟動(dòng)的程序入口,默認(rèn)為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 分別執(zhí)行的參數(shù)不同
            if (buildMode == "release" || buildMode == "profile") {
                args "--precompiled"
            } else {
                args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
            }
            //設(shè)置資源輸出目錄
            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"
            }
        }

也就是執(zhí)行了下面的命令(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

執(zhí)行完成后,最終會(huì)生成一個(gè)flutter_assetss的資源目錄在目錄中:

image

上面通過兩個(gè)命令最終生成的release資源產(chǎn)物與我們平時(shí)用release命令生成的結(jié)果是一致的。如果用要生成dubg模式下的產(chǎn)物那只要執(zhí)行最后的命令代碼,也就是執(zhí)行下面的命令(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

執(zhí)行后生成的文件資源如下:


image

注意執(zhí)行上面的命令的時(shí)候要保證 intermediateDir文件目錄已經(jīng)創(chuàng)建,既...intermediates/flutter/debug/目錄已經(jīng)創(chuàng)建。命令執(zhí)行的開頭代碼也顯示的進(jìn)行mkdir了。否則跑上面的命令會(huì)出現(xiàn)找不到目錄的異常:


image

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

image

在flutter_asserts目錄中, 在debug模式下多了isolate_snapshot_data、vm_snapshot_data、kernel_blob.bin文件 。而其中isolate_snapshot_data、vm_snapshot_data這兩個(gè)文件在release的外面目錄中生成,除此之外還多了isolate_snapshot_instr、vm_snapshot_instr這兩個(gè)文件。因此總的來說realease模式比debug模式多了isolate_snapshot_instrvm_snapshot_instr 這兩個(gè)文件,這兩個(gè)文件屬于AOT的指令段文件。

生成上面的Flutter構(gòu)建產(chǎn)物后,就會(huì)執(zhí)行下面的拷貝操作,也就是我們上一小結(jié)提到過的FutterTask的getAssets方法 負(fù)責(zé)把這些文件拷貝到 工程文件路徑/build/app/intermediates/merged_assets/[構(gòu)建類型]/merge[構(gòu)建類型]Assets/out目錄下 參與assets資源的編譯中!


image

merged_assets中的這些文件就是最后都會(huì)打包到apk的assets目錄下。下面對(duì)兩個(gè)構(gòu)建版本做了下簡(jiǎn)單的對(duì)比:


image

八、總結(jié)

總結(jié)下 Flutter工程混編進(jìn)入Android工程整個(gè)流程大概如下:

  1. 讀取根工程下local.properties獲取Flutter SDK路徑和版本信息。
  2. 添加三種構(gòu)建模式:dynamicProfile、dynamicRelease、profile。
  3. 為工程添加flutter.jar包的依賴,該包包含flutter類文件、icudtl.dat資源文件和lib/libflutter.so 庫(kù)文件
  4. 如果是debug版本,那么還把引擎庫(kù)下的android-x64/libflutter.so 和 android-x86/libflutter.so 這兩so打包成一個(gè)flutter-x86.jar,同時(shí)該jar包也作為依賴。
  5. 執(zhí)行Flutter命令構(gòu)建Flutter產(chǎn)物,把生成的產(chǎn)物通過拷貝任務(wù)拷貝至merged_assets下。生成的產(chǎn)物包括flutter_asserts下等資源文件 和 snapshot 程序數(shù)據(jù)段。
  6. 最后Android的資源處理任務(wù)會(huì)把merged_assets下所有flutter產(chǎn)物都打包到apk中asserts目錄下,最終完成Flutter工程的混編工作。

九、關(guān)于Flutter 1.2.1的補(bǔ)充

以上主要是針對(duì)Flutter 1.0 gradle腳本進(jìn)行的分析。不過前些天Flutter推出了1.2.1版本,對(duì)flutter.gradle腳本文件也新增了某些的修改,不過總體來說影響并不大,主要是對(duì)一些BUG的修復(fù)。

1、新增了mainModuleName動(dòng)態(tài)屬性,用來指定主project的工程名:

image

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

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

image

創(chuàng)建一個(gè)名為 copySharedFlutterAssets[構(gòu)建類型] 的task,該task的作用是把flutter.jar包下assets/flutter_shared下所有文件都拷貝出來。因?yàn)樵贔lutter 1.0的版本中當(dāng)作為aar進(jìn)行打包的時(shí)候,jar包下的assets資源不會(huì)打包到aar包中,因此這里做了修復(fù) (不過我發(fā)現(xiàn)在升級(jí)到1.2.1后,flutter.jar中已經(jīng)不存在assets目錄,icudtl.dat文件已經(jīng)被移除, 現(xiàn)在已經(jīng)被嵌入到 libflutter.so文件中了: Remove the flutter_shared assets directory from the Gradle script

歡迎關(guān)注我的公眾號(hào)【不喝咖啡的程序員】,最新的文章會(huì)在上面發(fā)布:


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容