有赞Flutter混编方案

2019 年 8 月 04 日

有赞Flutter混编方案

一、背景

目前准备试水 Flutter,但是多数 native 开发是不了解 Flutter,因此需要设计一种比较“舒服”的集成方式。

二、混编方案

2.1 方案考量

  • 如果直接采用 Flutter 工程结构来作为日常开发,那这部分 Native 开发也需要配置 Flutter 环境, 相当程度的了解 Flutter 一些技术,成本比较大;
  • 同时如果工程耦合,对于开发过程也是很难受的。

基于以上两点思考,针对 Android iOS 有如下方案:

2.2 Android

先看下官方的集成方式:

复制代码
# setting.gradle
setBinding(new Binding([gradle: this]))
evaluate(new File(
'../managementcenter/.android/include_flutter.groovy'
))
# build.gradle
dependencies {
implementation project(':flutter')
...........
}

这种方式使得工程强耦合,虽然便于开发调试,但是违背了第一点,大多数 native 同学都需要配置 Flutter 环境, 成本很大。

2.3 iOS

2.3.1 官方 iOS 混编方案简介

  • 在 native 项目 Podfile中通过 eval binding 特性注入 podhelper.rb脚本,在 pod install/update 时执行此脚本,脚本主要处理:
  • Pod 本地依赖 Flutter 引擎 (Flutter.framework) 与 Flutter 插件注册表 (FlutterPluginRegistrant)
  • Flutter 插件通过 flutter packagesget指令安装后生成的 .flutter-plugins文件解析,然后 Pod 本地依赖所有的插件
  • 在 pod install 执行完的钩子 post_install中,获取当前 pod target 工程对象,导入 Generated.xcconfig配置,其中都为环境变量的配置,主要为后续的 xcode_backend.sh脚本执行做准备
  • 在构建阶段 BuildPhases中注入构建是需要执行的 xcode_backend.sh脚本,脚本主要完成 Flutter 产物的构建并将其添加到对应的 native 工程中去,后续会进一步介绍此脚本

2.3.2 优点

  • 无缝开发,配置好后就可以只在 Flutter 工程内进行业务开发,无缝同步到 native 工程中
  • 不需要单独拆分组件,免去管理组件的版本及发布成本

2.3.3 缺点

  • 非常耦合,需要修改原有 native 工程配置,需要添加特定脚本去编译 Flutter
  • 需要修改原有 pod 的 xcconfig 配置
  • 所有团队开发成员都必须要配置 Flutter 开发环境才能编译成功

2.4 小结

基于以上思考,同时考虑到某个 Flutter 业务模块可能会引入到不同的 App 中,同时考虑到某个业务实现方式方面的解耦 (某个业务可能用 native, flutter, weex 开发),有以下方案(中间产物库每个 Flutter 业务模块都是独立的):

Android:

iOS:

三、Flutter 产物结构

3.1 Android

3.2 iOS

关于编译模式了解更多可参考查看 Flutter 的编译模式。

四、Flutter 产物收集

4.1 Android

在 Android 端集成 Flutter 较为简单,只需要获取到上文所讲的 Flutter 产物即 aar 文件。但是由于插件文件散落每次获取比较麻烦所以目前简单用脚本收集。

脚本收集主要是依靠项目里 .flutter_plugins 文件,该文件会记录 flutter 项目中引用的插件名以及本地路径等,因此可以通过该路径抓取插件的 aar 文件。

复制代码
from shutil import copyfile
import os
import requests
# 抓取文件类型
BuildRelease = True
aarType = "-release.aar" if BuildRelease else "-debug.aar"
pluginFilePath = '../.flutter-plugins'
# 当前项目的 flutter.aar
currentFlutterPath = '../.android/Flutter/build/outputs/aar/'
# 输出地址
outputFilePath = os.path.abspath('flutter_aar.py').replace("flutter_aar.py", "aars/")
endPath = 'android/build/outputs/aar/'
def collect_aar(plugins):
all_collection_success = True
if os.path.exists(outputFilePath):
print('copy aar to: ' + outputFilePath)
else:
print('target path: ' + outputFilePath + ' not exist')
os.makedirs(outputFilePath)
print('create target path: ' + outputFilePath)
for key, value in plugins.items():
aar_path = value + key + aarType
try:
copyfile(aar_path, outputFilePath + key + aarType)
print('copy flutter aar success at path: ' + aar_path)
except IOError:
all_collection_success = False
print('copy flutter aar error at path: ' + aar_path)
pass
file_object = open(pluginFilePath, 'r')
try:
plugin_map = {}
for line in file_object:
array = line.split('=')
plugin_map[array[0]] = array[1].replace('\n', '') + endPath
plugin_map['flutter'] = currentFlutterPath
collect_aar(plugin_map)
finally:
file_object.close()

