前言
分析了一堆的内容,其实主要就是为了做Flutter的CI的事情。让我们在本文中探讨如何制作Flutter CI for iOS。
踩坑的本机环境
注意对比下我的环境和你的环境是否一样,有些问题在Flutter的新版本中已经被修复了。
➜ app git:(master) ✗ flutter --version
Flutter 1.9.1+hotfix.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision cc949a8e8b (3 weeks ago) • 2019-09-27 15:04:59 -0700
Engine • revision b863200c37
Tools • Dart 2.5.0
铺垫
本文基于我的前几篇文章的分析所得,如果你不清楚Flutter的编译产物和编译流程,建议阅读我的前几篇文章:
文章 | 说明 |
---|---|
Flutter build ios产物分析 | 介绍Flutter的编译产物以及官方接入方案的产物的编译、组织流程。 |
Flutter xcode_backend分析 | 承接上文,上文中使用了xcode_backend.sh脚本生成编译产物,本文基于上文做深层次的Flutter混合编译分析和介绍。 |
初始环境
我配置好了一个临时的分析工程(Native接入Flutter的工程目录结构)。
➜ DevelopProjects tree -L 2 temp
temp
├── Android # Android工程
├── flutter_module # Flutter工程
│ ├── README.md
│ ├── build
│ ├── flutterApp.podspec
│ ├── flutter_01.log
│ ├── flutter_module.iml
│ ├── flutter_module_android.iml
│ ├── flutter_podhelper.rb
│ ├── lib
│ ├── pubspec.lock
│ ├── pubspec.yaml
│ └── test
├── iOS # iOS工程
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ ├── iOS
│ ├── iOS.xcodeproj
│ └── iOS.xcworkspace
└── startBuild.sh # iOS产物CI编译脚本
10 directories, 11 files
制作过程
先看一眼编译后的东西:
➜ app ✗ tree build -L 3 # 省略了部分内容
build # 编译目录
├── aot
│ ├── App.framework # Flutter业务层代码
│ ├── App.framework.dSYM.noindex
│ ├── app.dill
│ ├── arm64
│ ├── armv7
│ ├── frontend_server.d
│ └── kernel_compile.d
├── dSYMs.noindex
│ └── App.framework.dSYM
├── ios
│ ├── Debug-iphonesimulator # x86_64架构,Debug
│ ├── Release # 合并Debug-iphonesimulator和Release-iphoneos的产物
│ ├── Release
│ │ ├── libFlutterPluginRegistrant.a
│ │ ├── xxx.a
│ │ └── xxx.a
│ └── Release-iphoneos # arm64 armv7,Release
└── libFlutterPlugins.a # 所有的插件及FlutterPluginRegistrant
11 directories, 4 files
启动流程
本脚本可以应用于Jenkints,也可以做成其他的自动化的部分。
根据Flutter build ios产物分析、Flutter xcode_backend分析的分析结果,可以简单定下以下的启动流程,分别为如下流程:
- 获取输入的变量
- 初始化环境变量
- 编译App.framework(Flutter业务层代码的framework,包含了静态资源)
- 生成dSYM(iOS编译后的符号表,用户还原符号)
- Strip dSYM(剥离符号表)
- 编译Flutter资源文件
- 编译Flutter插件(将Flutter的插件连同注册制都编译为一个framework,对外仅仅提供注册制的header)
- unset环境变量
# 主要流程
main() {
# Flutter工程目录
cd ./flutter_module
InitVariable # 获取输入的变量
InitEnv # 初始化环境变量
BuildAppFramework # 编译App.framework
GeneratedSYM # 生成dSYM
StripdSYM # Strip dSYM
BuildFlutterAssets # 编译Flutter资源
BuildFlutterPlugin # 编译Flutter插件
UnsetVariable # unset环境变量
# 退出到原来目录
cd -
}
# 启动脚本
main
根据上述脚本,逐渐填充函数实现即可。
初始化变量
- 如果使用Jenkints的话,可以将下述参数配置:
# 用于命令行的输出
EchoDone() {
echo ""
echo " └─$1"
echo ""
}
# 初始化从外部输入的变量
InitVariable() {
echo " ├──Input variable..."
export FLT_PROJ_BUILD_MODE='release' # profile/release
Echo " ├────FLT_PROJ_BUILD_MODE:${FLT_PROJ_BUILD_MODE}"
export FLT_ARCH='armv7+arm64'
echo " ├────FLT_ARCH:${FLT_ARCH}"
}
##初始化编译环境
InitEnv() {
# 设置Flutter Pub地址
export PUB_HOSTED_URL='https://pub.flutter-io.cn'
echo " ├────PUB_HOSTED_URL=${PUB_HOSTED_URL}"
echo ""
echo " ├────Flutter version:"
echo ""
flutter --version
echo ""
echo " ├────Clean flutter building artifacts"
echo ""
flutter clean
EchoDone "Clean flutter building artifacts done"
echo " ├────Fetch flutter project dependences"
# 更新依赖
flutter pub get
flutter packages get
EchoDone "Fetch flutter project dependences done"
# 工程根目录
export FLT_PROJ="$(pwd)"
# 编译输出目录
export FLT_PROJ_BUILD="${FLT_PROJ}/build"
# Flutter编译的ios工程或者.ios的地址
export FLT_PROJ_iOS="${FLT_PROJ}/ios"
# 如果.ios存在则是Module工程
if [[ -e "${FLT_PROJ}/.ios" ]]; then
unset FLT_PROJ_iOS
export FLT_PROJ_iOS="${FLT_PROJ}/.ios"
fi
# 输出目录
export FLT_PROJ_iOS_FLT="${FLT_PROJ_iOS}/Flutter"
# 输出环境变量,([]:中括号表示可选)
echo " ├────./ -> ${FLT_PROJ}"
echo " ├────./build -> ${FLT_PROJ_BUILD}"
echo " ├────./[.]ios -> ${FLT_PROJ_iOS}"
echo " ├────./[.]ios/Flutter -> ${FLT_PROJ_iOS_FLT}"
# 需要创建文件夹用于存放生成的一些文件
# 不然部分文件因为目录不存在而创建失败
mkdir -p "${FLT_PROJ_iOS_FLT}"
# 清空缓存
rm -rf "${FLT_PROJ_iOS_FLT}/App.framework"
}
编译业务代码
编译Flutter业务层的代码:
BuildAppFramework() {
echo " ├──Building App.framework..."
echo ""
flutter --suppress-analytics build aot --${FLT_PROJ_BUILD_MODE} \
--target-platform=ios \
--target="lib/main.dart" \
--ios-arch="${FLT_ARCH/+/,}" \
--output-dir="${FLT_PROJ_BUILD}/aot"
export FLT_PROJ_BUILD_AOT_APP="${FLT_PROJ_BUILD}/aot/App.framework"
cp -r "${FLT_PROJ_BUILD_AOT_APP}" "${FLT_PROJ_iOS_FLT}"
echo " ├────FLT_PROJ_BUILD_AOT_APP:${FLT_PROJ_BUILD_AOT_APP}"
EchoDone "Building App.framework done"
}
生成dSYM
GeneratedSYM() {
echo " ├─Generating dSYM file..."
echo ""
mkdir -p -- "${FLT_PROJ_BUILD}/dSYMs.noindex"
xcrun dsymutil -o "${FLT_PROJ_BUILD}/dSYMs.noindex/App.framework.dSYM" "${FLT_PROJ_BUILD_AOT_APP}/App"
if [[ $? -ne 0 ]]; then
echo "Failed to generate debug symbols (dSYM) file for ${FLT_PROJ_BUILD_AOT_APP}/App."
exit -1
fi
EchoDone "Generating dSYM file done"
}
剥离dSYM并嵌入Info.plist
StripdSYM() {
echo " ├─Stripping debug symbols..."
echo ""
xcrun strip -x -S "${FLT_PROJ_iOS_FLT}/App.framework/App"
if [[ $? -ne 0 ]]; then
echo "Failed to strip ${FLT_PROJ_iOS_FLT}/App.framework/App."
exit -1
fi
cp "${FLT_PROJ_iOS_FLT}/AppFrameworkInfo.plist" ${FLT_PROJ_iOS_FLT}/App.framework/Info.plist
EchoDone "Stripping debug symbols done"
}
编译Flutter assets
BuildFlutterAssets() {
echo " ├─Building flutter_assets..."
echo " ├─Assembling Flutter resources..."
echo ""
flutter --suppress-analytics build bundle --${FLT_PROJ_BUILD_MODE} \
--target-platform=ios \
--target="lib/main.dart" \
--depfile="${FLT_PROJ_BUILD}/snapshot_blob.bin.d" \
--asset-dir="${FLT_PROJ_iOS_FLT}/App.framework/flutter_assets" \
--precompiled
EchoDone "Assembling Flutter resources done"
}
--precompiled
只有Release版本才会有这个参数。
插件静态库的构建
插件静态库的构建需要用到一个flutter pub get;flutter packages get;flutter build ios
的一个产物.flutter-plugins
。如果对于.flutter-plugins
不理解的,可以查看Flutter build ios产物分析的分析。
插件静态库的编译需要编译两份,一份是
arm64 armv7
(真机),一份是x86_64
(模拟器)
- arm64 armv7:真机Release
/usr/bin/env xcrun xcodebuild BUILD_DIR \
-configuration Release ARCHS='arm64 armv7' \
-target ${plugin_name} BUILD_DIR=../../build/ios \
-sdk iphoneos \
-quiet
- x86_64:用于虚拟机调试用
/usr/bin/env xcrun xcodebuild build \
-configuration Debug ARCHS='x86_64' \
-target ${plugin_name} BUILD_DIR=../../build/ios \
-sdk iphonesimulator -quiet
- 合并产物
lipo -create "../../build/ios/Debug-iphonesimulator/${plugin_name}/lib${plugin_name}.a" "../../build/ios/Release-iphoneos/${plugin_name}/lib${plugin_name}.a" -o "${plugin_lib_path}"
生成插件库
将源码编译为静态库,输出在./build/ios/Debug-iphonesimulator
和./build/ios/Release-iphoneos
。
# 生成静态库
GenerateStaticFramewrok() {
local pluginName=$1
/usr/bin/env xcrun xcodebuild build \
-configuration Release ARCHS='arm64 armv7' \
-target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
-sdk iphoneos \
-quiet >/dev/null
/usr/bin/env xcrun xcodebuild build \
-configuration Debug ARCHS='x86_64' \
-target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
-sdk iphonesimulator \
-quiet >/dev/null
}
合并插件库
# 合并静态库
MergeStaticFramework() {
local pluginName=$1
local pluginLibPath=$2
local debugFramework="${FLT_PROJ_BUILD}/ios/Debug-iphonesimulator/${pluginName}/lib${pluginName}.a"
local releaseFramework="${FLT_PROJ_BUILD}/ios/Release-iphoneos/${pluginName}/lib${pluginName}.a"
lipo -create "${debugFramework}" "${releaseFramework}" -o "${pluginLibPath}"
}
编译静态库并合并
# 编译静态库
BuildStaticFramework() {
local pluginName=$1
local pluginLibPath=$2
echo " ├───Generating lib${plugin_name}.a"
GenerateStaticFramewrok "${plugin_name}"
echo " ├───Merging lib${plugin_name}.a"
echo " ├───Plugin Lib Path: ${pluginLibPath}"
MergeStaticFramework "${plugin_name}" "${pluginLibPath}"
echo " └───Merging lib${plugin_name}.a done"
echo ""
}
合并所有插件和注册制
这里将Flutter插件都编译成了静态库,并将所有的静态库合并为一个,方便业务方接入。
- 注册制指的是:
FlutterPluginRegistrant
,Flutter用于主动注册插件用的库。
# 编译Flutter的插件和FlutterPluginRegistrant
BuildFlutterPlugin() {
echo " ├─Building Flutter Plugin..."
echo ""
cd ${FLT_PROJ_iOS}
# 更新插件依赖,可能会遇到依赖冲突的问题╮(╯▽╰)╭,这个就需要自己解决了
pod install --no-repo-update
cd -
# 进入Flutter/Pods编译插件
cd "${FLT_PROJ_iOS}/Pods"
if [ -f "${FLT_PROJ}/.flutter-plugins" ]; then
# 声明数组保存Plugin的路径
declare -a plugin_lib_path_arr
# 获取所有的插件名称
plugin_arr="$(cat "${FLT_PROJ}/.flutter-plugins" | awk -F "=" '{print $1}') FlutterPluginRegistrant"
echo ""
# ./build/ios/Release
local pluginOutput="${FLT_PROJ_BUILD}/ios/Release"
mkdir -p ${pluginOutput}
for plugin_name in ${plugin_arr}; do
if [ "${plugin_name}" = "FlutterPluginRegistrant" ]; then
echo " └─Building Flutter Plugin done"
echo ""
echo " ├─Building FlutterPluginRegistrant..."
fi
local pluginLibPath="${pluginOutput}/lib${plugin_name}.a"
BuildStaticFramework "${plugin_name}" "${pluginLibPath}"
plugin_lib_path_arr=("${plugin_lib_path_arr[@]}" "${pluginLibPath}")
done
fi
EchoDone "Building FlutterPluginRegistrant done"
echo " ├─Merging Flutter Plugin and FlutterPluginRegistrant..."
# 生成在build目录下
libtool -static -o "${FLT_PROJ_BUILD}/libFlutterPlugins.a" "${plugin_lib_path_arr[@]}" >/dev/null
EchoDone "Merging Flutter Plugin and FlutterPluginRegistrant done"
cd -
}
libtool -static -o 合并后的文件路径 aaa.a bbb.a ccc.a...
- 将
合并后的文件路径
后的所有*.a
合并为一个,且链接的类型-static
为静态库。 - 如果不希望不输出
has no symbols
的警告,可以加上-no_warning_for_no_symbols
。
- 将
完结
- CI脚本制作完毕!_
完整脚本如下:
EchoDone() {
echo ""
echo " └─$1"
echo ""
}
InitVariable() {
echo " ├──Input variable..."
export FLT_PROJ_BUILD_MODE='release' # profile/release
Echo " ├────FLT_PROJ_BUILD_MODE:${FLT_PROJ_BUILD_MODE}"
export FLT_ARCH='armv7+arm64'
echo " ├────FLT_ARCH:${FLT_ARCH}"
}
InitEnv() {
# 设置Flutter Pub地址
export PUB_HOSTED_URL='https://pub.flutter-io.cn'
echo " ├────PUB_HOSTED_URL=${PUB_HOSTED_URL}"
echo ""
echo " ├────Flutter version:"
echo ""
flutter --version
echo ""
echo " ├────Clean flutter building artifacts"
echo ""
flutter clean
EchoDone "Clean flutter building artifacts done"
echo " ├────Fetch flutter project dependences"
echo ""
# 更新依赖
flutter pub get
flutter packages get
EchoDone "Fetch flutter project dependences done"
# 如果.ios存在则是Module工程
export FLT_PROJ="$(pwd)"
# 编译输出目录
export FLT_PROJ_BUILD="${FLT_PROJ}/build"
# Flutter编译的ios工程或者.ios的地址
export FLT_PROJ_iOS="${FLT_PROJ}/ios"
# 如果.ios存在则是Module工程
if [[ -e "${FLT_PROJ}/.ios" ]]; then
unset FLT_PROJ_iOS
export FLT_PROJ_iOS="${FLT_PROJ}/.ios"
fi
# 输出目录
export FLT_PROJ_iOS_FLT="${FLT_PROJ_iOS}/Flutter"
echo " ├────./ -> ${FLT_PROJ}"
echo " ├────./build -> ${FLT_PROJ_BUILD}"
echo " ├────./[.]ios -> ${FLT_PROJ_iOS}"
echo " ├────./[.]ios/Flutter -> ${FLT_PROJ_iOS_FLT}"
# 需要创建文件夹用于存放生成的一些文件
# 不然部分文件因为目录不存在而创建失败
mkdir -p "${FLT_PROJ_iOS_FLT}"
# 清空缓存
rm -rf "${FLT_PROJ_iOS_FLT}/App.framework"
}
BuildAppFramework() {
echo " ├──Building App.framework..."
echo ""
flutter --suppress-analytics build aot --${FLT_PROJ_BUILD_MODE} \
--target-platform=ios \
--target="lib/main.dart" \
--ios-arch="${FLT_ARCH/+/,}" \
--output-dir="${FLT_PROJ_BUILD}/aot"
export FLT_PROJ_BUILD_AOT_APP="${FLT_PROJ_BUILD}/aot/App.framework"
cp -r "${FLT_PROJ_BUILD_AOT_APP}" "${FLT_PROJ_iOS_FLT}"
echo " ├────FLT_PROJ_BUILD_AOT_APP:${FLT_PROJ_BUILD_AOT_APP}"
EchoDone "Building App.framework done"
}
GeneratedSYM() {
echo " ├─Generating dSYM file..."
echo ""
mkdir -p -- "${FLT_PROJ_BUILD}/dSYMs.noindex"
xcrun dsymutil -o "${FLT_PROJ_BUILD}/dSYMs.noindex/App.framework.dSYM" "${FLT_PROJ_BUILD_AOT_APP}/App"
if [[ $? -ne 0 ]]; then
echo "Failed to generate debug symbols (dSYM) file for ${FLT_PROJ_BUILD_AOT_APP}/App."
exit -1
fi
EchoDone "Generating dSYM file done"
}
StripdSYM() {
echo " ├─Stripping debug symbols..."
echo ""
xcrun strip -x -S "${FLT_PROJ_iOS_FLT}/App.framework/App"
if [[ $? -ne 0 ]]; then
echo "Failed to strip ${FLT_PROJ_iOS_FLT}/App.framework/App."
exit -1
fi
cp "${FLT_PROJ_iOS_FLT}/AppFrameworkInfo.plist" ${FLT_PROJ_iOS_FLT}/App.framework/Info.plist
EchoDone "Stripping debug symbols done"
}
BuildFlutterAssets() {
echo " ├─Building flutter_assets..."
echo " ├─Assembling Flutter resources..."
echo ""
flutter --suppress-analytics build bundle --${FLT_PROJ_BUILD_MODE} \
--target-platform=ios \
--target="lib/main.dart" \
--depfile="${FLT_PROJ_BUILD}/snapshot_blob.bin.d" \
--asset-dir="${FLT_PROJ_iOS_FLT}/App.framework/flutter_assets" \
--precompiled
EchoDone "Assembling Flutter resources done"
}
# 生成静态库
GenerateStaticFramewrok() {
local pluginName=$1
/usr/bin/env xcrun xcodebuild build \
-configuration Release ARCHS='arm64 armv7' \
-target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
-sdk iphoneos \
-quiet >/dev/null
/usr/bin/env xcrun xcodebuild build \
-configuration Debug ARCHS='x86_64' \
-target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
-sdk iphonesimulator \
-quiet >/dev/null
}
# 合并静态库
MergeStaticFramework() {
local pluginName=$1
local pluginLibPath=$2
local debugFramework="${FLT_PROJ_BUILD}/ios/Debug-iphonesimulator/${pluginName}/lib${pluginName}.a"
local releaseFramework="${FLT_PROJ_BUILD}/ios/Release-iphoneos/${pluginName}/lib${pluginName}.a"
lipo -create "${debugFramework}" "${releaseFramework}" -o "${pluginLibPath}"
}
# 编译静态库
BuildStaticFramework() {
local pluginName=$1
local pluginLibPath=$2
echo " ├───Generating lib${plugin_name}.a"
GenerateStaticFramewrok "${plugin_name}"
echo " ├───Merging lib${plugin_name}.a"
echo " ├───Plugin Lib Path: ${pluginLibPath}"
MergeStaticFramework "${plugin_name}" "${pluginLibPath}"
echo " └───Merging lib${plugin_name}.a done"
echo ""
}
# 编译Flutter的插件和FlutterPluginRegistrant
BuildFlutterPlugin() {
echo " ├─Building Flutter Plugin..."
echo ""
cd ${FLT_PROJ_iOS}
# 更新插件依赖
pod install
cd -
# 进入Flutter/Pods编译插件
cd "${FLT_PROJ_iOS}/Pods"
if [ -f "${FLT_PROJ}/.flutter-plugins" ]; then
# 声明数组保存Plugin的路径
declare -a plugin_lib_path_arr
# 获取所有的插件名称
plugin_arr="$(cat "${FLT_PROJ}/.flutter-plugins" | awk -F "=" '{print $1}') FlutterPluginRegistrant"
echo ""
# ./build/ios/Release
local pluginOutput="${FLT_PROJ_BUILD}/ios/Release"
mkdir -p ${pluginOutput}
for plugin_name in ${plugin_arr}; do
if [ "${plugin_name}" = "FlutterPluginRegistrant" ]; then
echo " └─Building Flutter Plugin done"
echo ""
echo " ├─Building FlutterPluginRegistrant..."
fi
local pluginLibPath="${pluginOutput}/lib${plugin_name}.a"
BuildStaticFramework "${plugin_name}" "${pluginLibPath}"
plugin_lib_path_arr=("${plugin_lib_path_arr[@]}" "${pluginLibPath}")
done
fi
EchoDone "Building FlutterPluginRegistrant done"
echo " ├─Merging Flutter Plugin and FlutterPluginRegistrant..."
# 生成在build目录下
libtool -static -o "${FLT_PROJ_BUILD}/libFlutterPlugins.a" "${plugin_lib_path_arr[@]}" >/dev/null
EchoDone "Merging Flutter Plugin and FlutterPluginRegistrant done"
cd -
}
# 重置环境变量
UnsetVariable() {
echo "Project ${FLT_PROJ} built and packaged successfully."
unset FLT_ARCH # 编译的架构
unset FLT_PROJ_BUILD_MODE # 编译的模式:release/profile
unset PUB_HOSTED_URL # flutter pub域名
unset FLT_PROJ # Flutter工程./根目录
unset FLT_PROJ_iOS # Flutter工程./iOS目录
unset FLT_PROJ_iOS_FLT # Flutter工程./iOS/Flutter目录
unset FLT_PROJ_BUILD # Flutter工程./build目录
unset FLT_PROJ_BUILD_AOT_APP # Flutter工程./build/aot/App.framework目录
}
# 主要流程
main() {
# Flutter工程目录
cd ./flutter_module
InitVariable # 获取输入的变量
InitEnv # 初始化环境变量
BuildAppFramework # 编译App.framework
GeneratedSYM # 生成dSYM
StripdSYM # Strip dSYM
BuildFlutterAssets # 编译Flutter资源
BuildFlutterPlugin # 编译Flutter插件
UnsetVariable # unset环境变量
# 退出到原来目录
cd -
}
# 启动脚本
main
附录
- 闲鱼flutter混合工程持续集成的最佳实践-源于知乎博客
- 闲鱼flutter混合工程持续集成最佳实践-源于语雀
- Flutter iOS 混合工程自动化:介绍了iOS混合工程的方案,方案基本和闲鱼的一致。
- flutter混合工程持续集成的最佳实践:介绍了Flutter混合工程的方式,和闲鱼的方案基本一致。
iOS
- iOS 开发中的『库』(一)
- 如果对于iOS的库不甚了解,可以看看这篇文章了解下。