Flutter Android 工程结构及应用层编译源码深入分析

Posted 工匠若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter Android 工程结构及应用层编译源码深入分析相关的知识,希望对你有一定的参考价值。

Flutter 系列文章连载~

背景

本文部分配图及源码最近基于 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 及依赖流程源码分析

Flutter Android 端 FlutterInjector 及依赖流程源码分析

Flutter Android 端 FlutterView 相关流程源码分析