揭开Flutter工程编译的面纱(Android篇)

一、引言

本文主要对Flutter工程编译时如何把Flutter生成的产物打包进入Android工程中进行分析。在Flutter工中打包过程中涉及到了local.properties、settings.gradle、build.gradle、flutter.gradle这几个脚本文件的参与,与传统的Android工程相比这几个脚本文件中的内容也不相同,接下来我们通过一层层解析,解开Flutter工程编译的面纱。 同时也建议大家看的时候搭配Flutter工程一起食用效果更佳。

二、工程结构分析

首先我们创建了一个最普通的Flutter工程flutter_new,创建后整个工程的目录结构如下:


image

Flutter工程下包括了Android和IOS两个目录,分别用于运行在Android和IOS平台上。其中android目录结果与Android工程的目录结构是一样的。Flutter工程中的android目录下包含了两个工程:第一个是android根工程,第二个是app子工程

image

稍微有点不同的地方在于这两个工程的的输出目录搬到了Flutter工程根目录build下:

image

Flutter工程中android工程与传统的Android工程相比,都有.gradle、gradle、 setting.gradlew、gradlew等目录和文件,文件基本上都是一样的,在但是在local.properties、settings.gradle、build.gradle文件内容上又有所不同,接下来我们我们会一一做对比。

三、local.properties

image

在根工程下的local.properties文件中了多了Flutter SDK相关的配置信息,包括SDK的路径、版本名、版本号,这些信息在构建工程的过程自动从环境变量中获取的,我们无需手动配置。
image

四、根工程settings.gradle

如果你有配置Flutter工程根目录下.flutter-plugins这个文件,那么下面的操作就会把flutter插件用到的第三方工程include到当前的工程中,并为其配置工程的路径 projectDir:

include ':app'

//1、根工程的父目录,既Flutter工程目录
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()

//2、把Flutte工程下.flutter-plugins文件内容读取到内存中
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
//3、把.flutter-plugins文件中配置的flutter插件工程包含到当前工程中
plugins.each { name, path ->
    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
    include ":$name"
    project(":$name").projectDir = pluginDirectory
}

然,我们新创建的Flutter工程默认是没有.flutter-plugins这个文件的,所以上面的代码基本不走。

五、根工程build.gradle

根工程中的build.grandle主要的工作是重新配置了工程的输出目录 和 工程间配置执行时的依赖关系:

