Flutter Android 工程結構及應用層編譯原始碼深入分析

工匠若水發表於2021-07-18

背景

本文部分配圖及原始碼最近基於 Flutter 2.2.3 版本進行了修正更新發布。目的是為了弄清 Flutter 在安卓端應用層的整個編譯來龍去脈,以便編譯過程中出任何問題都能做到心裡有數,另一個目的是為了能夠在應用層定製 Flutter 編譯。全文比較長,圖文並茂,由工程結構深入到原始碼解析。

Flutter 模組的幾種形式

早期版本的 Flutter 是不支援建立 Flutter Module,只有其他三種型別,想要這種型別都是靠自己造輪子和指令碼實現的,現在新版本 Flutter 對於原生與 Flutter 混合模式的支援方便許多,所以目前 Flutter 支援建立如下四種模組。

在這裡插入圖片描述

這四種模組對應的專案結構大致如下,其使用場景也各不相同,我們要依據自己需要建立適合自己的模組。

在這裡插入圖片描述

Flutter 模組依賴及產物概覽

當我們在 yaml 檔案中新增依賴後執行flutter pub get命令就會自動從依賴配置的地方下載或複製。對於純 Dart 依賴(Flutter Package)的下載位置在你 Flutter SDK 目錄下的.pub-cache\hosted\pub.dartlang.org\dio-4.0.0位置(mac 下在自己賬號目錄下的.pub-cache中),以 pub.flutter-io.cn/packages/di…為例,這個目錄下 lib 為專案主要依賴,如下:

在這裡插入圖片描述

對應在 Android Studio 中依賴展開的樣子如下:

在這裡插入圖片描述

對於依賴 Flutter Plugin 下載位置在你 Flutter SDK 目錄下的.pub-cache\hosted\pub.dartlang.org\webview_flutter-2.0.10位置(mac 下在自己賬號目錄下的.pub-cache中),以 pub.flutter-io.cn/packages/we…為例,這個目錄下 lib 及對應平臺目錄為專案主要依賴,如下:

在這裡插入圖片描述

對應在 Android Studio 中依賴展開的樣子如下:

在這裡插入圖片描述

對於一個 Flutter App 來說,其執行flutter build apk命令編譯後的產物巨集觀如下:

在這裡插入圖片描述

請務必對上圖產物結構有個簡單的認識,因為下文原始碼分析的重點都是圍繞怎麼編譯出這些東西來了。

Flutter App 安卓編譯原始碼流程

下面我們從純 Flutter 專案的 app 編譯安卓端 apk 流程說起。

settings.gradle 原始碼流程分析

既然是安卓的編譯流程,那就先從android/settings.gradle看起,如下:

// 當前 app module
include ':app'

/**
 * 1、讀取android/local.properties檔案內容
 * 2、獲取flutter.sdk的值,也就是你本地flutter SDK安裝目錄
 * 3、gradle 指令碼常規操作 apply flutter SDK路徑下/packages/flutter_tools/gradle/app_plugin_loader.gradle檔案 
 */
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()

assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }

def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
複製程式碼

通過上面步驟我們可以將目光轉向你 Flutter SDK 安裝目錄下的/packages/flutter_tools/gradle/app_plugin_loader.gradle檔案,內容如下:

import groovy.json.JsonSlurper
//得到自己新建的 flutter 專案的根路徑,因為已經被自己新建的 project apply,所以這裡是專案根路徑哦
def flutterProjectRoot = rootProject.projectDir.parentFile

//獲取自己專案根路徑下的.flutter-plugins-dependencies json配置檔案
// Note: if this logic is changed, also change the logic in module_plugin_loader.gradle.
def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies')
if (!pluginsFile.exists()) {
  return
}
/**
 * 1、通過groovy的JsonSlurper解析json檔案內容。
 * 2、簡單校驗json內容欄位的型別合法性。
 * 3、把安卓平臺依賴的Flutter plugins全部自動include進來
 */
def object = new JsonSlurper().parseText(pluginsFile.text)
assert object instanceof Map
assert object.plugins instanceof Map
assert object.plugins.android instanceof List
// Includes the Flutter plugins that support the Android platform.
object.plugins.android.each { androidPlugin ->
  assert androidPlugin.name instanceof String
  assert androidPlugin.path instanceof String
  def pluginDirectory = new File(androidPlugin.path, 'android')
  assert pluginDirectory.exists()
  include ":${androidPlugin.name}"
  project(":${androidPlugin.name}").projectDir = pluginDirectory
}
複製程式碼

上面的 gradle 指令碼很簡單,大家看註釋即可。為了直觀說明問題,這裡新建了一個典型 demo 專案,然後其pubspec.yaml檔案依賴配置如下:

dependencies:
  flutter:
    sdk: flutter
  dio: ^4.0.0 #來自pub.dev倉庫的Flutter Package包
  webview_flutter: ^2.0.10 #來自pub.dev倉庫的Flutter Plugin包
  f_package: #來自自己本地新建的Flutter Package包
    path: ./../f_package
  f_plugin: #來自自己本地新建的Flutter Plugin包
    path: ./../f_plugin
複製程式碼

接著我們看看這個專案根路徑的.flutter-plugins-dependencies檔案,如下:

{
    "info":"This is a generated file; do not edit or check into version control.",
    "plugins":{
        "ios":[
            {"name":"f_plugin","path":"E:\\\\f_plugin\\\\","dependencies":[]},
            {"name":"webview_flutter","path":"D:\\\\software\\\\flutter\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\webview_flutter-2.0.10\\\\","dependencies":[]}
        ],
        "android":[
            {"name":"f_plugin","path":"E:\\\\f_plugin\\\\","dependencies":[]},
            {"name":"webview_flutter","path":"D:\\\\software\\\\flutter\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\webview_flutter-2.0.10\\\\","dependencies":[]}
        ],
        "macos":[],
        "linux":[],
        "windows":[],
        "web":[
            {"name":"f_plugin","path":"E:\\\\f_plugin\\\\","dependencies":[]}
        ]
    },
    "dependencyGraph":[
        {"name":"f_plugin","dependencies":[]},
        {"name":"webview_flutter","dependencies":[]}
    ],
    "date_created":"202x-0x-15 21:41:39.225336",
    "version":"2.2.3"
}
複製程式碼

