Flutter Android 工程结构及应用层编译源码深入分析
Posted 工匠若水
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter Android 工程结构及应用层编译源码深入分析相关的知识,希望对你有一定的参考价值。
Flutter 系列文章连载~
- 《Flutter Android 工程结构及应用层编译源码深入分析》
- 《Flutter 命令本质之 Flutter tools 机制源码深入分析》
- 《Flutter 的 runApp 与三棵树诞生流程源码分析》
- 《Flutter Android 端 Activity/Fragment 流程源码分析》
- 《Flutter Android 端 FlutterInjector 及依赖流程源码分析》
- 《Flutter Android 端 FlutterEngine Java 相关流程源码分析》
- 《Flutter Android 端 FlutterView 相关流程源码分析》
- 《Flutter 绘制动机 VSYNC 流程源码全方位分析》
- 《Flutter 安卓 Platform 与 Dart 端消息通信方式 Channel 源码解析》
背景
本文部分配图及源码最近基于 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 = "$variantNameApi";
else
configuration = "$variantNameCompile";
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'以上是关于Flutter Android 工程结构及应用层编译源码深入分析的主要内容,如果未能解决你的问题,请参考以下文章
Flutter Android 工程结构及应用层编译源码深入分析
Flutter Android 端 FlutterInjector 及依赖流程源码分析
Flutter Android 端 FlutterInjector 及依赖流程源码分析
Flutter Android 端 FlutterInjector 及依赖流程源码分析