buildscript {//...}

allprojects {//...}
 
//上面的代码是基本一样的

//第一点
rootProject.buildDir = '../build'  //根工程输出路径
subprojects {  //所有子工程输出路径
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
//第二点
subprojects {
    project.evaluationDependsOn(':app')  //为所有子工程配置app的依赖
}

//第三点
task clean(type: Delete) {
    delete rootProject.buildDir
}

第一点:配置了根工程 和 其所有子工程的输出路径。把所有的输出路径都搬到了Flutter根目录下的build目录中,如果是子工程则在build目录再建立属于自己名称的输出目录。可以看下面这张图:

image

第二点:所有子工程都配置app子工程的依赖,既让所有子工程运行配置阶段开始之前都要保证app工程的配置阶段都已经运行完毕。这样做的好处就是保证app工程的配置属性优先导入,防止其他子工程出现属性找不到的问题发生

第三点: 为根工程添加clean任务用来删除build目录下所有文件。

关于Project#evaluationDependsOn方法

evaluationDependsOn用于配置Project对象之间的依赖关系,跟Task的dependsOn原理一样。

举个例子,比如有两个工程app工程和lib工程,其中app依赖lib工程。在lib工程的build.gradle 添加如下的属性:

rootProject.ext.producerMsg = "Hello"

在app工程的build.gradle 添加如下的代码,既app工程使用lib工程的动态属性:

def msg = rootProject.ext.producerMsg

如果在在配置阶段app 工程先运行,这样就会导致app会导致producerMsg属性没有找到!因为此lib工程还未运行。

所以要解决这个问题 就要在app project运行配置之前,先运行lib project的配置,那么就可以用evaluationDependsOn来解决依赖,在app的build.gradle中添加如下依赖即可:

evaluationDependsOn(':lib') //运行app配置之前,先运行lib依赖

那么添加依赖之后,每次在运行app配置阶段之前,都会保证lib配置阶段先被执行。

六、APP工程build.gradle

build.gradle的内容如下,与原工程一样的地方就省略了:

//第一点:读取local.properties文件中内容到内存
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

//第二点:获取flutter sdk路径、versionCode、VersionName
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'

//第三点:导入flutter  gradle插件
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
  //省略...基本一样
}

//第四点
flutter {
    source '../..'
}

dependencies {
    //可见App工程并没有依赖support包
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

第一点:读取根工程下local.properties的内容到内存中(Properties), 该内容就是上面所介绍的Flutter SDK相关信息。
第二点:获取flutter sdk路径、versionCode、VersionName等信息。
第三点:从Flutter SDK目录下导入flutter gradle插件到当前工程中运行。
第四点:配置flutter插件的source属性,该属性指定了Flutter工程的路径。

该build.gradle最主要的功能从local.properties文件中获取Flutter SDK路径,并把该路径下的Flutter Gradle插件导入到当前工程中运行,接下来我们要看看该插件到底做了哪些工作。

七、flutter.gradle

Flutter代码打包到Android工程中秘密其实就是发生在flutter.gradle脚本中。该gradle脚本位于Flutter SDK/packages/flutter_tools/gradle/flutter.gradle中,接下来我们就揭开它的神秘面纱:

image

flutter.gradle代码分为了有两大核心部分:FlutterPluginFlutterTask

FlutterPlugin核心代码

apply方法

FlutterPlugin实现了Plugin接口,它是一个标准的gradle plugin。因此它的主入口在apply方法中,首先我们看看第一部分:

  @Override
    void apply(Project project) {
        // Add custom build types

        println "==== apply:" + project.getName() //app

        //1、新增profile、dynamicProfile、dynamicRelease 三种构建类型
        //在当前project下的android.buildTypes进行配置
        project.android.buildTypes {
            profile {
                initWith debug  //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']
                }
            }
        }

        //从根工程下local.properties文件中 获取SDK Flutter路径信息
        String flutterRootPath = resolveProperty(project, "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.")
        }

        println '===System.env.FLUTTER_ROOT: ' + System.env.FLUTTER_ROOT //默认为null
        println '===flutterRootPath: ' + flutterRootPath // /Users/chenrongyi/Develop/flutter/flutter

        flutterRoot = project.file(flutterRootPath)//仍然是Flutter SDK路径
        if (!flutterRoot.isDirectory()) {
            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
        }

        println '===flutterRoot: ' + flutterRoot // /Users/chenrongyi/Develop/flutter/flutter
        println '===Os.FAMILY_WINDOWS: ' + Os.isFamily(Os.FAMILY_WINDOWS) // 判断当前的系统环境

        //根据操作环境的不同选择执行 Flutter SDK/bin/flutter文件 还是 Flutter SDK/bin/flutter.bat文件
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
             
 }

第一部分主要的操作如下:
1、为当前工程新增 profile、dynamicProfile、dynamicRelease 这三种构建类型,而且大部分属性都是从debug中拷贝过来的。

image

2、从Android根工程的local.properties文件下获取Flutter SDK的路径和版本号信息。并且根据当前系统的设置运行Flutter默认程序: Linux/Mac OS执行的是Flutter SDK/bin/flutter,Windows执行的是Flutter SDK/bin/flutter.bat

第二部分代码如下:

        //当前工程是否有localEngineOut属性,默认为false,因此下面代码暂时不分析
        if (project.hasProperty('localEngineOut')) {
           //...
        } else {

            //获取Flutter引擎文件路径 Flutter SDK/bin/cache/artifacts/engine
            Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")

            println "=== 'target-platform :"+project.hasProperty('target-platform')//默认为false

            //根据target-platform的配置选项,选择arm平台,默认为arm
            String targetArch = 'arm'
            if (project.hasProperty('target-platform') &&
                    project.property('target-platform') == 'android-arm64') {
                targetArch = 'arm64'
            }

            //2、根据不同的配置 选择不同的jar包
            //android-arm/flutter.jar文件 (debug专用)
            debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile()
            //android-arm-profile/flutter.jar文件
            profileFlutterJar = baseEnginePath.resolve("android-${targetArch}-profile").resolve("flutter.jar").toFile()
            //android-arm-release/flutter.jar文件
            releaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-release").resolve("flutter.jar").toFile()
            //android-arm-dynamic-profile/flutter.jar文件
            dynamicProfileFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-profile").resolve("flutter.jar").toFile()
            //android-arm-dynamic-release//flutter.jar文件
            dynamicReleaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-dynamic-release").resolve("flutter.jar").toFile()

            println "===debugFlutterJar.isFile():"+debugFlutterJar.isFile() //true

            //如果android-arm/flutter.jar非文件或不存在,则运行Flutter SDK/bin/flutter脚本。
            //默认情况下debugFlutterJar文件都是存在的。
            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}")
                }
            }

            //定位当前工程下输出目录中的intermediates/flutter/flutter-x86.jar文件
            //注意,这里输出目录在根工程的build.gralde已经发生了改变,移动至Flutter工程的build/project下
            // Add x86/x86_64 native library. Debug mode only, for now.
            flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar")

            println "====flutterX86Jar: " + flutterX86Jar// .../flutter_new/flutter_new/build/app/intermediates/flutter/flutter-x86.jar

            //创建了一个任务,该任务的作用是把引擎目录下的x86 x64两个libflutter.so 打包成flutter-x86.jar包
            //该jar包生成于.../flutter_new/flutter_new/build/app/intermediates/flutter/flutter-x86.jar
            Task flutterX86JarTask = project.tasks.create("${flutterBuildPrefix}X86Jar", Jar) {
                destinationDir flutterX86Jar.parentFile  //压缩包生成的路径
                archiveName flutterX86Jar.name   //生成压缩包flutter-x86.jar的名称

                //下面是要拷贝的两个so路径 和 拷贝后的位置
                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.

            //重要:遍历buildTypes中的构建类型,并把根据当前构建类型,添加对应的jar包依赖
            project.android.buildTypes.each {
                println "====buildType:"+it.name //debug、dynamicProfile、dynamicRelease、profile、release
                addFlutterJarApiDependency(project, it, flutterX86JarTask)
            }
            //设置监听,当新的构建类型加入的时候执行的依赖操作
            project.android.buildTypes.whenObjectAdded {
                addFlutterJarApiDependency(project, it, flutterX86JarTask)
            }
        }