這時候我們回過頭去看自己專案android/settings.gradle,在 Gradle 生命週期的初始化階段(即解析settings.gradle),我們專案的settings.gradle經過apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"處理後自動變成如下虛擬碼:

include ':app'
// 自動通過匹配依賴然後app_plugin_loader.gradle解析生成
//include ":${androidPlugin.name}"
//project(":${androidPlugin.name}").projectDir = pluginDirectory
include ":f_plugin"
project(":f_plugin").projectDir = new File("E:\\\\f_plugin\\\\", 'android')

include ":webview_flutter"
project(":webview_flutter").projectDir = new File("D:\\\\software\\\\flutter\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\webview_flutter-2.0.10\\\\", 'android')
複製程式碼

咋說!是不是一下就恍然大悟了,其實就是“約定大於配置”的軟體工程原則,你只管按照規則擺放,本質最後都是我們平時標準 Android 專案那樣。

build.gradle原始碼流程分析

先看專案 android 下根目錄的build.gradle,如下:

//......省略無關緊要的常見配置
// 看到了吧,他將所有 android 依賴的構建產物挪到了根目錄下的 build 中,所以產物都在那兒
rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
    project.evaluationDependsOn(':app') //執行其他配置之前,先執行app依賴
}
複製程式碼

接著我們看看 app 模組下的build.gradle,如下:

/**
 * 1、讀取local.properties配置資訊。
 * 2、獲取flutter.sdk路徑。
 * 3、獲取flutter.versionCode值,此值在編譯時自動從pubspec.yaml中讀取賦值,所以修改版本號請修改yaml。
 * 4、獲取flutter.versionName值,此值在編譯時自動從pubspec.yaml中讀取賦值,所以修改版本號請修改yaml。
 */
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

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'
apply plugin: 'kotlin-android'
//重點1:apply 了 flutter SDK 下面的packages/flutter_tools/gradle/flutter.gradle指令碼檔案
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 30

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        applicationId "cn.yan.f1"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode flutterVersionCode.toInteger()	//賦值為yaml中讀取的值
        versionName flutterVersionName	//賦值為yaml中讀取的值
    }
	//......省略常規操作,不解釋
}
//重點2:一個擴充配置,指定source路徑為當前的兩級父級,也就是專案根目錄
flutter {
    source '../..'
}

//......省略常規操作,不解釋
複製程式碼

下面我們看看上面提到的重點1,也就是 Flutter SDK 中的packages/flutter_tools/gradle/flutter.gradle,我們按照指令碼執行時巨集觀到細節的方式來分析,如下:

//......省略一堆import標頭檔案
/**
 * 常規指令碼配置:指令碼依賴倉庫及依賴的 AGP 版本
 * 如果你自己沒有全域性配國內maven映象,修改這裡repositories也可以。
 * 如果你專案對於AGP這個版本不相容,自己修改這裡然後相容也可以。
 */
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
    }
}
//java8編譯配置
android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}
//又 apply 了一個外掛,只是這個外掛原始碼直接定義在下方
apply plugin: FlutterPlugin

//FlutterPlugin外掛實現原始碼,參考標準外掛寫法一樣,基本語法不解釋,這裡重點看邏輯。
class FlutterPlugin implements Plugin<Project> {
    //......
	//重點入口!!!!!!
    @Override
    void apply(Project project) {
        this.project = project

        //1、配置maven倉庫地址,環境變數有配置FLUTTER_STORAGE_BASE_URL就優先用,沒就預設
        String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST
        String repository = useLocalEngine()
            ? project.property('local-engine-repo')
            : "$hostedRepository/download.flutter.io"
        project.rootProject.allprojects {
            repositories {
                maven {
                    url repository
                }
            }
        }
		//2、建立app模組中配置的flutter{ source: '../../'}閉包extensions
        project.extensions.create("flutter", FlutterExtension)
        //3、新增flutter構建相關的各種task
        this.addFlutterTasks(project)

        //4、判斷編譯命令flutter build apk --split-per-abi是否新增--split-per-abi引數,有的話就拆分成多個abi包。
        if (shouldSplitPerAbi()) {
            project.android {
                splits {
                    abi {
                        // Enables building multiple APKs per ABI.
                        enable true
                        // Resets the list of ABIs that Gradle should create APKs for to none.
                        reset()
                        // Specifies that we do not want to also generate a universal APK that includes all ABIs.
                        universalApk false
                    }
                }
            }
        }
		//5、判斷編譯命令是否新增deferred-component-names引數,有就配置android dynamicFeatures bundle特性。
        if (project.hasProperty('deferred-component-names')) {
            String[] componentNames = project.property('deferred-component-names').split(',').collect {":${it}"}
            project.android {
                dynamicFeatures = componentNames
            }
        }
        //6、判斷編譯命令是否新增--target-platform=xxxABI引數,沒有就用預設,有就看這個ABI是否flutter支援的,支援就配置,否則丟擲異常。
        getTargetPlatforms().each { targetArch ->
            String abiValue = PLATFORM_ARCH_MAP[targetArch]
            project.android {
                if (shouldSplitPerAbi()) {
                    splits {
                        abi {
                            include abiValue
                        }
                    }
                }
            }
        }
		//7、通過屬性配置獲取flutter.sdk,或者通過環境變數FLUTTER_ROOT獲取,都沒有就丟擲環境異常。
        String flutterRootPath = resolveProperty("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.")
        }
        flutterRoot = project.file(flutterRootPath)
        if (!flutterRoot.isDirectory()) {
            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
        }
		//8、獲取Flutter Engine的版本號,如果通過local-engine-repo引數使用本地自己編譯的Engine則版本為+,否則讀取SDK目錄下bin\internal\engine.version檔案值,一串類似MD5的值。
        engineVersion = useLocalEngine()
            ? "+" // Match any version since there's only one.
            : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()
		//9、依據平臺獲取對應flutter命令指令碼,都位於SDK目錄下bin\中,名字為flutter
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
		//10、獲取flutter混淆配置清單,位於SDK路徑下packages\flutter_tools\gradle\flutter_proguard_rules.pro。
		//裡面配置只有 -dontwarn io.flutter.plugin.** 和 -dontwarn android.**
        String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
                "gradle", "flutter_proguard_rules.pro")
        project.android.buildTypes {
            //11、新增profile構建型別,在當前project下的android.buildTypes中進行配置
            profile {
                initWith debug //initWith操作複製所有debug裡面的屬性
                if (it.hasProperty("matchingFallbacks")) {
                    matchingFallbacks = ["debug", "release"]
                }
            }
            //......
        }
        //......
        //12、給所有buildTypes新增依賴,addFlutterDependencies
        project.android.buildTypes.all this.&addFlutterDependencies
    }
	//......
}
//flutter{}閉包Extension定義
class FlutterExtension {
    String source
    String target
}
//......
複製程式碼

