Android 混合Flutter之产物集成方式
一、前言上一篇文章Android 混合Flutter之源码集成方式有优点和也有缺点:优点1.简单快捷,Google原生支持2.开发调试方便,和原生交互较多或需要依赖原生数据环境的时候特别能体现出来缺点1.团队所有人都可能要会Flutter并且都要安装Flutter环境2.需要对现有的编译体系做出修改,也就是要同时编译Flutter项目和Native项目3.Flutter会直接...
一、前言
上一篇文章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项目的dart文件拉到这个项目中,代码就不贴出来了,主要是根据路由去跳转不同的页面。
2.build.gradle
首先看android目录下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任务,简单的话用一下一张图描述:

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依赖了mergeAssets和flutterTask,也就是当mergeAssets(Android的assets处理完成后)和flutterTask(flutter编译完)和执行完,Flutter产物就会被copyFlutterAssetsTask根据debug还是release复制到build/app/intermediates/merged_assets/debug/mergeDebugAssets/out或者build/app/intermediates/merged_assets/release/mergeReleaseAssets/out,Flutter的编译产物,具体是在flutterTask的getAssets方法指定的:
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构建代码:
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模式下的产物:

可以发现lib文件下多了x86_64、x86、arm64-v8a文件并对应的so文件,并且少了isolate_snapshot_instr和vm_snapshot_instr,因为在debug下只会执行一个命令flutter build bundle,它会生成assets、vm_snapshot_data、isolate_snapshot_data。release模式下,会执行另个命令:flutter build aot,flutter build bundle --precomiled,Android默认使用app-aot-blobs,这种模式会生成isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data和vm_snapshot_instr四个文件,多生成两个文件只要是为了执行速度更快。
3.产物分析
3.1.assets文件夹
assets文件夹有isolate_snapshot_instr,flutter_assets,vm_snapshot_data,vm_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文件,Flutter在Android平台下会默认生成arm-v7架构的so库,debug模式下同时生成x86_64、x86和arm64-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"注释

在android目录下的AndroidManifest.xml把android:label="xxx"和android:name="xxxxx"注释掉:

在Terminal执行下面命令,就能得到app-release.aar文件
- flutter clean
- cd android
- ./gradlew assembleRelease

三、Android项目
首先创建完Android项目,将上面打包成功的aar文件以普通的aar集成到Android项目中去,首先将aar文件拷贝到libs目录下:

并且在app模块下配置build.gradle,对aar文件的依赖

这时候你会发现没有Flutter类和FlutterFragment,在Android 混合Flutter之源码集成方式有提过,创建Flutter Module的时候,在.android->Flutter->io.flutter->facade会生成两个java文件,分别是Flutter和FlutterFragment:
- Flutter:Android应用程序中使用Flutter的主要入口点
- FlutterFragment:Fragment来管理FlutterView
下面把这两个文件复制过来:

最后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的方法来实现。
四、总结
源码方式集成方案和产物集成方案优缺点对比:

五、参考资料
六、源码案例
更多推荐


所有评论(0)