第二部分的主要操作如下:
1、获取Flutter引擎文件路径Flutter SDK/bin/cache/artifacts/engine,该目录下存在按构建环境目录分类的flutter.jar文件,该jar包含了Android代码的所以依赖的Flutter类文件 和 asserts/icudtl.dat 资源文件 和 lib/libflutter.so 库文件:

image

2、如果构建环境的debug,那么还会把Flutter SDK//bin/cache/artifacts/engine/android-x86/libflutter.so 和Flutter SDK/bin/cache/artifacts/engine/android-x64/libflutter.so 这两个so生成一个 flutter-x86.jar,该jar包生成在Flutter根工程下的build/app/intermediates/flutter/目录下:

image

这个jar包的目录结构大概是这样的:

lib
  -x86
     -libflutter.so
  -x86_64
     -libflutter.so

生成该jar包后,就会该jar包加入到该工程的依赖环境中,见如下的依赖代码:

/**
 *根据构建类型 添加指定的flutter.jar 包依赖
 */
private void addFlutterJarApiDependency(Project project, buildType, Task flutterX86JarTask) {
        project.dependencies {
            String configuration;

            if (project.getConfigurations().findByName("api")) {
                //plugin 3.0以上用api依赖
                configuration = buildType.name + "Api";
            } else {
                //plugin 3.0以下的用compile依赖
                configuration = buildType.name + "Compile";
            }
            //根据buildType的不同添加对应的jar包依赖,只依赖对应的构建类型jar包
            add(configuration, project.files {

                String buildMode = buildModeFor(buildType)
                //这里~ 如果是debug构建类型,还添加了flutter-x86.jar的依赖
                if (buildMode == "debug") {
                    [flutterX86JarTask, debugFlutterJar]
                } else if (buildMode == "profile") {
                    profileFlutterJar
                } else if (buildMode == "dynamicProfile") {
                    dynamicProfileFlutterJar
                } else if (buildMode == "dynamicRelease") {
                    dynamicReleaseFlutterJar
                } else {
                    releaseFlutterJar
                }
            })

        }
    }

