Flutter xcode_backend分析

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342