本篇主要讲解一个flutter工程是如何编译打包成一个apk的。
注意:这里我们是一个空的flutter项目,然后引入了一个webview-flutter的plugin来模拟一个plugin是如何一起打进去的。 flutter版本:2.4.0-5.0.pre.145
settings.gradle
因为最终产物是apk,所以其实总体上走的还是安卓编译,所以我们先来看settings.gradle文件: 这里我们可以看到,先读取了本地local.properties文件,里面包含了flutter sdk安装目录和android sdk安装目录等。根据flutter sdk安装目录,来到目录下的app_plugin_loader.gradle文件。 这里我们可以看到针对flutter-plugins-dependencies文件进行json解析,我们可以看下flutter-plugins-dependencies文件里的内容,包括了项目中各个平台引入的包的情况,我们项目中只引入了webview_flutter plugin。 ok,读取到了以后进行json解析,然后进行include和project对应处理,经过这一步以后settings.gradle就变成了
include ":webview_flutter"
project(":webview_flutter").projectDir = new File("xxxxxxx")
没错,这么看的话其实变成了普通的一个android项目的settings.gradle结构了。
根目录build.gradle
ok打包的话接下来我们来看build.gradle文件,先来看根目录下的build.gradle文件。 这里我们可以看到指定build产物目录为flutter项目目录的build文件下,以及build下的app目录下。以及首先我们会运行app依赖。ok接下去我们再来看app下的build.gradle文件。
app/build.gradle
首先读取了loacl.properties文件,读取了flutter sdk相关的参数以后,加载了flutter.gradle文件。我们来看看这个flutter sdk里的flutter.gradle文件具体做了些啥。 这里我们看到又apply了一个自定义的plugin FlutterPlugin,ok我就来看这个FlutterPlugin都做了些啥。
FlutterPlugin
class FlutterPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
this.project = project
def rootProject = project.rootProject
rootProject.tasks.register('generateLockfiles') {
rootProject.subprojects.each { subproject ->
def gradlew = (OperatingSystem.current().isWindows()) ?
"${rootProject.projectDir}/gradlew.bat" : "${rootProject.projectDir}/gradlew"
rootProject.exec {
workingDir rootProject.projectDir
executable gradlew
args ":${subproject.name}:dependencies", "--write-locks"
}
}
}
String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST
String repository = useLocalEngine()
? project.property('local-engine-repo')
: "$hostedRepository/download.flutter.io"
rootProject.allprojects {
repositories {
maven {
url repository
}
}
}
project.extensions.create("flutter", FlutterExtension)
this.addFlutterTasks(project)
if (shouldSplitPerAbi()) {
project.android {
splits {
abi {
enable true
reset()
universalApk false
}
}
}
}
if (project.hasProperty('deferred-component-names')) {
String[] componentNames = project.property('deferred-component-names').split(',').collect {":${it}"}
project.android {
dynamicFeatures = componentNames
}
}
getTargetPlatforms().each { targetArch ->
String abiValue = PLATFORM_ARCH_MAP[targetArch]
project.android {
if (shouldSplitPerAbi()) {
splits {
abi {
include abiValue
}
}
}
}
}
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")
}
engineVersion = useLocalEngine()
? "+"
: "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
"gradle", "flutter_proguard_rules.pro")
project.android.buildTypes {
profile {
initWith debug
if (it.hasProperty("matchingFallbacks")) {
matchingFallbacks = ["debug", "release"]
}
}
if (shouldShrinkResources(project)) {
release {
minifyEnabled true
shrinkResources isBuiltAsApp(project)
proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
}
}
}
if (useLocalEngine()) {
String engineOutPath = project.property('local-engine-out')
File engineOut = project.file(engineOutPath)
if (!engineOut.isDirectory()) {
throw new GradleException('local-engine-out must point to a local engine build')
}
localEngine = engineOut.name
localEngineSrcPath = engineOut.parentFile.parent
}
project.android.buildTypes.all this.&addFlutterDependencies
}
private static Boolean shouldShrinkResources(Project project) {
if (project.hasProperty("shrink")) {
return project.property("shrink").toBoolean()
}
return true
}
void addFlutterDependencies(buildType) {
String flutterBuildMode = buildModeFor(buildType)
if (!supportsBuildMode(flutterBuildMode)) {
return
}
if (!isFlutterAppProject() || getPluginList().size() == 0) {
addApiDependencies(project, buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
}
List<String> platforms = getTargetPlatforms().collect()
if (flutterBuildMode == "debug" && !useLocalEngine()) {
platforms.add("android-x86")
platforms.add("android-x64")
}
platforms.each { platform ->
String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
addApiDependencies(project, buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
}
}
ok整体过了一遍,我们接下来主要来看addFlutterTasks方法,这里才是整个编译过程最重要的一个地方。
private void addFlutterTasks(Project project) {
if (project.state.failure) {
return
}
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()
def addFlutterDeps = { variant ->
if (shouldSplitPerAbi()) {
variant.outputs.each { output ->
def abiVersionCode = ABI_VERSION.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride =
abiVersionCode * 1000 + variant.versionCode
}
}
}
String variantBuildMode = buildModeFor(variant.buildType)
String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
flutterRoot this.flutterRoot
flutterExecutable this.flutterExecutable
buildMode variantBuildMode
localEngine this.localEngine
localEngineSrcPath this.localEngineSrcPath
targetPath getFlutterTarget()
verbose isVerbose()
fastStart isFastStart()
fileSystemRoots fileSystemRootsValue
fileSystemScheme fileSystemSchemeValue
trackWidgetCreation trackWidgetCreationValue
targetPlatformValues = targetPlatforms
sourceDir getFlutterSourceDirectory()
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)
}
}
}
}
File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
//8.创建packLibsFlutterBuildDebug,packLibsFlutterBuildRelease,packLibsFlutterProfile指令,主要用于把build/intermediates/flutter/debug/目录下根据abi生成的app.so转换成libs.jar
Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
destinationDir libJar.parentFile
archiveName libJar.name
dependsOn compileTask
targetPlatforms.each { targetPlatform ->
String abi = PLATFORM_ARCH_MAP[targetPlatform]
from("${compileTask.intermediateDir}/${abi}") {
include "*.so"
// 9.把app.so转变成lib/<abi>/libapp.so
rename { String filename ->
return "lib/${abi}/lib${filename}"
}
}
}
}
//10.把packFlutterAppAotTask生成的产物添加到依赖里去
addApiDependencies(project, variant.name, project.files {
packFlutterAppAotTask
})
// 11.如果有is-plugin属性的话,我们就编译aar
boolean isBuildingAar = project.hasProperty('is-plugin')
// In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
// We know that `:flutter` is used as a subproject when these tasks exists and we aren't building an AAR.
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
//12.判断是否是FlutterModule依赖
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
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
//13.如果是flutter app module的话
if (isFlutterAppProject()) {
project.android.applicationVariants.all { variant ->
Task assembleTask = getAssembleTask(variant)
if (!shouldConfigureFlutterTask(assembleTask)) {
return
}
Task copyFlutterAssetsTask = addFlutterDeps(variant)
def variantOutput = variant.outputs.first()
def processResources = variantOutput.hasProperty("processResourcesProvider") ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
//14.将apk复制到已知位置,一遍遍运行flutter run,或者flutter build apk命令,一般默认位置是<app-dir>/build/app/outputs/flutter-apk/<filename>.apk
//
// The filename consists of `app<-abi>?<-flavor-name>?-<build-mode>.apk`.
// Where:
// * `abi` can be `armeabi-v7a|arm64-v8a|x86|x86_64` only if the flag `split-per-abi` is set.
// * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called.
// * `build-mode` can be `release|debug|profile`.
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"
}
}
}
}
}
//14.这里主要是去执行flutter pub get去获取flutter对应的依赖,并添加
configurePlugins()
return
}
//15.如果是模块依赖的方式集成到现在项目中的话,对模块做上述相应重复操作。
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()
}
packFlutterAppAotTask和copyFlutterAssetsTask两个task完成了以后相当于生成了flutter_assets目录下的这些flutter产物,最终将会编译打包进apk里,也就是apk里相对应的flutter_assets下。
至此,flutter打包的大框架上的流程就走完了,但你是不是发现其实还没到点上。。没错我们回到上面的第6条,我们来着重看下这个FlutterTask任务。
abstract class BaseFlutterTask extends DefaultTask {
@Internal
File flutterRoot
@Internal
File flutterExecutable
@Input
String buildMode
@Optional @Input
String localEngine
@Optional @Input
String localEngineSrcPath
@Optional @Input
Boolean fastStart
@Input
String targetPath
@Optional @Internal
Boolean verbose
@Optional @Input
String[] fileSystemRoots
@Optional @Input
String fileSystemScheme
@Input
Boolean trackWidgetCreation
@Optional @Input
List<String> targetPlatformValues
@Internal
File sourceDir
@Internal
File intermediateDir
@Optional @Input
String extraFrontEndOptions
@Optional @Input
String extraGenSnapshotOptions
@Optional @Input
String splitDebugInfo
@Optional @Input
Boolean treeShakeIcons
@Optional @Input
Boolean dartObfuscation
@Optional @Input
String dartDefines
@Optional @Input
String bundleSkSLPath
@Optional @Input
String codeSizeDirectory;
@Optional @Input
String performanceMeasurementFile;
@Optional @Input
Boolean deferredComponents
@Optional @Input
Boolean validateDeferredComponents
@OutputFiles
FileCollection getDependenciesFiles() {
FileCollection depfiles = project.files()
depfiles += project.files("${intermediateDir}/flutter_build.d")
return depfiles
}
void buildBundle() {
if (!sourceDir.isDirectory()) {
throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
}
intermediateDir.mkdirs()
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" }
}
project.exec {
logging.captureStandardError LogLevel.ERROR
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
if (verbose) {
args "--verbose"
} else {
args "--quiet"
}
args "assemble"
args "--no-version-check"
args "--depfile", "${intermediateDir}/flutter_build.d"
args "--output", "${intermediateDir}"
if (performanceMeasurementFile != null) {
args "--performance-measurement-file=${performanceMeasurementFile}"
}
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 "-dBundleSkSLPath=${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 {
@OutputDirectory
File getOutputDirectory() {
return intermediateDir
}
@Internal
String getAssetsDirectory() {
return "${outputDirectory}/flutter_assets"
}
@Internal
CopySpec getAssets() {
return project.copySpec {
from "${intermediateDir}"
include "flutter_assets/**"
}
}
@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();
}
@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()
}
}
按照task基础,我们直接来看@TaskAction,里面调用了buildBundle方法,我们来这个方法,里面重点执行了flutter编译命令脚本,饼配置了各种脚本参数,主要调用了flutter sdk下面的bin/flutter 编译命令。至此应用层面打包apk的流程就过完了。
|