可以看到,上面指令碼的本質是一個標準外掛,其內部主要就是基於我們傳遞的引數進行一些配置。上面的步驟 4 的表現看產物,這裡不再演示。步驟 11 其實就是新增了一種編譯型別,對應專案中就是效能模式,如下:

在這裡插入圖片描述

步驟 12 對應追加依賴的指令碼如下:

/**
 * 給每個buildType新增Flutter專案的dependencies依賴,主要包括embedding和libflutter.so
 */
void addFlutterDependencies(buildType) {
	//獲取build型別,值為debug、profile、release
    String flutterBuildMode = buildModeFor(buildType)
    //對使用本地Engine容錯,官方Engine忽略這個條件即可,繼續往下
    if (!supportsBuildMode(flutterBuildMode)) {
        return
    }
    //如果外掛不是applicationVariants型別,即android library,或者專案根目錄下`.flutter-plugins`檔案中安卓外掛個數為空。
    if (!isFlutterAppProject() || getPluginList().size() == 0) {
    	//簡單理解就是給Flutter Plugin的android外掛新增編譯依賴
    	//譬如io.flutter:flutter_embedding_debug:1.0.0,來自maven倉庫
        addApiDependencies(project, buildType.name,
                "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
    }
    //給project新增對應編譯依賴
    //譬如io.flutter:arm64_v8a_debug:1.0.0,來自maven倉庫
    List<String> platforms = getTargetPlatforms().collect()
    // Debug mode includes x86 and x64, which are commonly used in emulators.
    if (flutterBuildMode == "debug" && !useLocalEngine()) {
        platforms.add("android-x86")
        platforms.add("android-x64")
    }
    platforms.each { platform ->
        String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
        // Add the `libflutter.so` dependency.
        addApiDependencies(project, buildType.name,
                "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
    }
}

private static void addApiDependencies(Project project, String variantName, Object dependency, Closure config = null) {
    String configuration;
    // `compile` dependencies are now `api` dependencies.
    if (project.getConfigurations().findByName("api")) {
        configuration = "${variantName}Api";
    } else {
        configuration = "${variantName}Compile";
    }
    project.dependencies.add(configuration, dependency, config)
}
複製程式碼

上面這段指令碼的本質就是給 Flutter 專案自動新增編譯依賴,這個依賴本質也是 maven 倉庫的,很像我們自己編寫 gradle 中新增的 okhttp 等依賴,沒啥區別。譬如我們建立的 demo 專案匯入 Android Studio 後自動 sync 的 dependencies 依賴如下:

在這裡插入圖片描述

接下來我們把重心放回步驟 3(addFlutterTasks),這才是我們整個 Flutter app 編譯的重點,也是最複雜的部分,如下:

private void addFlutterTasks(Project project) {
	//gradle專案配置評估失敗則返回,常規操作,忽略
    if (project.state.failure) {
        return
    }
    //1、一堆屬性獲取與賦值操作
    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 = true
    if (project.hasProperty('track-widget-creation')) {
        trackWidgetCreationValue = project.property('track-widget-creation').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')
    }
    String splitDebugInfoValue = null
    if (project.hasProperty('split-debug-info')) {
        splitDebugInfoValue = project.property('split-debug-info')
    }
    Boolean dartObfuscationValue = false
    if (project.hasProperty('dart-obfuscation')) {
        dartObfuscationValue = project.property('dart-obfuscation').toBoolean();
    }
    Boolean treeShakeIconsOptionsValue = false
    if (project.hasProperty('tree-shake-icons')) {
        treeShakeIconsOptionsValue = project.property('tree-shake-icons').toBoolean()
    }
    String dartDefinesValue = null
    if (project.hasProperty('dart-defines')) {
        dartDefinesValue = project.property('dart-defines')
    }
    String bundleSkSLPathValue;
    if (project.hasProperty('bundle-sksl-path')) {
        bundleSkSLPathValue = project.property('bundle-sksl-path')
    }
    String performanceMeasurementFileValue;
    if (project.hasProperty('performance-measurement-file')) {
        performanceMeasurementFileValue = project.property('performance-measurement-file')
    }
    String codeSizeDirectoryValue;
    if (project.hasProperty('code-size-directory')) {
        codeSizeDirectoryValue = project.property('code-size-directory')
    }
    Boolean deferredComponentsValue = false
    if (project.hasProperty('deferred-components')) {
        deferredComponentsValue = project.property('deferred-components').toBoolean()
    }
    Boolean validateDeferredComponentsValue = true
    if (project.hasProperty('validate-deferred-components')) {
        validateDeferredComponentsValue = project.property('validate-deferred-components').toBoolean()
    }
    def targetPlatforms = getTargetPlatforms()
    ......
}
複製程式碼

可以看到,addFlutterTasks 方法的第一部分比較簡單,基本都是從 Project 中讀取各自配置屬性供後續步驟使用。所以我們接著繼續看 addFlutterTasks 這個方法步驟 1 之後的部分:

private void addFlutterTasks(Project project) {
    //一堆屬性獲取與賦值操作
    //......
    //1、定義 addFlutterDeps 箭頭函式,引數variant為標準構建對應的構建型別
    def addFlutterDeps = { variant ->
        if (shouldSplitPerAbi()) {
        	//2、常規操作:如果是構建多個變體apk模式就處理vc問題
            variant.outputs.each { output ->
                //由於GP商店不允許同一個應用的多個APK全都具有相同的版本資訊,因此在上傳到Play商店之前,您需要確保每個APK都有自己唯一的versionCode,這裡就是做這個事情的。
                //具體可以看官方文件 https://developer.android.com/studio/build/configure-apk-splits
                def abiVersionCode = ABI_VERSION.get(output.getFilter(OutputFile.ABI))
                if (abiVersionCode != null) {
                    output.versionCodeOverride =
                        abiVersionCode * 1000 + variant.versionCode
                }
            }
        }
        //3、獲取編譯型別,variantBuildMode值為debug、profile、release之一
        String variantBuildMode = buildModeFor(variant.buildType)
        //4、依據引數生成一個task名字,譬如這裡的compileFlutterBuildDebug、compileFlutterBuildProfile、compileFlutterBuildRelease
        String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
        //5、給當前project建立compileFlutterBuildDebug、compileFlutterBuildProfile、compileFlutterBuildRelease Task
        //實現為FlutterTask,主要用來編譯Flutter程式碼,這個task稍後單獨分析
        FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
        	//各種task屬性賦值操作,基本都來自上面的屬性獲取或者匹配分析
            flutterRoot this.flutterRoot
            flutterExecutable this.flutterExecutable
            buildMode variantBuildMode
            localEngine this.localEngine
            localEngineSrcPath this.localEngineSrcPath
            //預設dart入口lib/main.dart、可以通過target屬性自定義指向
            targetPath getFlutterTarget()
            verbose isVerbose()
            fastStart isFastStart()
            fileSystemRoots fileSystemRootsValue
            fileSystemScheme fileSystemSchemeValue
            trackWidgetCreation trackWidgetCreationValue
            targetPlatformValues = targetPlatforms
            sourceDir getFlutterSourceDirectory()
            //學到一個小技能,原來中間API是AndroidProject.FD_INTERMEDIATES,這也是flutter中間產物目錄
            intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/")
            extraFrontEndOptions extraFrontEndOptionsValue
            extraGenSnapshotOptions extraGenSnapshotOptionsValue
            splitDebugInfo splitDebugInfoValue
            treeShakeIcons treeShakeIconsOptionsValue
            dartObfuscation dartObfuscationValue
            dartDefines dartDefinesValue
            bundleSkSLPath bundleSkSLPathValue
            performanceMeasurementFile performanceMeasurementFileValue
            codeSizeDirectory codeSizeDirectoryValue
            deferredComponents deferredComponentsValue
            validateDeferredComponents validateDeferredComponentsValue
            //最後做一波許可權相關處理
            doLast {
                project.exec {
                    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                        commandLine('cmd', '/c', "attrib -r ${assetsDirectory}/* /s")
                    } else {
                        commandLine('chmod', '-R', 'u+w', assetsDirectory)
                    }
                }
            }
        }
        //專案構建中間產物的檔案,也就是根目錄下build/intermediates/flutter/debug/libs.jar檔案
        File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
        //6、建立packLibsFlutterBuildProfile、packLibsFlutterBuildDebug、packLibsFlutterBuildRelease任務,主要是產物的複製挪位置操作,Jar 型別的 task
        //作用就是把build/intermediates/flutter/debug/下依據abi生成的app.so通過jar命令打包成build/intermediates/flutter/debug/libs.jar
        Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
        	//目標路徑為build/intermediates/flutter/debug目錄
            destinationDir libJar.parentFile
            //檔名為libs.jar
            archiveName libJar.name
            //依賴前面步驟5定義的compileFlutterBuildDebug,也就是說,這個task基本作用是產物處理
            dependsOn compileTask
            //targetPlatforms取值為android-arm、android-arm64、android-x86、android-x64
            targetPlatforms.each { targetPlatform ->
            	//abi取值為armeabi-v7a、arm64-v8a、x86、x86_64
                String abi = PLATFORM_ARCH_MAP[targetPlatform]
                //資料來源來自步驟5的compileFlutterBuildDebug任務中間產物目錄
                //即把build/intermediates/flutter/debug/下依據abi生成的app.so通過jar命令打包成一個build/intermediates/flutter/debug/libs.jar檔案
                from("${compileTask.intermediateDir}/${abi}") {
                    include "*.so"
                    // Move `app.so` to `lib/<abi>/libapp.so`
                    rename { String filename ->
                        return "lib/${abi}/lib${filename}"
                    }
                }
            }
        }
        //前面有介紹過addApiDependencies作用,把 packFlutterAppAotTask 產物加到依賴項裡面參與編譯
        //類似implementation files('libs.jar'),然後裡面的so會在專案執行標準mergeDebugNativeLibs task時打包進標準lib目錄
        addApiDependencies(project, variant.name, project.files {
            packFlutterAppAotTask
        })
        // 當構建有is-plugin屬性時則編譯aar
        boolean isBuildingAar = project.hasProperty('is-plugin')
        //7、當是Flutter Module方式,即Flutter以aar作為已存在native安卓專案依賴時才有這些:flutter:模組依賴,否則沒有這些task
        //可以參見新建的FlutterModule中.android/include_flutter.groovy中gradle.project(":flutter").projectDir實現
        Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
        Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
        //判斷是否為FlutterModule依賴
        boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
        //8、新建copyFlutterAssetsDebug task,目的就是copy產物,也就是assets歸檔
        //常規merge中間產物類似,不再過多解釋,就是把步驟5 task產物的assets目錄在mergeAssets時複製到主包中間產物目錄
        Task copyFlutterAssetsTask = project.tasks.create(
            name: "copyFlutterAssets${variant.name.capitalize()}",
            type: Copy,
        ) {
            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
        }
        if (!isUsedAsSubproject) {
            def variantOutput = variant.outputs.first()
            def processResources = variantOutput.hasProperty("processResourcesProvider") ?
                variantOutput.processResourcesProvider.get() : variantOutput.processResources
            processResources.dependsOn(copyFlutterAssetsTask)
        }
        return copyFlutterAssetsTask
    } // end def addFlutterDeps
	......
}
複製程式碼