因此可见如果是debug类型,相对于其他构建类型还增添了flutter-x86.jar的依赖,既多了两个so库。见编译后的apk工程结构:

image

第三分部如下:

      //指定自己的DSL扩展,FlutterExtension包含source和code两个属性
        project.extensions.create("flutter", FlutterExtension)

        //工程配置完毕后执行addFlutterTask方法,该方法的作用是把Flutter的代码和资源工程加入到Android工程中
        project.afterEvaluate this.&addFlutterTask

        //.flutter-plugins默认情况下是没有配置的,因此忽略
        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
        Properties plugins = readPropertiesIfExist(pluginsFile)
        plugins.each { name, _ ->
            ....
        }

第三步主要做的操作如下:
1、添加了Flutter插件的DSL扩展flutter{},其扩展的类是FlutterExtension,包含下面两个属性:

class FlutterExtension {
    String source
    String target
}

也就是说你可以用在工程的build.gradle中 使用 flutter { } 闭包来配置sourcetarget两个属性。

  • source:用来配置当前Flutter工程的根路径,注意不是Android工程,如果没有配置抛出Must provide Flutter source directory异常。
  • target:用来指定Flutter代码的启动入口,如果没有配置默认为lib/main.dart

2、在工程配置阶段结束后,执行addFlutterTask方法,该方法很重要,它的作用是把Flutter的代码和资源进行编译和处理并加入到Android工程中

addFlutterTask方法

addFlutterTask方法仍然比较多,分为两部分进行讲解。

第一部分如下:

     if (project.state.failure) {
            return
        }
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }

        //target属性指定了,Flutter启动的程序入口,如果没有配置默认为lib/main.dart
        String target = project.flutter.target
        if (target == null) {
            target = 'lib/main.dart'
        }
        if (project.hasProperty('target')) {
            target = project.property('target')
        }

        Boolean verboseValue = null
        if (project.hasProperty('verbose')) {
            verboseValue = project.property('verbose').toBoolean()
        }
        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 = false
        if (project.hasProperty('track-widget-creation')) {
            trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
        }
        String compilationTraceFilePathValue = null
        if (project.hasProperty('precompile')) {
            compilationTraceFilePathValue = project.property('precompile')
        }
        Boolean buildHotUpdateValue = false
        if (project.hasProperty('hotupdate')) {
            buildHotUpdateValue = project.property('hotupdate').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')
        }
        Boolean buildSharedLibraryValue = false
        if (project.hasProperty('build-shared-library')) {
            buildSharedLibraryValue = project.property('build-shared-library').toBoolean()
        }
        String targetPlatformValue = null
        if (project.hasProperty('target-platform')) {
            targetPlatformValue = project.property('target-platform')
        }

第一部分的主要操作很简单,就是从project工程中读取各自配置属性例如:target、verbose、filesystem-roots、target-platform等等,以备后面运行的时候使用。这里尤其对target和source属性做了特殊处理,具体处理方式上面已经说过了。

