https://blog.csdn.net/suwu150/article/details/82682593
前言
xcode_backend.sh
位于$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh
,是Flutter编译iOS产物的一个关键部分,本篇文章用于分析该脚本。
为何要分析?
当我们创建完毕Flutter Module,并且通过官方的方式引入了Flutter框架后,我们会在Target->Build Phases->Run Script
中可以看到这么两句话:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
从字面上看,第一步是build
编译,第二个是embed
嵌入Framework到宿主APP。
- Embed 的意思是嵌入,但是这个嵌入并不是嵌入 app 可执行文件,而是嵌入 app 的 bundle 文件。
因此,如果我们要了解Flutter混合编译的来龙去脉,就需要分析下xcode_backend到底做了什么东西。
主函数入口
# 主函数入口
if [[ $# == 0 ]]; then # 如果不带参数则直接执行BuildApp函数
# Backwards-compatibility: if no args are provided, build.
BuildApp
else # 否则执行case语句
case $1 in
"build")
BuildApp ;; # 编译
"thin")
ThinAppFrameworks ;; # 只合并需要的架构
"embed")
EmbedFlutterFrameworks ;; # Embed
esac
fi
因此,Xcode_backend包含了三部分的实现,即build、thin、embed。
踩坑的本机环境
注意对比下我的环境和你的环境是否一样,有些问题在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
文章总结
xcode_backend.sh
的主要作用:
- iOS工程直接依赖Flutter工程,每次编译的时候都会执行
Target->Build Phases->Run Script
的xcode_backend.sh脚本。
在以下指令中:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
- xcode_backend.sh做的事情:
- 导入Flutter引擎的对应模式版本(
BuildApp
所做的事情) - 编译Dart代码为App.framework(
BuildApp
所做的事情) - 编译flutter_assets,并内嵌到App.framework(
BuildApp
所做的事情) - 复制资源,并签名(
EmbedFlutterFrameworks
所做的事情)
- 导入Flutter引擎的对应模式版本(
Build
主要包含几个部分的工作:
-
检查路径和资源是否存在
- 目录不存在就创建
- Flutter 引擎不存在则报错
检查输入的变量是否符合
拷贝Flutter引擎到工程目录下,
${SOURCE_ROOT}/Flutter
或者${project_path}/.ios/Flutter
。-
编译App.framework
- Debug模式:生成App.framework,并生成dSYM
- Release/Profile模式:生成App.framework
编译资源包
编译App.framework
为了方便大家阅读,特意将几个重要的命令提取出来:
Release/Profile
# 执行Flutter的编译命令
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}
生成dSYM
在Release/Profile
模式下,默认会生成符号表dSYM
,用于符号的还原。方便崩溃的时候分析问题所在。不需要打入App.framework。因此,这里会将dSYM
从App.framework中剥离。
# 生成 dSYM 文件
RunCommand xcrun dsymutil -o "${build_dir}/dSYMs.noindex/App.framework.dSYM" "${app_framework}/App"
StreamOutput " ├─Stripping debug symbols..."
# 剥离调试符号表
RunCommand xcrun strip -x -S "${derived_dir}/App.framework/App"
StreamOutput "done"
Debug
- Debug模式会包含程序的
JIT编译快照
。
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" -)"
-
static const int Moo = 88;
这一句我暂时也不知道什么用的,先放着,之后分析。╮(╯▽╰)╭
编译资源包
# 编译资源包,若是debug模式则会包含flutter代码的JIT编译快照,此时app.framework中不含dart代码
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}/flutter_assets" \
${precompilation_flag} \
${local_engine_flag} \
${track_widget_creation_flag}
完整源码
BuildApp() {
# xcode工程根目录,SOURCE_ROOT这个变量来自xcode工程环境
local project_path="${SOURCE_ROOT}/.."
# FLUTTER_APPLICATION_PATH flutter工程目录,该变量来自Generated.xcconfig文件
# 若FLUTTER_APPLICATION_PATH不为空则,赋值给project_path
if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then
project_path="${FLUTTER_APPLICATION_PATH}"
fi
# flutter的程序入口文件目录
local target_path="lib/main.dart"
if [[ -n "$FLUTTER_TARGET" ]]; then
target_path="${FLUTTER_TARGET}"
fi
# Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
# This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
# they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
# 获取编译模式
# 根据编译模式设置相应变量
# artifact_variant是后续拷贝flutter引擎的时候使用,决定引擎的版本
# 在podhelper.rb中已经把flutter引擎集成进去了,不过依赖的是flutter工程本身编译模式引入的版本,可能不同
# 所以在这个脚本之中希望能够重新引入相应模式的engine
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
local artifact_variant="unknown".
case "$build_mode" in
release*) build_mode="release"; artifact_variant="ios-release";;
profile*) build_mode="profile"; artifact_variant="ios-profile";;
debug*) build_mode="debug"; artifact_variant="ios";;
*)
EchoError "========================================================================"
EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}."
EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."
EchoError "This is controlled by the FLUTTER_BUILD_MODE environment varaible."
EchoError "If that is not set, the CONFIGURATION environment variable is used."
EchoError ""
EchoError "You can fix this by either adding an appropriately named build"
EchoError "configuration, or adding an appriate value for FLUTTER_BUILD_MODE to the"
EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})."
EchoError "========================================================================"
exit -1;;
esac
# Archive builds (ACTION=install) should always run in release mode.
if [[ "$ACTION" == "install" && "$build_mode" != "release" ]]; then
EchoError "========================================================================"
EchoError "ERROR: Flutter archive builds must be run in Release mode."
EchoError ""
EchoError "To correct, ensure FLUTTER_BUILD_MODE is set to release or run:"
EchoError "flutter build ios --release"
EchoError ""
EchoError "then re-run Archive from Xcode."
EchoError "========================================================================"
exit -1
fi
# Flutter引擎的详细地址
local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}"
# 检查路径是否正确
AssertExists "${framework_path}"
AssertExists "${project_path}"
# Flutter的目标存放目录
local derived_dir="${SOURCE_ROOT}/Flutter"
if [[ -e "${project_path}/.ios" ]]; then
derived_dir="${project_path}/.ios/Flutter"
fi
RunCommand mkdir -p -- "$derived_dir"
AssertExists "$derived_dir"
RunCommand rm -rf -- "${derived_dir}/App.framework"
local local_engine_flag=""
local flutter_framework="${framework_path}/Flutter.framework"
local flutter_podspec="${framework_path}/Flutter.podspec"
# 如果本地的引擎存在,则引擎使用此路径,后续拷贝引擎从这个目录拷贝
if [[ -n "$LOCAL_ENGINE" ]]; then
if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then
EchoError "========================================================================"
EchoError "ERROR: Requested build with Flutter local engine at '${LOCAL_ENGINE}'"
EchoError "This engine is not compatible with FLUTTER_BUILD_MODE: '${build_mode}'."
EchoError "You can fix this by updating the LOCAL_ENGINE environment variable, or"
EchoError "by running:"
EchoError " flutter build ios --local-engine=ios_${build_mode}"
EchoError "or"
EchoError " flutter build ios --local-engine=ios_${build_mode}_unopt"
EchoError "========================================================================"
exit -1
fi
# 通过--local-engine直接指定本地引擎的目录
local_engine_flag="--local-engine=${LOCAL_ENGINE}"
flutter_framework="${LOCAL_ENGINE}/Flutter.framework"
flutter_podspec="${LOCAL_ENGINE}/Flutter.podspec"
fi
# 复制Flutter engine 到依赖目录
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
# 切换脚本执行目录到flutter工程,以便执行flutter命令
RunCommand pushd "${project_path}" > /dev/null
AssertExists "${target_path}"
# 是否需要详细日志的输出标记
local verbose_flag=""
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
verbose_flag="--verbose"
fi
#flutter build 目录
local build_dir="${FLUTTER_BUILD_DIR:-build}"
# 是否检测weidget的创建,release模式不支持此参数
local track_widget_creation_flag=""
if [[ -n "$TRACK_WIDGET_CREATION" ]]; then
track_widget_creation_flag="--track-widget-creation"
fi
# 非debug模式:执行flutter build aot ios …… 编译dart代码成app.framework
# 生成 dSYM 文件
# 剥离调试符号表
# debug模式:把『static const int Moo = 88;』这句代码打成app.framework,
# 直接使用JIT模式的快照
if [[ "${build_mode}" != "debug" ]]; then
StreamOutput " ├─Building Dart code..."
# Transform ARCHS to comma-separated list of target architectures.
local archs="${ARCHS// /,}"
if [[ $archs =~ .*i386.* || $archs =~ .*x86_64.* ]]; then
EchoError "========================================================================"
EchoError "ERROR: Flutter does not support running in profile or release mode on"
EchoError "the Simulator (this build was: '$build_mode')."
EchoError "You can ensure Flutter runs in Debug mode with your host app in release"
EchoError "mode by setting FLUTTER_BUILD_MODE=debug in the .xcconfig associated"
EchoError "with the ${CONFIGURATION} build configuration."
EchoError "========================================================================"
exit -1
fi
# 执行Flutter的编译命令
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}
if [[ $? -ne 0 ]]; then
EchoError "Failed to build ${project_path}."
exit -1
fi
StreamOutput "done"
local app_framework="${build_dir}/aot/App.framework"
RunCommand cp -r -- "${app_framework}" "${derived_dir}"
StreamOutput " ├─Generating dSYM file..."
# Xcode calls `symbols` during app store upload, which uses Spotlight to
# find dSYM files for embedded frameworks. When it finds the dSYM file for
# `App.framework` it throws an error, which aborts the app store upload.
# To avoid this, we place the dSYM files in a folder ending with ".noindex",
# which hides it from Spotlight, https://github.com/flutter/flutter/issues/22560.
RunCommand mkdir -p -- "${build_dir}/dSYMs.noindex"
# 生成 dSYM 文件
RunCommand xcrun dsymutil -o "${build_dir}/dSYMs.noindex/App.framework.dSYM" "${app_framework}/App"
if [[ $? -ne 0 ]]; then
EchoError "Failed to generate debug symbols (dSYM) file for ${app_framework}/App."
exit -1
fi
StreamOutput "done"
StreamOutput " ├─Stripping debug symbols..."
# 剥离调试符号表
RunCommand xcrun strip -x -S "${derived_dir}/App.framework/App"
if [[ $? -ne 0 ]]; then
EchoError "Failed to strip ${derived_dir}/App.framework/App."
exit -1
fi
StreamOutput "done"
else
RunCommand mkdir -p -- "${derived_dir}/App.framework"
# Build stub for all requested architectures.
local arch_flags=""
# 获取当前调试模式的架构参数
# 模拟器是x86_64
# 真机则根据实际的架构armv7或arm64
read -r -a archs <<< "$ARCHS"
for arch in "${archs[@]}"; do
arch_flags="${arch_flags}-arch $arch "
done
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" -)"
fi
# 嵌入Info.plist
local plistPath="${project_path}/ios/Flutter/AppFrameworkInfo.plist"
if [[ -e "${project_path}/.ios" ]]; then
plistPath="${project_path}/.ios/Flutter/AppFrameworkInfo.plist"
fi
RunCommand cp -- "$plistPath" "${derived_dir}/App.framework/Info.plist"
local precompilation_flag=""
if [[ "$CURRENT_ARCH" != "x86_64" ]] && [[ "$build_mode" != "debug" ]]; then
precompilation_flag="--precompiled"
fi
# 编译资源包,若是debug模式则会包含flutter代码的JIT编译快照,此时app.framework中不含dart代码
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}/flutter_assets" \
${precompilation_flag} \
${local_engine_flag} \
${track_widget_creation_flag}
if [[ $? -ne 0 ]]; then
EchoError "Failed to package ${project_path}."
exit -1
fi
StreamOutput "done"
StreamOutput " └─Compiling, linking and signing..."
# 将命令的输出信息输入到/dev/null中,消除命令回显信息的显示。
RunCommand popd > /dev/null
echo "Project ${project_path} built and packaged successfully."
return 0
}
Thin
- framework 分为 Thin and Fat Frameworks。Thin 指的是单个架构,而 Fat 指的是多个架构。
ThinAppFrameworks
剥离出部分架构的Framework:
ThinAppFrameworks() {
local app_path="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"
local frameworks_dir="${app_path}/Frameworks"
[[ -d "$frameworks_dir" ]] || return 0
find "${app_path}" -type d -name "*.framework" | while read framework_dir; do
# $1:"$framework_dir",$2:"$ARCHS"
ThinFramework "$framework_dir" "$ARCHS"
done
}
ThinFramework
ThinFramework() {
local framework_dir="$1"
# 位置参数可以用shift命令左移,不带参数的shift命令相当于shift 1。
# $1:"$framework_dir",$2:"$ARCHS"
# 执行了shift后,变成了$1:"$ARCHS"
shift
local plist_path="${framework_dir}/Info.plist"
local executable="$(GetFrameworkExecutablePath "${framework_dir}")"
# 这里的$@指的是原来的$2,即ARCH参数
LipoExecutable "${executable}" "$@"
}
LipoExecutable
简单介绍下lipo
指令:
-
lipo -info
:查看 Framework 支持的CPU架构。
$ lipo -info /Debug-iphoneos/Someframework.framwork/Someframework
# Architectures in the fat file: Someframework are: armv7 armv7s arm64
-
lipo –create ... -outpu ...
:合并多个架构的 Framework。
# 合并a.framework b.framework为output.framework
$ lipo –create a.framework b.framework –output output.framework
-
lipo ... -thin 架构 -output ...
:拆分指定CPU架构。
# 从a.framework中拆分架构为armv7的a-output-armv7.framework
$ lipo App.framework/App -thin ${arch} -output App.framework/App
-
lipo -remove cpu(armv7/arm64等) xxxx -output xxxx
:移除掉特定的cpu架构的文件
下面我们看看源码分析:
# Destructively thins the specified executable file to include only the
# specified architectures.
LipoExecutable() {
local executable="$1"
# 位置参数可以用shift命令左移,不带参数的shift命令相当于shift 1。
# 去掉"${executable}",保留"$ARCHS"参数。
shift
# Split $@ into an array.
read -r -a archs <<< "$@"
# Extract architecture-specific framework executables.
local all_executables=()
for arch in "${archs[@]}"; do
local output="${executable}_${arch}"
local lipo_info="$(lipo -info "${executable}")"
if [[ "${lipo_info}" == "Non-fat file:"* ]]; then
if [[ "${lipo_info}" != *"${arch}" ]]; then
echo "Non-fat binary ${executable} is not ${arch}. Running lipo -info:"
echo "${lipo_info}"
exit 1
fi
else
lipo -output "${output}" -extract "${arch}" "${executable}"
if [[ $? == 0 ]]; then
all_executables+=("${output}")
else
echo "Failed to extract ${arch} for ${executable}. Running lipo -info:"
lipo -info "${executable}"
exit 1
fi
fi
done
# Generate a merged binary from the architecture-specific executables.
# Skip this step for non-fat executables.
if [[ ${#all_executables[@]} > 0 ]]; then
local merged="${executable}_merged"
# 合并指定的多个架构为一个Framework
lipo -output "${merged}" -create "${all_executables[@]}"
cp -f -- "${merged}" "${executable}" > /dev/null
rm -f -- "${merged}" "${all_executables[@]}"
fi
}
Embed
复制资源和签名Framework。
EmbedFlutterFrameworks
主要做了这几件事:
- 复制flutter_asserts到app目录下
- 复制Flutter引擎到app包
- 复制dart代码编译产物app.framework到app包
- 签名两个framework(App.framework、Flutter.framework)
EmbedFlutterFrameworks() {
AssertExists "${FLUTTER_APPLICATION_PATH}"
# Prefer the hidden .ios folder, but fallback to a visible ios folder if .ios
# doesn't exist.
local flutter_ios_out_folder="${FLUTTER_APPLICATION_PATH}/.ios/Flutter"
local flutter_ios_engine_folder="${FLUTTER_APPLICATION_PATH}/.ios/Flutter/engine"
if [[ ! -d ${flutter_ios_out_folder} ]]; then
flutter_ios_out_folder="${FLUTTER_APPLICATION_PATH}/ios/Flutter"
flutter_ios_engine_folder="${FLUTTER_APPLICATION_PATH}/ios/Flutter"
fi
AssertExists "${flutter_ios_out_folder}"
# Copy the flutter_assets to the Application's resources.
AssertExists "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/"
RunCommand cp -r -- "${flutter_ios_out_folder}/flutter_assets" "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/"
# Embed App.framework from Flutter into the app (after creating the Frameworks directory
# if it doesn't already exist).
local xcode_frameworks_dir=${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app/Frameworks"
RunCommand mkdir -p -- "${xcode_frameworks_dir}"
RunCommand cp -Rv -- "${flutter_ios_out_folder}/App.framework" "${xcode_frameworks_dir}"
# Embed the actual Flutter.framework that the Flutter app expects to run against,
# which could be a local build or an arch/type specific build.
# Remove it first since Xcode might be trying to hold some of these files - this way we're
# sure to get a clean copy.
RunCommand rm -rf -- "${xcode_frameworks_dir}/Flutter.framework"
RunCommand cp -Rv -- "${flutter_ios_engine_folder}/Flutter.framework" "${xcode_frameworks_dir}/"
# Sign the binaries we moved.
local identity="${EXPANDED_CODE_SIGN_IDENTITY_NAME:-$CODE_SIGN_IDENTITY}"
if [[ -n "$identity" && "$identity" != "\"\"" ]]; then
RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/App.framework/App"
RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi
}
签名
这里会对生成的framework进行签名。
# Sign the binaries we moved.
local identity="${EXPANDED_CODE_SIGN_IDENTITY_NAME:-$CODE_SIGN_IDENTITY}"
if [[ -n "$identity" && "$identity" != "\"\"" ]]; then
RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/App.framework/App"
RunCommand codesign --force --verbose --sign "${identity}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi
其他
脚本中其他的函数,这里就简单的注释说明下。
RunCommand
执行命令(通过VERBOSE_SCRIPT_LOGGING
控制是否需要详细日志的输出标记)
RunCommand() {
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
echo "♦ $*"
fi
"$@"
return $?
}
-
$*
和$@
:一次性获取所有的参数——浅谈$*
和$@
的区别。 -
$?
:获取上一个命令的退出状态。
StreamOutput
输出字符串到指定的路径。
StreamOutput() {
if [[ -n "$SCRIPT_OUTPUT_STREAM_FILE" ]]; then
echo "$1" > $SCRIPT_OUTPUT_STREAM_FILE
fi
}
EchoError
EchoError() {
echo "$@" 1>&2
}
AssertExists
验证路径中的资源是否存在。
AssertExists() {
if [[ ! -e "$1" ]]; then
if [[ -h "$1" ]]; then
EchoError "The path $1 is a symlink to a path that does not exist"
else
EchoError "The path $1 does not exist"
fi
exit -1
fi
return 0
}
GetFrameworkExecutablePath
Returns the CFBundleExecutable for the specified framework directory.
GetFrameworkExecutablePath() {
local framework_dir="$1"
local plist_path="${framework_dir}/Info.plist"
local executable="$(defaults read "${plist_path}" CFBundleExecutable)"
echo "${framework_dir}/${executable}"
}
附录
Flutter混编分析
- Flutter iOS 混合工程自動化:繁体字版本,这两篇文章是一样的。
- Flutter iOS 混合工程自动化