我們必須全力以赴,同時又不抱持任何希望。不管做什麼事,都要當它是全世界最重要的一件事,但同時又知道這件事根本無關緊要。
前言
分析了一堆的內容,其實主要就是爲了做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的庫不甚瞭解,可以看看這篇文章瞭解下。