第二部分如下(关键):

 def addFlutterDeps = { variant ->   //variant对应的构建类型
            //获取当前的构建类型
            String flutterBuildMode = buildModeFor(variant.buildType)

         
            if (flutterBuildMode == 'debug' && project.tasks.findByName('${flutterBuildPrefix}X86Jar')) {
                //...
            }

            //根据构建类型,创建任务flutterBuild[构建类型] 任务,该任务是用于编译flutter dart代码
            FlutterTask flutterTask = project.tasks.create(name: "${flutterBuildPrefix}${variant.name.capitalize()}", type: FlutterTask) {
                //下面这些都是属性赋值
                flutterRoot this.flutterRoot
                flutterExecutable this.flutterExecutable
                //...中间忽略
                //Flutter根工程路径
                sourceDir project.file(project.flutter.source) 
                //Flutter编译的中间产物输出路径
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
                // ... /FlutterProject/flutter_new/flutter_new/build/app/intermediates/flutter/[debug,dynamicProfile,release..]  根据构建类型的不同
               
                //...
            }


            // We know that the flutter app is a subproject in another Android app when these tasks exist.
            //查找 :flutter:package[构建类型]Assets的Task【当flutter作为依赖库的时候,否则工程模式为null 】
            Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")

            //查找:flutter:cleanPackage[构建类型]Assets的Task【当flutter作为依赖库的时候,否则工程模式为null 】
            Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
        
            //创建 copyFlutterAssets[构建类型] 的task的拷贝操作.
            //同时依赖flutterTask、variant.mergeAssets (mergeDebugAssets)、 cleanMerge[构建类型]Assets
            Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
                dependsOn flutterTask
                dependsOn packageAssets ? packageAssets : variant.mergeAssets
                dependsOn cleanPackageAssets ? cleanPackageAssets : "clean${variant.mergeAssets.name.capitalize()}"

                //variant.mergeAssets.outputDir  == /build/app/intermediates/merged_assets/debug/mergeDebugAssets/out
                into packageAssets ? packageAssets.outputDir : variant.mergeAssets.outputDir
                //运行FlutterTask的getAssets方法执行拷贝操作
                with flutterTask.assets
            }
            if (packageAssets) { 
                // Only include configurations that exist in parent project.
                Task mergeAssets = project.tasks.findByPath(":app:merge${variant.name.capitalize()}Assets")
                if (mergeAssets) {
                    mergeAssets.dependsOn(copyFlutterAssetsTask)
                }
            } else {
                //最后processResources的task依赖copyFlutterAssetsTask
                variant.outputs[0].processResources.dependsOn(copyFlutterAssetsTask)
            }
        }
    

        if (project.android.hasProperty("applicationVariants")) {
            //applicationVariants.all对应多种构建类型,添加addFlutterDeps闭包
            project.android.applicationVariants.all addFlutterDeps
        } else {
            project.android.libraryVariants.all addFlutterDeps
        }

该部分主要做的操作如下:

  1. 创建名为flutterBuild[构建类型] 的task任务,该任务的执行逻辑位于FlutterTask类当中的build方法中
  2. 同时查找是否存在package[构建类型]Assets 和 cleanPackage[构建类型]Assets 这两个task。
  3. 创建copyFlutterAssets[构建类型]的task用于assert资源的拷贝操作拷贝逻辑位于FlutterTask的getAssets方法。 同时该task依赖flutterBuild[构建类型] task 和 上面两个task,由于上面两个task在只要在flutter库作为依赖的时候才存在,flutter工程模式下这两个task都为null。因此,转而依赖variant.mergeAssets( merge[构建类型]Assets )cleanMerge[构建类型]Assets 这两个task进行构建。
  4. 让构建类型process[构建类型]Resources 依赖 copyFlutterAssets[构建类型]

可见在整个Gradle构建过程中插入很多Flutter自行的Task,因此上面task整个依赖关系如下:
process[构建类型]Resources 【Android任务】 -> copyFlutterAssets[构建类型] 【Flutter任务】 -> flutterBuild[构建类型] 【Flutter任务】 merge[构建类型]Assets 【Android任务】、 cleanMerge[构建类型]Assets 【Android任务】

