Flutter 做爲當下比較流行的技術,很多公司已經開始在原生項目中接入它,但這也帶來了一些問題:android
文章基於 v1.5.4-hotfix.2 Flutter SDK 版本git
要優化它,就須要先了解它。以 Android 爲例,要接入 Flutter 很方便,首先在 settings.gradle 中:github
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}
複製代碼
這裏會將 Flutter 所依賴的第三方插件,include 到咱們項目中,而相關的配置就記錄在 .flutter-plugins 中。接着在 app 模塊下的 build.gradle 中:api
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
複製代碼
flutter.gradle 這個文件在 Flutter SDK 目錄中,咱們上面說到編譯成產物的操做就是在這個腳本中定義的。因此咱們注重看下這個文件:緩存
apply plugin: FlutterPlugin
複製代碼
FlutterPlugin 是一個自定義的 Gradle Plugin,並且也是定義在這個文件中的。閉包
project.android.buildTypes {
profile {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
dynamicProfile {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
dynamicRelease {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
}
複製代碼
除了默認的 debug 和 release 以外,Flutter 會定義 profile、dynamicProfile、dynamicRelease 這三種 buildType,這裏須要注意下,若是項目已經定義了同名的 buildType 的話。matchingFallbacks
表示若是引用的模塊中不存在相同的 buildType,則使用這些替補選項。架構
if (project.hasProperty('localEngineOut')) {
//...
}
複製代碼
localEngineOut
能夠用於指定特定的 engine 目錄,默認用 SDK 中的,若是本身從新編譯了 engine,能夠用這個選項來指向。具體可見:Flutter-Engine-編譯指北app
Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
String targetArch = 'arm'
if (project.hasProperty('target-platform') &&
project.property('target-platform') == 'android-arm64') {
targetArch = 'arm64'
}
debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile()
profileFlutterJar = baseEnginePath.resolve("android-${targetArch}-profile").resolve("flutter.jar").toFile()
releaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-release").resolve("flutter.jar").toFile()
dynamicProfileFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-profile").resolve("flutter.jar").toFile()
dynamicReleaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-release").resolve("flutter.jar").toFile()
if (!debugFlutterJar.isFile()) {
project.exec {
executable flutterExecutable.absolutePath
args "--suppress-analytics"
args "precache"
}
if (!debugFlutterJar.isFile()) {
throw new GradleException("Unable to find flutter.jar in SDK: ${debugFlutterJar}")
}
}
// Add x86/x86_64 native library. Debug mode only, for now.
flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar")
Task flutterX86JarTask = project.tasks.create("${flutterBuildPrefix}X86Jar", Jar) {
destinationDir flutterX86Jar.parentFile
archiveName flutterX86Jar.name
from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") {
into "lib/x86"
}
from("${flutterRoot}/bin/cache/artifacts/engine/android-x64/libflutter.so") {
into "lib/x86_64"
}
}
// Add flutter.jar dependencies to all <buildType>Api configurations, including custom ones
// added after applying the Flutter plugin.
project.android.buildTypes.each { addFlutterJarApiDependency(project, it, flutterX86JarTask) }
project.android.buildTypes.whenObjectAdded { addFlutterJarApiDependency(project, it, flutterX86JarTask) }
複製代碼
這裏的代碼看起來很長,其實作的事情就是一件,添加 flutter.jar 依賴,不一樣的 buildType 添加不一樣的版本,debug 模式額外增長 x86/x86_64 架構的版本。maven
project.extensions.create("flutter", FlutterExtension)
project.afterEvaluate this.&addFlutterTask
複製代碼
首先添加一個 FlutterExtension 配置塊,可選的配置有 source 和 target,用於指定編寫的 Flutter 代碼目錄和執行 Flutter 代碼的入口 dart 文件,默認爲 lib/main.dart
。post
在 afterEvaluate
鉤子上添加一個執行方法:addFlutterTask。
verbose
、filesystem-roots
、filesystem-scheme
這些一些額外可選的參數,這裏咱們先不關心。
if (project.android.hasProperty("applicationVariants")) {
project.android.applicationVariants.all addFlutterDeps
} else {
project.android.libraryVariants.all addFlutterDeps
}
複製代碼
存在 applicationVariants
屬性表示當前接入 Flutter 的模塊是使用 com.android.application
,applicationVariants
和 libraryVariants
都是表示當前模塊的構建變體,addFlutterDeps
是一個閉包,這裏的意思是,遍歷全部變體,調用 addFlutterDeps。
def addFlutterDeps = { variant ->
String flutterBuildMode = buildModeFor(variant.buildType)
if (flutterBuildMode == 'debug' && project.tasks.findByName('${flutterBuildPrefix}X86Jar')) {
Task task = project.tasks.findByName("compile${variant.name.capitalize()}JavaWithJavac")
if (task) {
task.dependsOn project.flutterBuildX86Jar
}
task = project.tasks.findByName("compile${variant.name.capitalize()}Kotlin")
if (task) {
task.dependsOn project.flutterBuildX86Jar
}
}
FlutterTask flutterTask = project.tasks.create(name: "${flutterBuildPrefix}${variant.name.capitalize()}", type: FlutterTask) {
flutterRoot this.flutterRoot
flutterExecutable this.flutterExecutable
buildMode flutterBuildMode
localEngine this.localEngine
localEngineSrcPath this.localEngineSrcPath
targetPath target
verbose verboseValue
fileSystemRoots fileSystemRootsValue
fileSystemScheme fileSystemSchemeValue
trackWidgetCreation trackWidgetCreationValue
compilationTraceFilePath compilationTraceFilePathValue
createPatch createPatchValue
buildNumber buildNumberValue
baselineDir baselineDirValue
buildSharedLibrary buildSharedLibraryValue
targetPlatform targetPlatformValue
sourceDir project.file(project.flutter.source)
intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
extraFrontEndOptions extraFrontEndOptionsValue
extraGenSnapshotOptions extraGenSnapshotOptionsValue
}
// We know that the flutter app is a subproject in another Android app when these tasks exist.
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
dependsOn flutterTask
if (packageAssets && cleanPackageAssets) {
dependsOn packageAssets
dependsOn cleanPackageAssets
into packageAssets.outputDir
} else {
dependsOn variant.mergeAssets
dependsOn "clean${variant.mergeAssets.name.capitalize()}"
into variant.mergeAssets.outputDir
}
with flutterTask.assets
}
if (packageAssets) {
String mainModuleName = "app"
try {
String tmpModuleName = project.rootProject.ext.mainModuleName
if (tmpModuleName != null && !tmpModuleName.empty) {
mainModuleName = tmpModuleName
}
} catch (Exception e) {
}
// Only include configurations that exist in parent project.
Task mergeAssets = project.tasks.findByPath(":${mainModuleName}:merge${variant.name.capitalize()}Assets")
if (mergeAssets) {
mergeAssets.dependsOn(copyFlutterAssetsTask)
}
} else {
variant.outputs[0].processResources.dependsOn(copyFlutterAssetsTask)
}
}
複製代碼
variant
就是上面遍歷的構建變體。首先當構建類型爲 debug 時,會在 compileJavaWithJavac 和 compileKotlin 這兩個 task 以前先執行 flutterBuildX86Jar task。它的做用是引入 x86 架構的 jar 和 so 文件。
這裏有個 bug
project.tasks.findByName('${flutterBuildPrefix}X86Jar') 複製代碼
判斷是否存在 task 時,拼接字符串用的是單引號,正確應該用雙引號,最新版本已經改正了。
接下來,會建立兩個 task,flutterBuild 和 copyFlutterAssets,flutterBuild 用於編譯產物,copyFlutterAssets 則是將產物拷貝到 assets 目錄。由於使用 com.android.application
和 com.android.library
擁有的 task 是不同的,全部這裏用是否存在 packageAssets 和 cleanPackageAssets 這兩個 task 去判斷引用不一樣插件的模塊,同時引入 library 插件的模塊,flutterBuild 須要依賴於這兩個 task。
flutterBuild task 實際上 FlutterTask 類型,同時 FlutterTask 繼承於 BaseFlutterTask。
abstract class BaseFlutterTask extends DefaultTask {
@OutputFiles
FileCollection getDependenciesFiles() {
FileCollection depfiles = project.files()
// Include the kernel compiler depfile, since kernel compile is the
// first stage of AOT build in this mode, and it includes all the Dart
// sources.
depfiles += project.files("${intermediateDir}/kernel_compile.d")
// Include Core JIT kernel compiler depfile, since kernel compile is
// the first stage of JIT builds in this mode, and it includes all the
// Dart sources.
depfiles += project.files("${intermediateDir}/snapshot_blob.bin.d")
return depfiles
}
}
複製代碼
@OutputFiles
註解用於標示 task 輸出的目錄,這個能夠用來作增量編譯和任務緩存等等。
class FlutterTask extends BaseFlutterTask {
@TaskAction
void build() {
buildBundle()
}
}
void buildBundle() {
if (!sourceDir.isDirectory()) {
throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
}
intermediateDir.mkdirs()
if (buildMode == "profile" || buildMode == "release") {
project.exec {
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
args "build", "aot"
args "--suppress-analytics"
args "--quiet"
args "--target", targetPath
args "--target-platform", "android-arm"
args "--output-dir", "${intermediateDir}"
if (trackWidgetCreation) {
args "--track-widget-creation"
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"
}
if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
}
if (buildSharedLibrary) {
args "--build-shared-library"
}
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
args "--${buildMode}"
}
}
project.exec {
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
args "build", "bundle"
args "--suppress-analytics"
args "--target", targetPath
if (verbose) {
args "--verbose"
}
if (fileSystemRoots != null) {
for (root in fileSystemRoots) {
args "--filesystem-root", root
}
}
if (fileSystemScheme != null) {
args "--filesystem-scheme", fileSystemScheme
}
if (trackWidgetCreation) {
args "--track-widget-creation"
}
if (compilationTraceFilePath != null) {
args "--compilation-trace-file", compilationTraceFilePath
}
if (createPatch) {
args "--patch"
args "--build-number", project.android.defaultConfig.versionCode
if (buildNumber != null) {
assert buildNumber == project.android.defaultConfig.versionCode
}
}
if (baselineDir != null) {
args "--baseline-dir", baselineDir
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"
}
if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
}
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
if (buildMode == "release" || buildMode == "profile") {
args "--precompiled"
} else {
args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
}
args "--asset-dir", "${intermediateDir}/flutter_assets"
if (buildMode == "debug") {
args "--debug"
}
if (buildMode == "profile" || buildMode == "dynamicProfile") {
args "--profile"
}
if (buildMode == "release" || buildMode == "dynamicRelease") {
args "--release"
}
if (buildMode == "dynamicProfile" || buildMode == "dynamicRelease") {
args "--dynamic"
}
}
}
複製代碼
用 @TaskAction
表示的方法就是 task 執行時候的方法。這裏代碼也很長,其實就是執行了兩個命令。第一,若是是 release 或 profile 模式下,執行 flutter build aot
。而後執行 flutter build bundle
。
分析完 Flutter 接入的流程後,再回頭去看咱們一開始面臨的問題,如今咱們來解決它。
爲了其餘成員不須要依賴於 Flutter 環境,首先咱們須要將 Flutter 代碼提早生成爲 aar,之因此不是 jar,是由於有圖片資源等。生成產物的命令能夠參照 FlutterBuildTask,要注意的是,debug 和 release 模式下生成的產物是不一致的。
debug 模式下的構建產物:
release 模式下的構建產物:
Flutter 產物生成不麻煩,照搬命令便可,這主要解決的問題是,Flutter 模塊中依賴的第三方插件,上面咱們說到,Flutter 模塊依賴的第三方插件會生成到配置文件 .flutter-plugins 中。而後在 settings.gradle 中,將這些項目的源碼加入咱們項目的依賴中去。全部,咱們要提早構建的話,就須要將這些代碼也打進咱們的 aar 中。惋惜,官方不支持這種操做,這時候須要第三方庫來支持了,fataar-gradle-plugin,不過這個庫有個小坑,Android Gradle 插件 3.1.x 的時候,沒有將 jni 目錄的 so 輸出到 aar 中,解決方式,添加:
project.copy {
from "${project.projectDir.path}/build/intermediates/library_and_local_jars_jni/${variantName}"
include "**"
into "${temporaryDir.path}/${variantName}/jni"
}
複製代碼
通過這兩個步驟後,咱們就能提早將 Flutter 產物和第三方插件的 aar 都打包一個 aar,上傳 maven 上等等。
由於 Android 項目和 iOS 項目都須要用到同一套 Flutter 源碼,因此這裏咱們可使用 git 提供的 submodule 的形式接入源碼。關於 Flutter SDK 版本管理,能夠參照以前的文章:flutterw
由於篇幅緣由,因此不能將實現細節完整寫出來,只能將一些關鍵點整理出來,但願能對你們有點啓發。有其餘疑問,歡迎留言討論。