目前该 python 脚本只抓取 Release 的 aar 文件,如果需要获取 debug 的可以手动修改:

复制代码
BuildRelease = False

执行抓取脚本 ./flutter_aar.sh

复制代码
#!/usr/bin/env bash
cd ..
cd .android
echo "start clean"
./gradlew clean
echo "start assembleRelease"
./gradlew assembleRelease
cd ..
cd android-build
echo "clean old aar file"
rm -rf aars
echo "start copy aar file"
# 只抓取 release
python flutter_aar.py
echo "copy aar file finish"

脚本执行完 Flutter 产物 aar 文件统一生成在根目录下 android-build 文件夹中。

4.2 iOS

通过查看 Flutter 编译脚本 xcode_backend.sh 和测试单独引入编译产物,发现其实 只要拥有 Flutter 的编译产物,宿主项目就可以接入 Flutter 的功能。

4.2.1 脚本简单分析

  • engine/Flutter.framework Flutter 核心库拷贝 -> Flutter.framework
复制代码
if [[ -e "${project_path}/.ios" ]]; then
RunCommand rm -rf -- "${derived_dir}/engine"
mkdir "${derived_dir}/engine"
RunCommand cp -r -- "${flutter_podspec}" "${derived_dir}/engine"
RunCommand cp -r -- "${flutter_framework}" "${derived_dir}/engine"
RunCommand find "${derived_dir}/engine/Flutter.framework" -type f -exec chmod a-w "{}" ;
else
RunCommand rm -rf -- "${derived_dir}/Flutter.framework"
RunCommand cp -r -- "${flutter_framework}" "${derived_dir}"
RunCommand find "${derived_dir}/Flutter.framework" -type f -exec chmod a-w "{}" ;
fi
  • debug 模式下 Dart 业务代码编译 (JIT) -> App.framework
复制代码
RunCommand eval "$(echo "static const int Moo = 88;" | 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" -)"
  • 非 debug 模式下 Dart 业务代码编译 (AOT) -> App.framework
复制代码
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics
${verbose_flag}
build aot
--output-dir="${build_dir}/aot"
--target-platform=ios
--target="${target_path}"
--${build_mode}
--ios-arch="${archs}"
${local_engine_flag}
${track_widget_creation_flag}
  • 资源文件等打包 -> flutter_assets
复制代码
StreamOutput " ├─Assembling Flutter resources..."
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics
${verbose_flag}
build bundle
--target-platform=ios
--target="${target_path}"
--${build_mode}
--depfile="${build_dir}/snapshot_blob.bin.d"
--asset-dir="${derived_dir}/App.framework/flutter_assets"
${precompilation_flag}
${local_engine_flag}
${track_widget_creation_flag}

4.2.2 方案分析

设计

  • 插件统一编译成.a 库,添加对应头文件
  • App.framework 及 engine/Flutter.framework 添加
  • 目前初期 demo 将上述生成的产物统一放入到私有库当中,然后 native 宿主工程 pod 依赖此库,只需要在使用 Flutter 代码的地方 import 对应的头文件即可正常使用