上面這段比較直觀,步驟5細節我們後面會分析這個 FlutterTask;對於步驟 6 其實也蠻直觀,我們執行 flutter build apk 後看產物目錄如下:

在這裡插入圖片描述

這個 jar 也是重點,它裡面其實不是 class,而是上圖中的 abi 對應 app.so,也就是 dart app 編譯的 so。所以 libs.jar 解壓如下:

在這裡插入圖片描述

這貨會被類似 implementation files('libs.jar') 新增進我們 project 的編譯依賴項中,然後裡面的 so 會在專案執行標準 mergeDebugNativeLibs task 時打包進標準 lib 目錄,所以最終 apk 中 app.so 位於 lib 目錄下(好奇反思:官方這裡為什麼不直接弄成 aar,而是把 so 打進 jar,感覺回到了 eclipse 時代,沒整明白為什麼)。

對於步驟 8 來說,assets 合併複製操作在 app 主包的中間產物中效果如下:

在這裡插入圖片描述

因此,步驟 6、步驟 8 的產物最終編譯後就是 apk 中對應的東西,對應 apk 解壓如下:

在這裡插入圖片描述

上面步驟5中的 FlutterTask 我們先放一放,讓我們先繼續看 addFlutterTasks 這個方法剩下的部分:

private void addFlutterTasks(Project project) {
    //......上面已分析,下面接續分析
    //1、如果是applicationVariants就走進去,也就是說project是app module
    if (isFlutterAppProject()) {
        project.android.applicationVariants.all { variant ->
        	//也就是assemble task咯
            Task assembleTask = getAssembleTask(variant)
            //正常容錯,不用關心
            if (!shouldConfigureFlutterTask(assembleTask)) {
              return
            }
            //把前面定義的addFlutterDeps函式呼叫返回的copyFlutterAssetsTask任務拿到作為依賴項
            //這貨的作用和產物前面已經圖示貼了產物
            Task copyFlutterAssetsTask = addFlutterDeps(variant)
            def variantOutput = variant.outputs.first()
            def processResources = variantOutput.hasProperty("processResourcesProvider") ?
                variantOutput.processResourcesProvider.get() : variantOutput.processResources
            processResources.dependsOn(copyFlutterAssetsTask)

            //2、執行flutter run或者flutter build apk的產物apk歸檔處理
            //不多解釋,下面會圖解說明
            variant.outputs.all { output ->
                assembleTask.doLast {
                    // `packageApplication` became `packageApplicationProvider` in AGP 3.3.0.
                    def outputDirectory = variant.hasProperty("packageApplicationProvider")
                        ? variant.packageApplicationProvider.get().outputDirectory
                        : variant.packageApplication.outputDirectory
                    //  `outputDirectory` is a `DirectoryProperty` in AGP 4.1.
                    String outputDirectoryStr = outputDirectory.metaClass.respondsTo(outputDirectory, "get")
                        ? outputDirectory.get()
                        : outputDirectory
                    String filename = "app"
                    String abi = output.getFilter(OutputFile.ABI)
                    if (abi != null && !abi.isEmpty()) {
                        filename += "-${abi}"
                    }
                    if (variant.flavorName != null && !variant.flavorName.isEmpty()) {
                        filename += "-${variant.flavorName.toLowerCase()}"
                    }
                    filename += "-${buildModeFor(variant.buildType)}"
                    project.copy {
                        from new File("$outputDirectoryStr/${output.outputFileName}")
                        into new File("${project.buildDir}/outputs/flutter-apk");
                        rename {
                            return "${filename}.apk"
                        }
                    }
                }
            }
        }
        //3、小重點
        configurePlugins()
        return
    }
    //3、是不是模組原始碼依賴方式整合到現有專案,參見 https://flutter.cn/docs/development/add-to-app/android/project-setup
    //是的話對模組也做類似一堆處理即可,不再重複分析了,也是 assets 合併
    String hostAppProjectName = project.rootProject.hasProperty('flutter.hostAppProjectName') ? project.rootProject.property('flutter.hostAppProjectName') : "app"
    Project appProject = project.rootProject.findProject(":${hostAppProjectName}")
    assert appProject != null : "Project :${hostAppProjectName} doesn't exist. To custom the host app project name, set `org.gradle.project.flutter.hostAppProjectName=<project-name>` in gradle.properties."
    // Wait for the host app project configuration.
    appProject.afterEvaluate {
        assert appProject.android != null
        project.android.libraryVariants.all { libraryVariant ->
            Task copyFlutterAssetsTask
            appProject.android.applicationVariants.all { appProjectVariant ->
                Task appAssembleTask = getAssembleTask(appProjectVariant)
                if (!shouldConfigureFlutterTask(appAssembleTask)) {
                    return
                }
                // Find a compatible application variant in the host app.
                //
                // For example, consider a host app that defines the following variants:
                // | ----------------- | ----------------------------- |
                // |   Build Variant   |   Flutter Equivalent Variant  |
                // | ----------------- | ----------------------------- |
                // |   freeRelease     |   release                      |
                // |   freeDebug       |   debug                       |
                // |   freeDevelop     |   debug                       |
                // |   profile         |   profile                     |
                // | ----------------- | ----------------------------- |
                //
                // This mapping is based on the following rules:
                // 1. If the host app build variant name is `profile` then 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`.
                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)
            }
        }
    }
    configurePlugins()
}
複製程式碼

