开源|Magpie:混合开发工程化框架

开源项目专题系列

(八)

1.开源项目名称: magpie

2.github地址:

https://github.com/wuba/magpie_sdk

3.简介: magpie SDK是一个flutter plugin,提供了native与dart侧常用的一些通信能力和协议动态注册等常用功能;支持路由及页面生命周期管理等功能。项目于2020年4月份开源。

全文大纲如下:

  • 背景

  • 现状分析及优化实践

  • 工具链支持

  • Magpie混合页面交互设计

  • Magpiet通信设计

  • 总结及后续规划

背景

Flutter好处不言而喻,优点很多,通常我们在选择一个技术栈的时候通常会考虑跨平台性,性能,动态性,社区环境等因素,经过调研,Flutter满足以上所有点,但是在用到实际的业务场景中还需要我们经过一些工程化的搭建才能真正发挥出Flutter优点,提升开发、发布等环节效率 所以我们在19年下半年开启了Flutter工程化混合开发之旅。

今天主要分享一下我们工程化搭建中的独立编译的分析设计、及Magpie Plugin混合开发框架的介绍。

现状分析及优化实践

flutter原⽣的开发方式, 它是一个比较的⿊盒的过程, 拿flutter fun这个最常⽤的功能为例:

dart代码编译出产物,然后native工程集成编译产物, 编译打包出app包再安装到设备中,然后通过attach命令连接到设备上。整个过程做了非常多的事情,也实现了傻瓜式的一键操作,但是这个⽅式不⼀定适合我们的需求。

主要问题有以下几个:

1. 接入成本高:官方的整个集成方式具有非常强的侵入性,有很多的环境变量,脚本和文件都被放到了工程之中,开发者很难掌握所有的细节。同时拿IOS举例来说,这个过程是dart和iOS工程先后进行编译,把iOS和flutter的开发环境绑定在了一起,实际上,iOS只需要flutter的运行库即可在app中运行flutter app。

2. 环境一致性问题:由于本地开发者的环境千差万别,缺少一个统一的开发、调试、发布环境,比如安装的FlutterSDK版本可能不一样导致兼容问题;比如Android端的架构兼容问题;由此,我们需要统一开发者的基础环境,做到可控,方便接入。

3. 职责不明确问题:目前native侧开发与dart侧开发流程及代码上强耦合,比如dart同学在开发阶段怎么方便的与native端进行调试,而不需要两个工程进行类似软连接而带来的代码上的强耦合。

4. 易用性问题:目前存在的开源方案缺少可方便进行扩展、及动态注册解注册协议进行dart与native侧方法互调的支持以满足混合开发场景;缺少一个可视化的工具来方便的进行设备连接、打包、attach、发布等一套标准化流程的操作。

5. 开发效率问题:flutter fun这个过程是dart的代码先编译,然后native代码再编译,拖慢了了我们在开发、调试整个过程中的效率;而且目前flutter fun这个过程中,默认会⽣生成debug、profile、release三种产物包,编译速度很慢,实际上我们可能并不不需要所有模式的产物。

基于以上这些问题,我们把整个流程从中间拆开,变成dart和native两个部分,然后通过magpie workflow再把他们联结起来。通过magpie workflow 的GUI调试dart代码,然后通过magpie workflow编译dart代码产出编译产物,再通过workflow发布; native侧通过远程依赖或者本地依赖集成dart的编译产物,像平时一样编译调试和打包。对于我们所处的实际的开发场景来讲,这个⽅方式对开发⼈员的角色定位更加的清晰,整个开发过程也更更加效率。

下面我们先来说一说Android在整个workflow中所做的一些工作:

1. Android端编译流程优化

Android 来说,为了保持 native 视角开发接入简单易用,我们需要稍微改造下构建流程,让 flutter 环境在我们的 SDK 中进行依赖,这样 native 开发同学无需关心 flutter 环境问题,只需要简单集成 magpieSDK

1.1 构建产物

Flutter v1.12.13版本更新对构建产物进行了分离,将flutter.jar和libflutter.so由aar依赖改为远程maven依赖,大大减小了flutter aar的大小的同时,也提升了build aar的效率