通过运行debug构建类型后的关系图如下: (使用了 cz.malohlava.visteg插件)

image

运行时候任务的构建顺序:
image

也就是说当flutterBuild[构建类型] 任务使得flutter编译完成,并且merge[构建类型]Assets执行完毕、也就是正常Android的assets处理完成后、flutter相应的产物就会被copyFlutterAssets[构建类型]复制到 Flutter根工程/build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下。

flutter的编译产物,具体是由FutterTask的getAssets方法指定的:

CopySpec getAssets() {
        return project.copySpec {
            //Flutte根工程/build/app/intermediates/flutter/[debug,dynamicProfile,release..]  
            from "${intermediateDir}"
         
            include "flutter_assets/**" // the working dir and its files

            if (buildMode == 'release' || buildMode == 'profile') {
                if (buildSharedLibrary) {
                    include "app.so"
                } else {
                    include "vm_snapshot_data"
                    include "vm_snapshot_instr"
                    include "isolate_snapshot_data"
                    include "isolate_snapshot_instr"
                }
            }
        }
    }

也就是说copyFlutterAssets[构建类型] 任务的作用就是把 Flutte根工程/build/app/intermediates/flutter/[构建类型] 目录下面flutter_assets/目录中所有的内容都拷贝到 Flutter根工程/build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下。如果是release或者profile版本的话,还包含拷贝了Dart的二进制产物snapshot 或 app.so,可以看到,除了默认情况下的snapshot,我们还可以指定Dart产物为还可以编译成的so库形式。

下面贴出了debug类型 和 release类型的拷贝对比:


image

image

那么Flutter的这些产物是怎么生成的呢?那么就要看 flutterBuild[构建类型] 任务是如何构建的,也就是我们接下来要讲的FlutterTask类

FlutterTask核心代码

FlutterTask继承自BaseFlutterTask,BaseFlutterTask是一个自定义的Task( DefaultTask),因此入口就在@TaskAction注解的build方法中,build方法直接调用BaseFlutterTask的buildBundle方法。

buildBundle方法代码也比较多,我们仍然分为两个部分。首先先看它的第一部分:

      intermediateDir.mkdirs()

        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }
        intermediateDir.mkdirs()

        //如果当前的构建类型 是profile和release 则先执行下面的操作
        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   //Flutter启动的程序入口,默认为lib/main.dart
                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}"
            }
        }
 //....

这里,由于是release版本,因此会先编译aot的二进制Dart产物,也就是snapshot产物,实际是执行以下命令(release):

flutter build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir 工程路径/build/app/intermediates/flutter/release --release

执行完成后会生成以下的文件在release目录中:

image

接着,buildBundle方法的后半部分还会调用一次flutter命令,不过这次命令是所有编译模式都会调用

        //执行下面的任务
        project.exec {
            //Users/chenrongyi/Develop/flutter/flutter/bin/flutter
            //执行bin/flutter程序
            executable flutterExecutable.absolutePath
            workingDir sourceDir

            // 当前工程是有localEngineOut属性,才会有localEngine,默认为null
            println "===localEngine:"+localEngine

            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
            args "build", "bundle"
            args "--suppress-analytics"
            args "--target", targetPath   //Flutter启动的程序入口,默认为lib/main.dart
            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 "--precompile", compilationTraceFilePath
            }
            if (buildHotUpdate) {
                args "--hotupdate"
            }
            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}"
            }
            //注意,debug和release 分别执行的参数不同
            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"
            }
        }

也就是执行了下面的命令(release):

flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --precompiled --asset-dir 工程路径/build/app/intermediates/flutter/release/flutter_assets --release

执行完成后,最终会生成一个flutter_assetss的资源目录在目录中:

image

