Android 混合Flutter之产物集成方式

1、前言

上一篇文章Android 混合Flutter之源码集成方式有优势和也有缺点:html

优势java

  • 1.简单快捷,Google原生支持
  • 2.开发调试方便,和原生交互较多或须要依赖原生数据环境的时候特别能体现出来

缺点android

  • 1.团队全部人均可能要会Flutter而且都要安装Flutter环境
  • 2.须要对现有的编译体系作出修改,也就是要同时编译Flutter项目和Native项目
  • 3.Flutter会直接侵入到Native项目中去
  • 4.编译速度慢

Android混合Flutter除了上面所说的源码集成方式还有没有其余方式呢?答案确定是有的,那就是Flutter以产物的方式集成到Native,简而言之将开发的Flutter项目单独编译成aar文件,而后以组件的形式被主工程(Native工程)依赖,aar文件能够以maven方式(远程方式)的依赖,本文主要为了体验产物集成和源码集成方案对比,就先用本地依赖的方式来集成。ios

2、Flutter项目

1.编写Flutter项目代码

这里和源码集成不一样的是在New Flutter Project选择的是Flutter Application而不是Flutter Modulegit

建立Flutter项目
项目结构具体以下:

项目结构
把上一篇源码集成 Flutter项目的 dart文件拉到这个项目中,代码就不贴出来了,主要是根据路由去跳转不一样的页面。

2.build.gradle

首先看android目录下build.gradle:github

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任务,简单的话用一下一张图描述:json

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这个方法能够体现:api

.....
           // 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:app

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文件

3、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的方法来实现。

4、总结

若是想要以混编的方式来开发项目,能够自行根据这两种方案的特色来选择,下面附上两种方案的优缺点:

优缺点对比

5、参考资料

6、源码案例

本博文例子Flutter端

本博文例子Android端