Android 混合Flutter之產物整合方式

真丶深紅騎士發表於2019-09-02

一、前言

上一篇文章Android 混合Flutter之原始碼整合方式有優點和也有缺點:

優點

  • 1.簡單快捷,Google原生支援
  • 2.開發除錯方便,和原生互動較多或需要依賴原生資料環境的時候特別能體現出來

缺點

  • 1.團隊所有人都可能要會Flutter並且都要安裝Flutter環境
  • 2.需要對現有的編譯體系做出修改,也就是要同時編譯Flutter專案和Native專案
  • 3.Flutter會直接侵入到Native專案中去
  • 4.編譯速度慢

Android混合Flutter除了上面所說的原始碼整合方式還有沒有其他方式呢?答案肯定是有的,那就是Flutter以產物的方式整合到Native,簡而言之將開發的Flutter專案單獨編譯成aar檔案,然後以元件的形式被主工程(Native工程)依賴,aar檔案可以以maven方式(遠端方式)的依賴,本文主要為了體驗產物整合和原始碼整合方案對比,就先用本地依賴的方式來整合。

二、Flutter專案

1.編寫Flutter專案程式碼

這裡和原始碼整合不同的是在New Flutter Project選擇的是Flutter Application而不是Flutter Module

建立Flutter專案
專案結構具體如下:

專案結構
把上一篇原始碼整合Flutter專案的dart檔案拉到這個專案中,程式碼就不貼出來了,主要是根據路由去跳轉不同的頁面。

2.build.gradle

首先看android目錄下build.gradle:

build.gradle
如果做過安卓領域的同學對這個檔案很熟悉了,在原生安卓專案app模組下也有這個檔案,這個檔案是app模組的gradle構建指令碼,一般用來管理app包名、版本號以及新增修改依賴庫,在Flutter專案中,這個檔案是由Flutter SDK生成的,相比原生安卓工程有些許不同,當然如果你根據gradle的知識體系來理解就行,下面看看這個檔案:

//取得`local.properties`中的關於Flutter相關屬性
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}
//獲取flutter.sdk資訊
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.")
}

//獲取flutter.versionCode
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

//獲取flutter.versionName
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 {
    //編譯版本
    compileSdkVersion 28
    //lint配置
    lintOptions {
        disable 'InvalidPackage'
    }
    
    //基本配置資訊 包名,最低支援版本號 版本號 版本名字等
    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutter_app"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            //可以增加簽名資訊
            signingConfig signingConfigs.debug
        }
    }
}
flutter {
    source '../..'
}
dependencies {
    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'
}
複製程式碼

apply from:"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"這句話是引入flutter.gradle配置模組,可以這麼理解向普通Android工程打包流程插入一些Flutter Task任務,簡單的話用一下一張圖描述:

gradle執行順序
flutter.gradle原始碼 大體結構如下:

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}

android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

apply plugin:FlutterPlugin
class FlutterPlugin implements Plugin<Project>{...}
class FlutterExtension {...}
abstract FlutterTask extends BaseFlutterTask{...}
gradle.useLogger(new FlutterEventLogger)
class FlutterEventLogger extends BuildAdapter implements TaskExecutinListener{...}
複製程式碼

flutter.gradle配置了一個名為FlutterPlugin的外掛,這個外掛實現了Plugin<Project>介面的apply方法,這是標準的gradle plugin,那麼它肯定會定義一些task和必要的依賴,在addFlutterTask這個方法可以體現:

          .....
           // We know that the flutter app is a subproject in another Android
           // app when these tasks exist.
           //我們知道flutter應用程式是另一個安卓系統的子專案
            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 compileTasks
                if (packageAssets && cleanPackageAssets) {
                    //擋在flutter模組中,存在cleanPackageAssets和packageAssets時
                    dependsOn packageAssets
                    dependsOn cleanPackageAssets
                    into packageAssets.outputDir
                } else {
                    //依賴於mergeAssets任務
                    dependsOn variant.mergeAssets
                    //依賴於cleanAssets任務
                    dependsOn "clean${variant.mergeAssets.name.capitalize()}"
                    variant.mergeAssets.mustRunAfter("clean${variant.mergeAssets.name.capitalize()}")
                    into variant.mergeAssets.outputDir
                }
                compileTasks.each { flutterTask ->
                    //執行flutterTask的getAssets方法
                    with flutterTask.assets
                }
            }
            //processResource依賴於copyFlutterAssetsTask
            variant.outputs.first().processResources.dependsOn(copyFlutterAssetsTask)
複製程式碼