上面這段程式碼分析中的步驟2本質就是對標準安卓構建產物進行一次重新按照格式歸檔,如果是 split api 模式就能很直觀看出來效果,下面圖示是直接執行 flutter build apk 的步驟 2 效果:

在這裡插入圖片描述

對於上面程式碼片段中的步驟 3,我們可以詳細來分析下:

/**
 * flutter的依賴都新增在pubspec.yaml中
 * 接著都會執行flutter pub get,然後工具會生成跟目錄下.flutter-plugins等檔案
 * 這裡做的事情就是幫忙給module自動新增上這些外掛dependencies依賴模組
 */
private void configurePlugins() {
    if (!buildPluginAsAar()) {
    	//專案根目錄下的.flutter-plugins檔案
        getPluginList().each this.&configurePluginProject
        //專案根目錄下的.flutter-plugins-dependencies檔案
        getPluginDependencies().each this.&configurePluginDependencies
        return
    }
    project.repositories {
        maven {
            url "${getPluginBuildDir()}/outputs/repo"
        }
    }
    getPluginList().each { pluginName, pluginPath ->
        configurePluginAar(pluginName, pluginPath, project)
    }
}
複製程式碼

到此整個 addFlutterTasks 核心方法我們就分析完畢。接下來讓我們把目光轉向 FlutterTask 的實現,Task 機制不懂就自己去補習 gradle 基礎吧,重點入口就是 @TaskAction,如下(比較長,但是比較直觀簡單):

abstract class BaseFlutterTask extends DefaultTask {
    //......一堆task屬性宣告,忽略

