無侵入引入Flutter模塊

前言

Flutter 做爲當下比較流行的技術,很多公司已經開始在原生項目中接入它,但這也帶來了一些問題:android

  • Flutter SDK 問題,在 Android 中,Flutter 的代碼和 Framework 會被編譯成產物,並且 debug 和 release 生成的產物也是不太同樣的。要編譯就須要有 SDK,這意味着其餘成員也須要下載 Flutter SDK,即便他不須要開發 Flutter 模塊,還有 Flutter 版本的管理也是一個問題,不過這個已經有解決方案了。
  • Android 和 iOS 項目須要共用一套 Flutter 代碼,這就須要用合適的方式去管理 Flutter 模塊。

文章基於 v1.5.4-hotfix.2 Flutter SDK 版本git

Flutter的接入

要優化它,就須要先了解它。以 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.dartpost

afterEvaluate 鉤子上添加一個執行方法:addFlutterTask。

verbosefilesystem-rootsfilesystem-scheme 這些一些額外可選的參數,這裏咱們先不關心。

if (project.android.hasProperty("applicationVariants")) {   
    project.android.applicationVariants.all addFlutterDeps  
} else {                                                    
    project.android.libraryVariants.all addFlutterDeps      
}                                                           
複製代碼

存在 applicationVariants 屬性表示當前接入 Flutter 的模塊是使用 com.android.applicationapplicationVariantslibraryVariants 都是表示當前模塊的構建變體,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.applicationcom.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 接入的流程後,再回頭去看咱們一開始面臨的問題,如今咱們來解決它。

生成 aar

爲了其餘成員不須要依賴於 Flutter 環境,首先咱們須要將 Flutter 代碼提早生成爲 aar,之因此不是 jar,是由於有圖片資源等。生成產物的命令能夠參照 FlutterBuildTask,要注意的是,debug 和 release 模式下生成的產物是不一致的。

debug 模式下的構建產物:

debug

release 模式下的構建產物:

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

結尾

由於篇幅緣由,因此不能將實現細節完整寫出來,只能將一些關鍵點整理出來,但願能對你們有點啓發。有其餘疑問,歡迎留言討論。

相關文章
相關標籤/搜索