开源|Magpie:混合开发工程化框架
开源项目专题系列(八)
1.开源项目名称:magpie2.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命令连接到设备上。整个过程做了非常多的事情,也实现了傻瓜式的一键操作,但是这个⽅式不⼀定适合我们的需求。
主要问题有以下几个:
基于以上这些问题,我们把整个流程从中间拆开,变成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工程依赖,这里需要考虑两个问题:
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<String> 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/<abi>/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模式
生成App库文件,在release模式下dart源码会编译为库文件。
flutter build aot --target-platform=ios
复制flutter环境下AppFrameworkInfo.plist得到Info.plist
/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
生成flutter_assets目录,在release模式下此目录中仅有字体和图片等资源
flutter build bundle --target-platform=ios
复制以上产物至App.framework
Debugs模式
生成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"
复制flutter环境下AppFrameworkInfo.plist得到Info.plist
/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
生成flutter_assets目录,Debug模式下包含业务产物kernel_blob.bin
flutter build bundle --debug
复制以上产物至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<FlutterPluginRegistry>*)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页面结构层级示意图:
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
Flutter专场——Flutter在58的应用实践系列话题
系列2已准备就绪
添加小秘书微信后由小秘书拉您进项目交流群
END
开源|Magpie:58 跨平台技术应用及 Flutter 实践概览
开源|WPaxos:一致性算法Paxos的生产级高性能Java实现