buildType engine产物 可变产物
debug libflutter.so

isolate_snapshot_data

vm_snapshot_data

kernel_blob.bin

release libflutter.so libapp.so

1.2 问题分析

首先我们设想整个开发流程大概如下:

如果让Flutter和Native工程独立的方式去开发,那么Flutter module最后肯定是以aar的形式提供给Native工程依赖,这里需要考虑两个问题:

Native工程如何集成Flutter载体页?在Flutter 1.9.1版本,Flutter engine被打入Flutter module aar中,Native端想要集成Flutter engine产物需要对Flutter module aar进行产物拆分工作,详情可参考Android Flutter 基于1.9.1版本 构建分析及引擎拆分,但是在1.12.13版本中,Flutter将engine产物已经从Flutter module aar产物中剥离,改为用远程Maven仓形式提供给Native端,我们需要在给Native端提供的SDK中依赖这个Maven,即可实现Native工程集成载体页的工作。

Flutter module在debug下如何调 试? 我们都知道在Flutter支持hot reload,那么如果让Flutter module独立开发,该怎么进行hot reload? 答案是可以的,这里其实只要Native工程集成Flutter引擎相关的产物,然后在手机端安装 上带有Flutter载体页的应用程序,打开Flutter载体页使用Flutter官方提供的命令flutter attach天然就可以支持hot reload功能,所以Flutter module独立开发debug模式下的调试功能不是问题。

1.3 具体实现

  • 在magpie plugin的Android目录下的build.gradle中插入依赖flutter engine的任务即可实现在编译阶段进行引擎依赖:

apply from: "flutter_magpie.gradle"//集成flutter engine任务

  • 在flutter_magpie.gradle中加入Flutter engine远程Maven依赖配置:

private static final String MAVEN_REPO = “https://dl.bintray.com/hxingood123/flutter/” ;

  • 在gradle.properties下设置引擎版本号:

#引擎版本
engineVersion=2994f7e1e682039464cb25e31a78b86a3c59b695

  • 在flutter_magpie.gradle通过gradle脚本动态根据当前BuildType和Platform获取对应的Flutter engine:

void addFlutterDependencies(buildType) {
String flutterBuildMode = buildModeFor(buildType)
if (!supportsBuildMode(flutterBuildMode)) {
return
}
String repository = useLocalEngine()
? project.property(\'local-engine-repo\')
: MAVEN_REPO
project.rootProject.allprojects {
repositories {
maven {
url repository
}
}
}
// Add the embedding dependency.
addApiDependencies(project, buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
List platforms = getTargetPlatforms().collect()
// Debug mode includes x86 and x64, which are commonly used in emulators.
if (flutterBuildMode == "debug" && !useLocalEngine()) {
platforms.add("android-x86")
platforms.add("android-x64")
}
platforms.each { platform ->
String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
// Add the `libflutter.so` dependency.
addApiDependencies(project, buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
}
}

  • 最后将编译之后的aar及相关pom等文件上maven服务即可进行远程依赖,如果没有maven服务也可以直接进行aar本地依赖

2. Android架构兼容方案

Android端编译需要考虑架构兼容开发,目前flutter打包流程默认是不包含armabi架构,需要额外进行兼容处理。

将全架构引擎相关包上传maven,在flutter_magpie.gralde中进行动态依赖,开发这可以根据自己的项目进行选择行架构依赖,减少了本地依赖的繁琐步骤。

依赖关系如下:

对于Flutter业务侧的armabi架构兼容,我们通过在点击workflow界面中的构建按钮之后的构建流程中的packFlutterAppAotTask任务中插入相关脚本将armeabi-v7a架构下的业务产物*.so文件move到armeabi架构下,这样就解决了Flutter侧业务产物的架构兼容问题,关键代码如下:

from( “${compileTask.intermediateDir}/armeabi-v7a” ) {

include “*.so”

// Move `app.so` to `lib//libapp.so`

rename { String filename ->

return “lib/armeabi/lib${filename}”

}

}

3. iOS端编译流程优化

整个编译流程,就是把dart的代码编译为iOS工程中可用的产物,同时还附带了dart依赖的plugin的native代码、plugin的注册器、Cocoapods的配置文件podspec以及快速集成到iOS工程用的脚本。

iOS工程使用编译的产物就可以通过Cocoapods直接集成flutter app。

3.1 构建产物

编译的产物如下:

  • App.framework

    • isolate_snapshot_data 用于加速isolate启动,业务无关代码,Debug模式独有

    • vm_snapshot_data: 用于加速dart vm启动的产物,业务无关代码,Debug模式独有

    • kernel_blob.bin:业务代码产物,Debug模式独有

    • App 库文件

    • Info.plist 库配置文件

    • flutter_assets 资源和映射文件

  • GeneratedPluginRegistrant.h 插件注册h文件

  • GeneratedPluginRegistrant.m 插件注册m文件

  • FlutterBusiness.podspec 业务和插件注册Cocoapod配置

  • podhelper.rb Podfile生成脚本

  • Plugins 插件目录

    • Classes iOS源码

    • Assets 资源

    • A.podspec Cocoapod配置

    • PluginA 插件A

    • PluginA 插件B

3.2 问题分析

拆分flutterf官方的流程,很多步骤需要搞清楚原理,然后把每一个点还原出来,比如:app.framework在不同环境下的生成,plugin的处理,Cocoapods的配置等。

3.3 具体实现

生成App.framework

App.framework即dart源码编译后的成品。根据编译模式的不同,在文件的细节上有差异。

Release模式

  1. 生成App库文件,在release模式下dart源码会编译为库文件。

flutter build aot --target-platform=ios
  1. 复制flutter环境下AppFrameworkInfo.plist得到Info.plist

/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
  1. 生成flutter_assets目录,在release模式下此目录中仅有字体和图片等资源

flutter build bundle --target-platform=ios
  1. 复制以上产物至App.framework

Debugs模式

  1. 生成App库文件,Debug模式下只有基础的API,不包含业务代码

xcrun clang -x c arch_flags -dynamiclib -Xlinker -rpath -Xlinker \'@executable_path/Frameworks\' -Xlinker -rpath -Xlinker \'@loader_path/Frameworks\' -install_name \'@rpath/App.framework/App\' -o "${derived_dir}/App.framework/App"
  1. 复制flutter环境下AppFrameworkInfo.plist得到Info.plist

/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
  1. 生成flutter_assets目录,Debug模式下包含业务产物kernel_blob.bin

flutter build bundle --debug
  1. 复制以上产物至App.framework

3.4 配置plugin

在开发dart时,不可避免的会用到一些plugin,这些plugin除了dart的源码外,还会有对应的iOS和android端的代码和依赖,需要集成到native工程中才可以保证flutter app在native正常的运行。

通过flutter提供的命令行可以下载dart依赖的package和plugin。

flutter pub get

在flutter pub get后,会在dart工程中生成一个.flutter-plugins文件,通过读取该文件,得到每个plugin名字和本地路径。

battery=/Users/sac/flutter/.pub-cache/hosted/pub.dartlang.org/battery-0.3.1+7/
magpie=/Users/sac/magpie/
sqflite=/Users/sac/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.2.0/

通过路径可以找到每个plugin的目录,ios目录即plugin在ios端的部分。

通过读取对应路径的目录下的pubspec.yaml,得到plugin在不同平台下的Class名称。

flutter:
 plugin:
  platforms:
   android:
    package: io.flutter.plugins.battery
    pluginClass: BatteryPlugin
   ios:
    pluginClass: FLTBatteryPlugin

iOS目录中包括源码、资源、podspec文件,复制各个plugin路径中的iOS目录至同一个Plugins目录中汇总。

plugin在native app中需要运行注册方法才可以使用,所以需要动态的生成注册方法。我们现在已经有了每个plugin的名称和Class名称,通过mustache模板生成文件即可。

  • GeneratedPluginRegistrant.h 固定内容写入

  • GeneratedPluginRegistrant.m 使用mustache生成头文件导入和注册方法调用

#import "GeneratedPluginRegistrant.h"

{{#plugins}}
#if __has_include(<{{name}}/{{class}}.h>)
#import <{{name}}/{{class}}.h>
#else
@import {{name}};
#endif
{{/plugins}}

@implementation GeneratedPluginRegistrant

+ (void)registerWithRegistry:(NSObject*)registry {
{{#plugins}}
[{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]];
{{/plugins}}
}
@end

通过podspec配置了App.framework和plugin注册文件,需要在podspec中生成对各个plugin的依赖。通过mustache模板生成文件即可。

s.vendored_frameworks = \'App.framework\'
s.dependency \'Flutter\'
{{#plugins}}
s.dependency \'{{name}}\'
{{/plugins}}

Cocoapods

整个产物里有多个podspec,我们提供了一个嵌入podfile的脚本,类似flutter官方做法,根据传入的参数生成FlutterBusiness和各个plugin的Cocoapod配置。

工具链支持

在dart侧我们为界面化工具开发了比如:一键编译打包、attach、发布等常用开发流程中涉及到的功能。

workflow界面:

开发者可以很方便的通过以上这些界面化的操作进行进行编译、调试、发布的工作,大大减少了一些繁琐的命令执行过程,提高了易用性。

Magpie混合页面交互设计

对于混合页面跳转,我们看下面这张图:

FlutterView页面结构层级示意图:

显而易见,不处理的情况下的混合页面跳转:activity、FlutterView、Engine、isolate资源都是重新创建;那么这样会带来内存、资源重复、通信复杂等问题;Flutter官方也意识到了这个问题,在FlutterSDK1.12新版方已经支持Engine缓存。那么通常使用引擎缓存方案之后就变成如下图所示:

Magpiet通信设计

1. 基于 业界成熟的方案,我们在混合栈的处理上使用了FlutterBoost的开源方案,不把时间花在重复的事情上。

2. Flutter的的dart 侧与native侧的通信方式通过flutter plugin来实现。我们封装了通信的部分,在dart和native端都提供了可快速开发、⾼扩展性的功能实现接口。

3. 我们预先封装了一些基础通用能力,如果广播、日志、数据通信等通信功能

如何贡献&问题反馈

我们诚挚地希望开发者提出宝贵的意见和建议,您可以在https://github.com/wuba/magpie_sdk阅读magpie_sdk项目源码、了解使用方法,可以通过提交PR或者Issue来反馈建议和问题。

总结及后续规划

目前我们主要是在Magpie混合开发工程化方面做了一些基础的搭建,那么接下来我们会从以下几个方面继续完善、深耕、探索。

1. flutter对app包的增量过⼤一直是一个令人诟病的问题,所以包大小的优化是接下来我们的重点。

2. 相比RN或者H5,Flutter唯一不足的地方就是业务动态化,目前业内也有一些思路可以借鉴,那么接下来此方向也是我们一个重点突破的方向。

3. 在此过程中,我们也希望能集思⼴益,借助社区的力量,如果有好的方案欢迎和我们一起讨论。

作者介绍

黄鑫 / 58同城汽车事业群Android高级开发工程师,2015年加入58同城,目前主要负责58二手车移动端相关工作。

张达理 / 2014年加入58同城,目前主要负责58同城本地版研发工作。

参考文献

https://flutter.dev/docs/development/add-to-app

https://github.com/alibaba/flutter_boost

https://github.com/alibaba-flutter/flutter-boot

Live

58对外技术沙龙第五期

Flutter专场——Flutter在58的应用实践系列话题

系列2已准备就绪

本周日晚7:00准时开幕

扫码添加“58技术小秘书”微信 : jishu-58

添加小秘书微信后由小秘书拉您进项目交流群

阅读推荐

Go服务在容器内CPU使用率异常问题排查手记

开源|Magpie:平台工具链开发实践

开源|Magpie:58 跨平台技术应用及 Flutter 实践概览

开源|WPaxos:一致性算法Paxos的生产级高性能Java实现

开源|dl_inference:通用深度学习推理服务