    @OutputFiles
    FileCollection getDependenciesFiles() {
        FileCollection depfiles = project.files()

        // Includes all sources used in the flutter compilation.
        depfiles += project.files("${intermediateDir}/flutter_build.d")
        return depfiles
    }
	//重點!!!!!!!!!!!!!!!!!!!!!
	//整個flutter android編譯的核心實現在此!!!!
    void buildBundle() {
        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }
		//1、預設以app為例建立build/app/intermediates/flutter目錄
        intermediateDir.mkdirs()

        //2、計算flutter assemble的規則名稱列表
        String[] ruleNames;
        if (buildMode == "debug") {
            ruleNames = ["debug_android_application"]
        } else if (deferredComponents) {
            ruleNames = targetPlatformValues.collect { "android_aot_deferred_components_bundle_${buildMode}_$it" }
        } else {
            ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
        }
        //3、重點執行命令
        project.exec {
            logging.captureStandardError LogLevel.ERROR
            //4、windows的話就是flutter SDK路徑下 bin/flutter.bat檔案,unix就是bin/flutter
            executable flutterExecutable.absolutePath
            //5、我們app的build.gradle中配置的flutter { source '../../' }閉包,路徑,也就是專案根目錄下
            workingDir sourceDir
            //6、使用本地自己編譯的flutter engine才需要的引數
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
            //7、類似標準gradle構建引數列印控制
            if (verbose) {
                args "--verbose"
            } else {
                args "--quiet"
            }
            //8、追加一堆編譯引數
            args "assemble"
            args "--no-version-check"
            args "--depfile", "${intermediateDir}/flutter_build.d"
            //flutter 編譯產物輸出路徑
            args "--output", "${intermediateDir}"
            if (performanceMeasurementFile != null) {
                args "--performance-measurement-file=${performanceMeasurementFile}"
            }
            //Flutter dart程式入口,預設為lib/main.dart
            if (!fastStart || buildMode != "debug") {
                args "-dTargetFile=${targetPath}"
            } else {
                args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}"
            }
            args "-dTargetPlatform=android"
            args "-dBuildMode=${buildMode}"
            if (trackWidgetCreation != null) {
                args "-dTrackWidgetCreation=${trackWidgetCreation}"
            }
            if (splitDebugInfo != null) {
                args "-dSplitDebugInfo=${splitDebugInfo}"
            }
            if (treeShakeIcons == true) {
                args "-dTreeShakeIcons=true"
            }
            if (dartObfuscation == true) {
                args "-dDartObfuscation=true"
            }
            if (dartDefines != null) {
                args "--DartDefines=${dartDefines}"
            }
            if (bundleSkSLPath != null) {
                args "-iBundleSkSLPath=${bundleSkSLPath}"
            }
            if (codeSizeDirectory != null) {
                args "-dCodeSizeDirectory=${codeSizeDirectory}"
            }
            if (extraGenSnapshotOptions != null) {
                args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
            }
            if (extraFrontEndOptions != null) {
                args "--ExtraFrontEndOptions=${extraFrontEndOptions}"
            }
            args ruleNames
        }
    }
}

class FlutterTask extends BaseFlutterTask {
	//預設以app為例則為build/app/intermediates/flutter目錄。
    @OutputDirectory
    File getOutputDirectory() {
        return intermediateDir
    }
	//預設以app為例則為build/app/intermediates/flutter/flutter_assets目錄,前面我們已經截圖展示過這個目錄產物。
    @Internal
    String getAssetsDirectory() {
        return "${outputDirectory}/flutter_assets"
    }
	//assets複製操作定義,intermediateDir就是getOutputDirectory路徑
    @Internal
    CopySpec getAssets() {
        return project.copySpec {
            from "${intermediateDir}"
            include "flutter_assets/**" // the working dir and its files
        }
    }
	//dart編譯的產物複製操作定義(注意:release和profile模式才是so產物),intermediateDir就是getOutputDirectory路徑
    @Internal
    CopySpec getSnapshots() {
        return project.copySpec {
            from "${intermediateDir}"

            if (buildMode == 'release' || buildMode == 'profile') {
                targetPlatformValues.each {
                    include "${PLATFORM_ARCH_MAP[targetArch]}/app.so"
                }
            }
        }
    }
	//依賴格式解析生成檔案路徑集合
    FileCollection readDependencies(File dependenciesFile, Boolean inputs) {
      if (dependenciesFile.exists()) {
        // Dependencies file has Makefile syntax:
        //   <target> <files>: <source> <files> <separated> <by> <non-escaped space>
        String depText = dependenciesFile.text
        // So we split list of files by non-escaped(by backslash) space,
        def matcher = depText.split(': ')[inputs ? 1 : 0] =~ /(\\ |[^\s])+/
        // then we replace all escaped spaces with regular spaces
        def depList = matcher.collect{it[0].replaceAll("\\\\ ", " ")}
        return project.files(depList)
      }
      return project.files();
    }
	//輸入源為所有依賴模組的pubspec.yaml檔案集合
    @InputFiles
    FileCollection getSourceFiles() {
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
          sources += readDependencies(depfile, true)
        }
        return sources + project.files('pubspec.yaml')
    }

    @OutputFiles
    FileCollection getOutputFiles() {
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
          sources += readDependencies(depfile, false)
        }
        return sources
    }
	//重點實現!!!!!!!
    @TaskAction
    void build() {
        buildBundle()
    }
}
複製程式碼

可以很直觀的看到,整個構建編譯的核心都是通過執行 Flutter SDK 中 bin 目錄下的 flutter 指令碼完成的,大段程式碼只是為了為執行這個指令碼準備引數配置資訊。也就是說 flutter 編譯本質命令大致如下:

flutter assemble --no-version-check \
--depfile build/app/intermediates/flutter/release/flutter_build.d \
--output build/app/intermediates/flutter/release/ \
-dTargetFile=lib/main.dart \
-dTargetPlatform=android \
-dBuildMode=release \
-dDartObfuscation=true \
android_aot_bundle_release_android-arm \
android_aot_bundle_release_android-arm64 \
android_aot_bundle_release_android-x86 \
android_aot_bundle_release_android-x64
複製程式碼

這就走到了 SDK 裡面的純 flutter 命令指令碼了。

Flutter SDK 下bin/flutter編譯命令分析

