主頁 > 移動端開發 > Flutter Android 工程結構及應用層編譯原始碼深入分析

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

2021-07-21 14:05:36 移動端開發

背景

本文部分配圖及原始碼最近基于 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中),以 https://pub.flutter-io.cn/packages/dio為例,這個目錄下 lib 為專案主要依賴,如下:
在這里插入圖片描述
對應在 Android Studio 中依賴展開的樣子如下:
在這里插入圖片描述
對于依賴 Flutter Plugin 下載位置在你 Flutter SDK 目錄下的.pub-cache\hosted\pub.dartlang.org\webview_flutter-2.0.10位置(mac 下在自己賬號目錄下的.pub-cache中),以 https://pub.flutter-io.cn/packages/webview_flutter為例,這個目錄下 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原始碼,敬請期待,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/289330.html

標籤:其他

上一篇:uniapp打包Android應用步驟(不包括IOS)

下一篇:10分鐘手把手教你用Android手擼一個簡易的個人記賬App

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more