Flutter xcode_backend分析

我从来不相信什么懒洋洋的自由,我向往的自由是通过勤奋和努力实现的更广阔的人生,那样的自由才是珍贵的、有价值的;我相信一万小时定律,我从来不相信天上掉馅饼的灵感和坐等的成就。做一个自由又自律的人,靠势必实现的决心认真地活着。

前言

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所做的事情)

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混编分析

发布了181 篇原创文章 · 获赞 217 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/Notzuonotdied/article/details/103844022
今日推荐