承接上面分析,上一小節最後的命令本質就是本小節的指令碼,我們把目光轉向 Flutter SDK 中 bin 目錄下的 flutter 指令碼,如下:

#!/usr/bin/env bash
#1、該命令之後出現的程式碼,一旦出現了返回值非零,整個指令碼就會立即退出,那麼就可以避免一些指令碼的危險操作。
set -e
#2、清空CDPATH變數值
unset CDPATH

# 在Mac上,readlink -f不起作用,因此follow_links一次遍歷一個連結的路徑,然後遍歷cd進入連結目的地並找出它。
# 返回的檔案系統路徑必須是Dart的URI解析器可用的格式,因為Dart命令列工具將其引數視為檔案URI,而不是檔名。
# 例如,多個連續的斜槓應該減少為一個斜槓,因為雙斜槓表示URI的authority。
function follow_links() (
  cd -P "$(dirname -- "$1")"
  file="$PWD/$(basename -- "$1")"
  while [[ -h "$file" ]]; do
    cd -P "$(dirname -- "$file")"
    file="$(readlink -- "$file")"
    cd -P "$(dirname -- "$file")"
    file="$PWD/$(basename -- "$file")"
  done
  echo "$file"
)
# 這個變數的值就是Flutter SDK根目錄下的bin/flutter
PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
OS="$(uname -s)"

# 平臺相容
if [[ $OS =~ MINGW.* || $OS =~ CYGWIN.* ]]; then
  exec "${BIN_DIR}/flutter.bat" "$@"
fi

#3、source匯入這個shell指令碼後執行其內部的shared::execute方法
source "$BIN_DIR/internal/shared.sh"
shared::execute "$@"
複製程式碼

很明顯,我們需要將目光轉向 Flutter SDKbin/internal/shared.sh檔案,且關注其內部的shared::execute方法,如下:

#......
function shared::execute() {
  #1、預設FLUTTER_ROOT值為FlutterSDK根路徑
  export FLUTTER_ROOT="$(cd "${BIN_DIR}/.." ; pwd -P)"
  #2、如果存在就先執行bootstrap指令碼,預設SDK下面是沒有這個檔案的,我猜是預留給我們自定義初始化掛載用的。
  BOOTSTRAP_PATH="$FLUTTER_ROOT/bin/internal/bootstrap.sh"
  if [ -f "$BOOTSTRAP_PATH" ]; then
    source "$BOOTSTRAP_PATH"
  fi
  #3、一堆基於FlutterSDK路徑的位置定義
  FLUTTER_TOOLS_DIR="$FLUTTER_ROOT/packages/flutter_tools"
  SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
  STAMP_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.stamp"
  SCRIPT_PATH="$FLUTTER_TOOLS_DIR/bin/flutter_tools.dart"
  DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk"

  DART="$DART_SDK_PATH/bin/dart"
  PUB="$DART_SDK_PATH/bin/pub"

  #4、路徑檔案平臺相容,常規操作,忽略
  case "$(uname -s)" in
    MINGW*)
      DART="$DART.exe"
      PUB="$PUB.bat"
      ;;
  esac
  #5、測試執行指令碼的賬號是否為超級賬號,是的話警告提示,Docker和CI環境不警告。
  if [[ "$EUID" == "0" && ! -f /.dockerenv && "$CI" != "true" && "$BOT" != "true" && "$CONTINUOUS_INTEGRATION" != "true" ]]; then
    >&2 echo "   Woah! You appear to be trying to run flutter as root."
    >&2 echo "   We strongly recommend running the flutter tool without superuser privileges."
    >&2 echo "  /"
    >&2 echo "?"
  fi

  #6、測試git命令列環境配置是否正常,不正常就丟擲錯誤。
  if ! hash git 2>/dev/null; then
    >&2 echo "Error: Unable to find git in your PATH."
    exit 1
  fi
  #7、FlutterSDK是否來自clone等測試。
  if [[ ! -e "$FLUTTER_ROOT/.git" ]]; then
    >&2 echo "Error: The Flutter directory is not a clone of the GitHub project."
    >&2 echo "       The flutter tool requires Git in order to operate properly;"
    >&2 echo "       to install Flutter, see the instructions at:"
    >&2 echo "       https://flutter.dev/get-started"
    exit 1
  fi

  # To debug the tool, you can uncomment the following lines to enable checked
  # mode and set an observatory port:
  # FLUTTER_TOOL_ARGS="--enable-asserts $FLUTTER_TOOL_ARGS"
  # FLUTTER_TOOL_ARGS="$FLUTTER_TOOL_ARGS --observe=65432"
  #7、日常編譯遇到命令lock檔案鎖住問題就是他,本質該方法就是建立/bin/cache目錄並維持鎖狀態等事情,不是我們關心的重點。
  upgrade_flutter 7< "$PROG_NAME"
  #8、相關引數值,別問我怎麼知道的,問就是自己在原始碼對應位置echo輸出列印的
  # BIN_NAME=flutter、PROG_NAME=FLUTTER_SDK_DIR/bin/flutter
  # DART=FLUTTER_SDK_DIR/bin/cache/dart-sdk/bin/dart
  # FLUTTER_TOOLS_DIR=FLUTTER_SDK_DIR/packages/flutter_tools
  # FLUTTER_TOOL_ARGS=空
  # SNAPSHOT_PATH=FLUTTER_SDK_DIR/bin/cache/flutter_tools.snapshot
  # @=build apk
  BIN_NAME="$(basename "$PROG_NAME")"
  case "$BIN_NAME" in
    flutter*)
      # FLUTTER_TOOL_ARGS aren't quoted below, because it is meant to be
      # considered as separate space-separated args.
      "$DART" --disable-dart-dev --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
      ;;
    dart*)
      "$DART" "$@"
      ;;
    *)
      >&2 echo "Error! Executable name $BIN_NAME not recognized!"
      exit 1
      ;;
  esac
}
複製程式碼

可以看到,由於 Flutter SDK 內部內建了 Dart,所以當配置環境變數後 flutter、dart 命令都可以使用了。而我們安裝 Flutter SDK 後首先做的事情就是把 SDK 的 bin 目錄配置到了環境變數,所以執行的 flutter build apk、flutter upgrade、flutter pub xxx 等命令本質都是走進了上面這些指令碼,且 flutter 命令只是對 dart 命令的一個包裝,所以執行flutter pub get其實等價於dart pub get。所以假設我們執行flutter build apk命令,本質走到上面指令碼最終執行的命令如下:

FLUTTER_SDK_DIR/bin/cache/dart-sdk/bin/dart \
--disable-dart-dev --packages=FLUTTER_SDK_DIR/packages/flutter_tools/.packages \
FLUTTER_SDK_DIR/bin/cache/flutter_tools.snapshot \
build apk
複製程式碼

上面命令列中 FLUTTER_SDK_DIR 代表的就是 Flutter SDK 的根目錄,--packages可以理解成是一堆 SDK 相關依賴,FLUTTER_SDK_DIR/bin/cache/flutter_tools.snapshot就是FLUTTER_SDK_DIR/packages/flutter_tools的編譯產物。所以,上面其實通過 dart 命令執行flutter_tools.snapshot檔案也就是等價於執行flutter_tools.dartmain()方法。因此上面命令繼續簡化大致如下:

dart --disable-dart-dev --packages=xxx flutter_tools.dart build apk
複製程式碼

也就是說,我們執行的任何 flutter 命令,本質都是把引數傳遞到了FLUTTER_SDK_DIR/packages/flutter_tools/bin/flutter_tools.dart原始碼的 main 方法中,所以真正做事情的都在這部分原始碼裡。這裡由於篇幅問題不展開說明,後面專門寫一篇解析,然後與本文關聯閱讀即可徹底搞懂。

Flutter Plugin 安卓編譯流程

對於包含 android 程式碼的 flutter plugin 模組來說,其 android 部分就是一個標準的原生 android library,沒有任何額外的干預指令碼,所以就不分析了。這裡只是提醒下,當我們新建一個 flutter plugin 時,其專案預設除過 plugin 會幫我們生成一個 example 的模組,目的只是為了方便我們獨立開發 flutter plugin 時能脫離自己主專案進行 demo 驗證,大致目錄如下:

在這裡插入圖片描述

Flutter Module 安卓編譯流程

對於原生現有工程整合 flutter 來說,flutter module 就是最好的隔離選擇,這也就造就了其與 flutter app 在編譯上的一些差異與共性。這部分我們重點分析 flutter module 與 上面分析的 app 編譯流程差異,共性部分不再分析。

同樣先從.android/settings.gradle看起來:

// app 是測試 module,用來驗證 flutter module 的,本質最後 flutter module 會生成可整合的 aar
include ':app'
//匯入配置.android/include_flutter.groovy
rootProject.name = 'android_generated'
setBinding(new Binding([gradle: this]))
evaluate(new File(settingsDir, 'include_flutter.groovy'))
複製程式碼

目光轉向當前 flutter module 專案.android/include_flutter.groovy檔案,如下:

//1、以當前指令碼為座標找到當前專案根路徑
def scriptFile = getClass().protectionDomain.codeSource.location.toURI()
def flutterProjectRoot = new File(scriptFile).parentFile.parentFile
//2、匯入flutter module名稱為相對當前目錄的flutter
gradle.include ":flutter"
//3、flutter module android真正的實現位於.android/Flutter目錄下
gradle.project(":flutter").projectDir = new File(flutterProjectRoot, ".android/Flutter")
//4、前面見過了,就是獲取 flutter sdk 路徑,然後匯入指令碼
def localPropertiesFile = new File(flutterProjectRoot, ".android/local.properties")
def properties = new Properties()

assert localPropertiesFile.exists(), "❗️The Flutter module doesn't have a `$localPropertiesFile` file." +
                                     "\nYou must run `flutter pub get` in `$flutterProjectRoot`."
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }

def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
//5、類似之前,apply匯入一個flutter sdk目錄下的指令碼
gradle.apply from: "$flutterSdkPath/packages/flutter_tools/gradle/module_plugin_loader.gradle"
複製程式碼

目光轉向 Flutter SDK 目錄下packages/flutter_tools/gradle/module_plugin_loader.gradle指令碼檔案,你會發現和前面 app 的settings.gradle中 apply 的指令碼很像,也是自動配置一些依賴模組啥的,所以不分析了。

接著看看.android/app/build.gradle,你會發現他就是一個標準的 android app 指令碼,dependencies 中只是多了上面settings.gradle中的 flutter module,即implementation project(':flutter')

接著看看真正 flutter module android 相關的指令碼,即.android/Flutter/build.gradle,如下:

//......
apply plugin: 'com.android.library'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

//......
flutter {
    source '../..'
}
複製程式碼

咋說?不用我多解釋了吧,本質回到了flutter.gradle,我們前面已經分析過了,到此一切真相大白。

pubspec.yaml及相關流程分析

先看一下其內部內容,大致如下:

# 專案名稱和描述
name: f1
description: A new f1 project.
# 想要釋出的位置,刪除就是釋出到pub.dev
publish_to: 'none'
# 版本號,修改這裡後會自動修改安卓專案下local.properties檔案中的versionName、versionCode
version: 1.0.1+10
# dart SDK 版本範圍
environment:
  sdk: ">=2.13.0 <3.0.0"
# 編譯依賴
dependencies:
  flutter:
    sdk: flutter
  dio: ^4.0.0 #來自pub.dev的純dart依賴,即Flutter Package
  webview_flutter: ^2.0.10 #來自pub.dev的外掛依賴,即Flutter Plugin
  f_package: #來自本地的純dart依賴,即Flutter Package
    path: ./../f_package
  f_plugin: #來自本地的外掛依賴,即Flutter Plugin
    path: ./../f_plugin
# 開發模式依賴
dev_dependencies:
  flutter_test:
    sdk: flutter
# ......
複製程式碼

pubspec.yaml檔案中version: 1.0.1+10修改後會自動覆蓋android/local.properties中的flutter.versionNameflutter.versionCode。當我們追加依賴後一般都會執行flutter pub get或者flutter pub upgrade等命令來更新,這個命令背後的邏輯其是也是走進了我們上面 Flutter SDK 下bin/flutter編譯命令分析相關內容。

總結

到此,Flutter Android 應用層編譯的全方位都分析到位了。由於篇幅問題,下一篇我們接續分析 Flutter SDK 下bin/flutter編譯命令的本質FLUTTER_SDK_DIR/packages/flutter_tools/flutter_tools.dart原始碼,敬請期待。

相關文章