從上面原始碼可以看出processResource這個Task依賴於copyFlutterAssetsTask,意思是要先執行完copyFlutterAssetsTask才能執行processResource,看英文意思就把flutter相關Task加到gradle的編譯流程中,另外copyFlutterAssetsTask依賴了mergeAssetsflutterTask,也就是當mergeAssets(Android的assets處理完成後)和flutterTask(flutter編譯完)和執行完,Flutter產物就會被copyFlutterAssetsTask根據debug還是release複製到build/app/intermediates/merged_assets/debug/mergeDebugAssets/out或者build/app/intermediates/merged_assets/release/mergeReleaseAssets/outFlutter的編譯產物,具體是在flutterTaskgetAssets方法指定的:

class FlutterTask extends BaseFlutterTask {
    @OutputDirectory
    File getOutputDirectory() {
        return intermediateDir
    }

    CopySpec getAssets() {
        return project.copySpec {
            from "${intermediateDir}"
            include "flutter_assets/**" // the working dir and its files
        }
    }
    ......
}
複製程式碼

也就是說,這些產物就是build/app/intermediates/flutter/xxx(xxx指debug或者release)下面的flutter_assets/目錄中的所有內容,那現在在命令列輸入打包命令flutter build apk,會編譯生成apk檔案,路徑位於build/app/outputs/apk/release/app-release.apk,注意如果你輸入flutter build apk,實際預設打release包,也就是等價於flutter build --release,如果需要打debug包,可以輸入flutter build apk --debug:

Flutter_build_apk

Flutter產物分析
可以生成很多產物,這些產物都是來自Flutter構建程式碼:

final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');                                              
if (buildSharedLibrary || platform == TargetPlatform.ios) {                                                               
  // Assembly AOT snapshot.                                                                                               
  outputPaths.add(assembly);                                                                                              
  genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');                                                                
  genSnapshotArgs.add('--assembly=$assembly');                                                                            
} else {                                                                                                                  
  // Blob AOT snapshot.                                                            
  final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');                                         
  final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');                               
  final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr');                                
  final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');                      
  outputPaths.addAll(<String>[vmSnapshotData, isolateSnapshotData, vmSnapshotInstructions, isolateSnapshotInstructions]); 
  genSnapshotArgs.addAll(<String>[                                                                                        
    '--snapshot_kind=app-aot-blobs',                                                                                      
    '--vm_snapshot_data=$vmSnapshotData',                                                                                 
    '--isolate_snapshot_data=$isolateSnapshotData',                                                                       
    '--vm_snapshot_instructions=$vmSnapshotInstructions', 
複製程式碼

看看debug模式下的產物:

Flutter下debug下的產物
可以發現lib檔案下多了x86_64x86arm64-v8a檔案並對應的so檔案,並且少了isolate_snapshot_instrvm_snapshot_instr,因為在debug下只會執行一個命令flutter build bundle,它會生成assetsvm_snapshot_dataisolate_snapshot_datarelease模式下,會執行兩個命令:flutter build aotflutter build bundle --precomiledAndroid預設使用app-aot-blobs,這種模式會生成isolate_snapshot_dataisolate_snapshot_instrvm_snapshot_datavm_snapshot_instr四個檔案,多生成兩個檔案只要是為了執行速度更快。

3.產物分析

3.1.assets資料夾

assets資料夾有isolate_snapshot_instrflutter_assetsvm_snapshot_datavm_snapshot_instr

  • flutter_assets:是Flutter工程產生的assets檔案,包含字型檔案,協議等。
  • isolate_snapshot_instr:包含由Dart isolate執行的AOT程式碼。
  • isolate_snapshot_data:表示isolates堆儲存區的初始狀態和特定的資訊,和vm_snapshot_data配合,更快啟動Dart_VM。
  • vm_snapshot_data:表示isolates之間共享的Dart堆儲存區的初始狀態,用於更快的啟動Dart VM。
  • vm_snapshot_instr:包含VM中所有的isolates之間的常見例程指令。

3.2.lib資料夾

lib資料夾是特定平臺(arm或者x86)的so檔案,FlutterAndroid平臺下會預設生成arm-v7架構的so庫,debug模式下同時生成x86_64x86arm64-v8a的so檔案,當然有的專案可能配置了

ndk{
    abiFilters 'armeabi'
}
複製程式碼

為了解決so對其問題,需要在Flutter專案中手動armeabi的so檔案,這樣的話打包出來就aar包含了armeabi的so檔案,這個armeabi的so檔案可以拷貝armeabi-v7下面的,一般情況下他們兩個是沒什麼區別,在app目錄下建立libs/armeabi,然後將libflutter.so拷貝到armeabi的目錄下,然後在gradle中配置

android{
	sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}
複製程式碼

因為Flutter SDK版本速度很快,每個版本打出的so檔案可能稍有不同,所有隻要升級sdk可能就需要拷貝so檔案,比較麻煩,所以可以監聽打包aar的任務來進行自動拷貝,在gradle檔案中配置以下程式碼

//以下任務為了拷貝so  因為Flutter預設只生成v7的so
task copyFlutterSo(dependsOn: 'transformNativeLibsWithSyncJniLibsForRelease', type: Copy) {
    //${buildDir} =  /Users/xueshanshan/project/flutter/flutter_debug_library/build/app
    def dir = "${buildDir}/intermediates/library_and_local_jars_jni/release"
    from "${dir}/armeabi-v7a/libflutter.so"
    into "${dir}/armeabi/"
}
複製程式碼

本文暫時還不需要用到這兩步。

4.打包aar檔案

上面通過編譯命令得到apk,那麼如果想打包aar,只要把app/build.gradle中的apply plugin: 'com.android.application'改為apply plugin: 'com.android,library' 並且把applicationId "com.example.flutter_app"註釋

打包aar檔案一
android目錄下的AndroidManifest.xmlandroid:label="xxx"android:name="xxxxx"註釋掉:

AndroidMenifest.xml檔案配置
Terminal執行下面命令,就能得到app-release.aar檔案

  • flutter clean
  • cd android
  • ./gradlew assembleRelease

得到aar檔案

三、Android專案

首先建立完Android專案,將上面打包成功的aar檔案以普通的aar整合到Android專案中去,首先將aar檔案拷貝到libs目錄下:

新增aar檔案
並且在app模組下配置build.gradle,對aar檔案的依賴

新增aar檔案依賴
這時候你會發現沒有Flutter類和FlutterFragment,在Android 混合Flutter之原始碼整合方式有提過,建立Flutter Module的時候,在.android->Flutter->io.flutter->facade會生成兩個java檔案,分別是FlutterFlutterFragment

  • Flutter:Android應用程式中使用Flutter的主要入口點
  • FlutterFragment:Fragment來管理FlutterView

下面把這兩個檔案複製過來:

新增Flutter和FlutterFragment
最後Android原生呼叫Flutter方式:

  • 繼承FlutterActivity
    @Override
    public FlutterView createFlutterView(Context context){
        getIntentData();
        WindowManager.LayoutParams matchParent = new WindowManager.LayoutParams(-1, -1);
        //建立FlutterNativeView
        FlutterNativeView nativeView = this.createFlutterNativeView();
        //建立FlutterView
        FlutterView flutterView = new FlutterView(FlutterMainActivity.this,(AttributeSet)null,nativeView);
        //給FlutterView傳遞路由引數
        flutterView.setInitialRoute(routeStr);
        //FlutterView設定佈局引數
        flutterView.setLayoutParams(matchParent);
        //將FlutterView設定進ContentView中,設定內容檢視
        this.setContentView(flutterView);
        return flutterView;
    }

複製程式碼
  • 繼承AppCompatActivity
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceStae){
        super.onCreate(savedInstanceStae);

        String route = getIntent().getStringExtra("_route_");
        String params = getIntent().getStringExtra("_params_");
        JSONObject jsonObject = new JSONObject();
        try{
            jsonObject.put("pageParams",params);
        } catch(JSONException e){
            e.printStackTrace();

        }
        //將FlutterView設定進ContentView中,設定內容檢視
        //建立FlutterView
        flutterView = Flutter.createView(this,getLifecycle(),route + "?" + jsonObject.toString());
        //設定顯示檢視
        setContentView(flutterView);
        //外掛註冊
        registerMethodChannel();

    }
複製程式碼
  • Fragment方式:
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState){
        Log.d(TAG,"onCreateView-mRoute:"+mRoute);

        mFlutterView = Flutter.createView(getActivity(),getLifecycle(),mRoute);
        //綜合解決閃屏,佈局覆蓋問題
        mFlutterView.setZOrderOnTop(true);
        mFlutterView.setZOrderMediaOverlay(false);
        mFlutterView.getHolder().setFormat(Color.parseColor("#00000000"));

        //註冊channel
       // GeneratedPluginRegistrant.registerWith(mFlutterView.getPluginRegistry());
        //返回FlutterView
        return mFlutterView;
    }
複製程式碼

實際效果如下圖:

最終效果圖

注意 這裡會牽扯到如果Flutter工程依賴了第三方的Flutter plugin那麼打包aar檔案的時候是無法把Plugin內容打進去的,網上有文章說可以用fataar-gradle-plugin或者fat-aar-android,找遍gradle沒找到修改的地方,可以採用這兩篇文章把flutter專案作為aar新增到已有的Android工程上Flutter混編一鍵打包並上傳maven的方法來實現。

四、總結

如果想要以混編的方式來開發專案,可以自行根據這兩種方案的特點來選擇,下面附上兩種方案的優缺點:

優缺點對比

五、參考資料

六、原始碼案例

本博文例子Flutter端

本博文例子Android端

相關文章