脚本编写
复制代码
echo "==b 清理 flutter 历史编译 ==="
flutter clean
echo "=== 重新生成 plugin 索引 ==="
flutter packages get
echo "=== 生成 App.framework 和 flutter_assets==="
flutter build ios --debug
echo "=== 获取所有 plugin 并找到头文件 ==="
while read -r line
do
if [[ ! "$line" =~ ^// ]]; then
array=(${line//=/ })
plugin_name=${array[0]}
cd .ios/Pods
echo " 生成 lib${plugin_name}.a..."
/usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' -target ${plugin_name} BUILD_DIR=../../build/ios -sdk iphoneos -quiet
/usr/bin/env xcrun xcodebuild build -configuration Debug ARCHS='x86_64' -target ${plugin_name} BUILD_DIR=../../build/ios -sdk iphonesimulator -quiet
echo " 合并 lib${plugin_name}.a..."
lipo -create "../../build/ios/Debug-iphonesimulator/${plugin_name}/lib${plugin_name}.a" "../../build/ios/Release-iphoneos/${plugin_name}/lib${plugin_name}.a" -o "../../product/lib${plugin_name}.a"
echo " 复制头文件 "
classes=${array[1]}ios/Classes
for header in `find "$classes" -name *.h`; do
cp -f $header "../../product/"
done
else
echo " 读取文件出错 "
fi
done < .flutter-plugins
echo "=== 生成注册入口的二进制库文件 ==="
for reg_enter_name in "FlutterPluginRegistrant"
do
echo " 生成 libFlutterPluginRegistrant.a..."
/usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' -target FlutterPluginRegistrant BUILD_DIR=../../build/ios -sdk iphoneos
/usr/bin/env xcrun xcodebuild build -configuration Debug ARCHS='x86_64' -target FlutterPluginRegistrant BUILD_DIR=../../build/ios -sdk iphonesimulator
echo " 合并 libFlutterPluginRegistrant.a..."
lipo -create "../../build/ios/Debug-iphonesimulator/FlutterPluginRegistrant/lib$FlutterPluginRegistrant.a" "../../build/ios/Release-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a" -o "../../product/libFlutterPluginRegistrant.a"
echo " 复制头文件 "
classes="../Flutter/FlutterPluginRegistrant/Classes"
for header in `find "$classes" -name *.h`; do
cp -f $header "../../product/"
done
done
  • 后续规划
  • 脚本优化,添加自动 pod 库检测及上传
  • App.framework/Flutter.framework 体积太大,放到 git 仓库不太友好,考虑后续上传到 CDN,然后在 pod 安装的时候预先执行脚本把两个产物拉下来

五、Flutter 产物上传

5.1 Android

上面产物搜集完成后,需要上传 maven 仓库,方便集成以及版本控制:

复制代码
apply plugin: 'youzan.maven.upload'
zanMavenUpload {
version = '0.0.2'
childGroup = "flutter-apub"
}
uploadItems {
"fluttertoast" {
targetFile = file('../../android-build/aars/fluttertoast-release.aar')
}
"image_picker" {
targetFile = file('../../android-build/aars/image_picker-release.aar')
}
............
}

因此引用链如下:

  • Android

  • iOS

六、总结

以上比较全面的描述了有赞的 Flutter 混编方案,目前有赞已经在内部使用的 App 上使用 Flutter 开发了一些页面作为试点。后续会考虑在线上 App 试点,目前正在进行 Flutter 基础库的搭建,之后会专门有文章分享。

本文转载自公众号有赞 coder(ID:youzan_coder)

原文链接

https://mp.weixin.qq.com/s?__biz=MzAxOTY5MDMxNA==&mid=2455759923&idx=1&sn=030ef038363723928b74176ec67fb2d1&chksm=8c686a16bb1fe30094cd3e79f56f70dcb0a9bcc146822bca67acf9b14687ffaef6921b64d587&scene=27#wechat_redirect

2019 年 8 月 04 日 08:00 15653

评论

发布
暂无评论
发现更多内容

负载均衡方式

羽球

负载均衡

程序的机器级表示-程序的编码

引花眠

计算机基础

时间去哪了?

escray

推荐系统大规模特征工程与FEDB的Spark基于LLVM优化

范式AI云

spark Sparksql 推荐系统 LLVM FEDB

我在项目中是这样配置Vue的

前端有的玩

JavaScript Vue 前端 框架设计

低代码与无代码

lidaobing

低代码 无代码开发

Cache解决算法 Charles断点调试breakpoint John 易筋 ARTS 打卡 Week 08

John(易筋)

ARTS 打卡计划

数据驱动 vs 关键字驱动:对UI自动化测试框架搭建的探索

Winfield

DevOps 敏捷 自动化测试

Mysql插入百万条数据

Java小咖秀

MySQL 运维 数据

第6周-作业1

seng man

ARTS打卡-06

Geek_yansheng25

设计模式(1)—什么是设计模式?设计模式的六大原则是什么?

爱嘤嘤嘤斯坦

Java 程序员 编程语言 设计模式 23种设计模式

Spring5-Reactor函数式编程

小技术君

spring reactor Spring5 springboot

《重学 Java 设计模式》PDF 出炉了 - 小傅哥,肝了50天写出18万字271页的实战编程资料

小傅哥

Java 设计模式 小傅哥 重构 代码质量

第6周-作业2-总结

seng man

简述CAP理论

lei Shi

自动化测试首先是一种工作文化

wangwei1237

自动化测试 测试文化

redis系列之——高可用(主从、哨兵、集群)

诸葛小猿

redis redis集群 redis哨兵 redis主从

Go:Stringer命令,通过代码生成提高效率

陈思敏捷

go golang stringer

智慧4S店解决方案发布,看英特尔如何引领汽车销售行业变革

飞天鱼2017

看动画学算法之:排序-插入排序

程序那些事

Java 数据结构 算法 插入排序

计算机网络基础(一)---计算机网络概览篇

书旅

php laravel 计算机网络

分布式系统设计理念这么难学?

架构师修行之路

架构 分布式

SpringBoot 入门:03 - 统一请求返回

阿亮

Java spring springboot

人人都需要一份自己的「使用说明书」

非著名程序员

程序员 程序人生 提升认知 独立思考 自我思考

CAP原理简述

刘志刚

设计模式六大原则

刘志刚

设计原则

ARTS打卡 - Week 07

teoking

抽象工厂模式

Leetao

Python 面试 设计模式

ARTS打卡 第7周

引花眠

ARTS 打卡计划

MySQL实战45讲总结

`

MySQL

有赞Flutter混编方案-InfoQ