上面通过两个命令最终生成的release资源产物与我们平时用release命令生成的结果是一致的。如果用要生成dubg模式下的产物那只要执行最后的命令代码,也就是执行下面的命令(debug):

flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --depfile 工程路径/build/app/intermediates/flutter/debug/snapshot_blob.bin.d --asset-dir 工程路径/build/app/intermediates/flutter/debug/flutter_assets --debug

执行后生成的文件资源如下:


image

注意执行上面的命令的时候要保证 intermediateDir文件目录已经创建,既...intermediates/flutter/debug/目录已经创建。命令执行的开头代码也显示的进行mkdir了。否则跑上面的命令会出现找不到目录的异常:


image

可以对比Release和Dubug模式下,上面的命令生成资源的对比:

image

在flutter_asserts目录中, 在debug模式下多了isolate_snapshot_data、vm_snapshot_data、kernel_blob.bin文件 。而其中isolate_snapshot_data、vm_snapshot_data这两个文件在release的外面目录中生成,除此之外还多了isolate_snapshot_instrvm_snapshot_instr这两个文件。因此总的来说realease模式比debug模式多了isolate_snapshot_instrvm_snapshot_instr 这两个文件,这两个文件属于AOT的指令段文件。

生成上面的Flutter构建产物后,就会执行下面的拷贝操作,也就是我们上一小结提到过的FutterTask的getAssets方法 负责把这些文件拷贝到 工程文件路径/build/app/intermediates/merged_assets/[构建类型]/merge[构建类型]Assets/out目录下 参与assets资源的编译中!


image

merged_assets中的这些文件就是最后都会打包到apk的assets目录下。下面对两个构建版本做了下简单的对比:


image

八、总结

总结下 Flutter工程混编进入Android工程整个流程大概如下:

  1. 读取根工程下local.properties获取Flutter SDK路径和版本信息
  2. 添加三种构建模式:dynamicProfile、dynamicRelease、profile
  3. 为工程添加flutter.jar包的依赖,该包包含flutter类文件、icudtl.dat资源文件和lib/libflutter.so 库文件
  4. 如果是debug版本,那么还把引擎库下的android-x64/libflutter.so 和 android-x86/libflutter.so 这两so打包成一个flutter-x86.jar,同时该jar包也作为依赖
  5. 执行Flutter命令构建Flutter产物,把生成的产物通过拷贝任务拷贝至merged_assets下。生成的产物包括flutter_asserts下等资源文件 和 snapshot 程序数据段
  6. 最后Android的资源处理任务会把merged_assets下所有flutter产物都打包到apk中asserts目录下,最终完成Flutter工程的混编工作

九、关于Flutter 1.2.1的补充

以上主要是针对Flutter 1.0 gradle脚本进行的分析。不过前些天Flutter推出了1.2.1版本,对flutter.gradle脚本文件也新增了某些的修改,不过总体来说影响并不大,主要是对一些BUG的修复。

1、新增了mainModuleName动态属性,用来指定主project的工程名:

image

默认情况下主proejct的工程名为app,如果用于擅自修改了工程名,那么就会出现编译异常的情况,见issues 26948 。并且在pull request 27154 修复了该问题,如果主工程名变更,那么只要在setBinding中传入主app名即可:

2、解决了当Flutter工程作为aar的依赖时,没有把icudtl.dat文件引入到aar中的问题,见issues18025

image

创建一个名为 copySharedFlutterAssets[构建类型] 的task,该task的作用是把flutter.jar包下assets/flutter_shared下所有文件都拷贝出来。因为在Flutter 1.0的版本中当作为aar进行打包的时候,jar包下的assets资源不会打包到aar包中,因此这里做了修复 (不过我发现在升级到1.2.1后,flutter.jar中已经不存在assets目录,icudtl.dat文件已经被移除, 现在已经被嵌入到 libflutter.so文件中了: Remove the flutter_shared assets directory from the Gradle script

欢迎关注我的公众号【不喝咖啡的程序员】,最新的文章会在上面发布:


image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容