Flutter iOS 混合工程自动化


问题

Flutter提供的混编方案直接依赖于Flutter工程和Flutter环境,非Flutte团队成员无法脱离Flutter环境进行开发,团队合作成本加重。

期望

Flutter默认的混编方式:不光依赖于flutter工程中的flutter产物,还依赖于flutter SDK中的xcode_backend.sh脚本。我们希望能够做到当项目混编的时候,没有开发flutter的团队成员能够完全脱离flutter,不需要flutter项目代码和安装flutter环境;而写flutter的团队成员能够按照原有的混编方式以方便开发和调试。

带着这个目标,我们来一步一步分析混编过程。

理清依赖

iOS项目都依赖了Flutter的哪些东西
Flutter生成的iOS项目

看图,看图,这个是Flutter编译生成的Runner工作空间。iOS依赖的Flutter产物都在这个Flutter文件夹中。
依次来介绍一下这些家伙:

  • .symlinks
    Flutter的三方包package,是各个文件夹的索引,指向了本地的pub缓存区的包。每一个包里面都包含一个iOS的本地pod仓库,在包的iOS文件夹中。因而Flutter包的依赖方式直接pod导入即可。

  • App.framework
    由Flutter项目的Dart代码编译而成,仅仅是framework。集成的时候可以自己做成本地pod库也可以直接拷贝进app包,然后签名。

  • AppFrameworkInfi.plist
    Flutter的一些无关紧要的配置信息,忽略

  • engine
    Flutter渲染引擎,也是一个本地pod仓库

  • flutter_assets
    Flutter的资源文件,图片等,集成时拷贝进app包即可

  • FlutterPluginRegistrant
    Fluttter三方包的注册代码,有引入三方包时,需要引入这个,也是一个本地pod仓库

  • Generated.xcconfig
    Flutter相关的一些路径信息,配置信息等。整个文件会被引入到iOS工程的各个*.xcconfig配置文件中。这些配置信息,在xcode runscript中引入的flutter编译嵌入脚本xcode_backend.sh中会使用到。当然你也可以修改脚本,去除对这个文件的依赖。

  • podhelper.rb
    ruby脚本,包含了一个 cocoapod钩子,在pod的安装过程中引入flutter的所有本地库依赖,并在每个*.xcconfig配置文件中写进 『导入Generated.xcconfig』的代码,如#include '.../Generated.xcconfig');

脚本分析

本质上,理清依赖的前提是 阅读脚本,提前贴出来是为了分析脚本的时候能够更好地理解过程。

默认的混编方案流程是
1 在Podfile加入脚本

#Flutter工程路径
flutter_application_path = 'flutter_project_dir'
#读取 podhelper.rb 的Ruby代码在当前目录执行
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

2 添加Run script 脚本

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build 
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

然后pod install即可。

一切的秘㊙️就在这两个脚本中。

分析podhelper.rb

这个Ruby脚本只有七十多行,鉴于不是每个人都熟悉Ruby脚本,我详细注释了一下:

# 解析文件内容为字典数组
# 文件内容格式为  A=B换行C=D   的类型
# 如 A=B
#    C=D
# 解析为:
# {"A"="B","C"="D"}

def parse_KV_file(file, separator='=')
    file_abs_path = File.expand_path(file)
    if !File.exists? file_abs_path
        return [];
    end
    pods_array = []
    skip_line_start_symbols = ["#", "/"]
    File.foreach(file_abs_path) { |line|
        next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
        plugin = line.split(pattern=separator)
        if plugin.length == 2
            podname = plugin[0].strip()
            path = plugin[1].strip()
            podpath = File.expand_path("#{path}", file_abs_path)
            pods_array.push({:name => podname, :path => podpath});
         else
            puts "Invalid plugin specification: #{line}"
        end
    }
    return pods_array
end


# 这是个函数,功能是从flutter工程生成的iOS依赖目录中的Generated.xcconfig文件解析
# FLUTTER_ROOT目录,也就是你安装的flutter SDKf根目录
def flutter_root(f)
    generated_xcode_build_settings = parse_KV_file(File.join(f, File.join('.ios', 'Flutter', 'Generated.xcconfig')))
    if generated_xcode_build_settings.empty?
        puts "Generated.xcconfig must exist. Make sure `flutter packages get` is executed in ${f}."
        exit
    end
    generated_xcode_build_settings.map { |p|
        if p[:name] == 'FLUTTER_ROOT'
            return p[:path]
        end
    }
end


# 代码入口在这里
# flutter工程目录,如果没有值,则取向上退两级的目录(也就是Flutter生成整个iOS项目的情况)
flutter_application_path ||= File.join(__dir__, '..', '..')
# Flutter生成的framework目录,引擎库,编译完成的代码库等几乎所有iOS项目的依赖都放在这里
framework_dir = File.join(flutter_application_path, '.ios', 'Flutter')

# flutter引擎目录
engine_dir = File.join(framework_dir, 'engine')

# 如果引擎目录不存在就去 flutter SDK目录中拷贝一份,引擎是一个本地pod库
# File.join,功能是拼接文件目录
if !File.exist?(engine_dir)
    # 这个是debug版本的flutter引擎目录,release的最后一级为「ios-release」,profile版本为ios-profile
    debug_framework_dir = File.join(flutter_root(flutter_application_path), 'bin', 'cache', 'artifacts', 'engine', 'ios')
    FileUtils.mkdir_p(engine_dir)
    FileUtils.cp_r(File.join(debug_framework_dir, 'Flutter.framework'), engine_dir)
    FileUtils.cp(File.join(debug_framework_dir, 'Flutter.podspec'), engine_dir)
end

# 这个应该每个人都很熟悉
#加载flutter引擎pod库
pod 'Flutter', :path => engine_dir
#加载flutter三方库的注册代码库
pod 'FlutterPluginRegistrant', :path => File.join(framework_dir, 'FlutterPluginRegistrant')

#flutter三方库的快捷方式文件夹,最终索引到pub缓存中的各个库的目录
symlinks_dir = File.join(framework_dir, '.symlinks')
FileUtils.mkdir_p(symlinks_dir)
#解析.flutter-plugins文件,获取当前flutter工程用到的三方库
plugin_pods = parse_KV_file(File.join(flutter_application_path, '.flutter-plugins'))
#加载当前工程用到的每一个pod库
plugin_pods.map { |r|
    symlink = File.join(symlinks_dir, r[:name])
    FileUtils.rm_f(symlink)
    File.symlink(r[:path], symlink)
    pod r[:name], :path => File.join(symlink, 'ios')
}

# 修改所有pod库的 ENABLE_BITCODE 为 NO,含原生代码引用的pod库
# 并在每一个pod库的.xcconfig文件中引入Generated.xcconfig文件
# 该文件中包含一系列flutter需要用到的变量,具体在xcode_backend.sh脚本中会使用到
post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['ENABLE_BITCODE'] = 'NO'
            xcconfig_path = config.base_configuration_reference.real_path
            File.open(xcconfig_path, 'a+') do |file|
                file.puts "#include \"#{File.realpath(File.join(framework_dir, 'Generated.xcconfig'))}\""
            end
        end
    end
end

总结一下,这个Ruby脚本旧做了以下这几件事情

  • 引入Flutter引擎
  • 引入Flutter三方库的注册代码
  • 引入Flutter的所有三方库
  • 在每一个pod库的配置文件中写入对Generated.xcconfig 文件的导入
  • 修改pod库的的ENABLE_BITCODE

至此,还缺少Dart代码库以及flutter引入的资源,这个在xcode_backend.sh脚本实现了。这个脚本在flutter SDK的packages/flutter_tools/bin

同样看一下所有代码,以及详细注释:

#!/bin/bash
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

RunCommand() {
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    echo "♦ $*"
  fi
  "$@"
  return $?
}

# When provided with a pipe by the host Flutter build process, output to the
# pipe goes to stdout of the Flutter build process directly.
StreamOutput() {
  if [[ -n "$SCRIPT_OUTPUT_STREAM_FILE" ]]; then
    echo "$1" > $SCRIPT_OUTPUT_STREAM_FILE
  fi
}

EchoError() {
  echo "$@" 1>&2
}

# 验证路径中的资源是否存在
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
}

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_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..."

  RunCommand popd > /dev/null

  echo "Project ${project_path} built and packaged successfully."
  return 0
}

# 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}"
}

# Destructively thins the specified executable file to include only the
# specified architectures.
LipoExecutable() {
  local executable="$1"
  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"
    lipo -output "${merged}" -create "${all_executables[@]}"

    cp -f -- "${merged}" "${executable}" > /dev/null
    rm -f -- "${merged}" "${all_executables[@]}"
  fi
}

# Destructively thins the specified framework to include only the specified
# architectures.
ThinFramework() {
  local framework_dir="$1"
  shift

  local plist_path="${framework_dir}/Info.plist"
  local executable="$(GetFrameworkExecutablePath "${framework_dir}")"
  LipoExecutable "${executable}" "$@"
}

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
    ThinFramework "$framework_dir" "$ARCHS"
  done
}

# Adds the App.framework as an embedded binary and the flutter_assets as
# resources.
# 主要做了这几件事:
# 复制flutter_asserts到app包
# 复制Flutter引擎到app包
# 复制dart代码编译产物app.framework到app包
# 签名两个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
}

# 主函数入口
# 以下结合xcode run srcript中的脚本就很好理解
# 编译、嵌入
#"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
#"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

if [[ $# == 0 ]]; then # 如果不带参数则直接执行BuildApp函数
  # Backwards-compatibility: if no args are provided, build.
  BuildApp
else # 否则执行case语句
  case $1 in
    "build")
      BuildApp ;;
    "thin")
      ThinAppFrameworks ;;
    "embed")
      EmbedFlutterFrameworks ;;
  esac
fi

同样,总结一下,这个shell脚本做了以下事情

  • 编译Dart代码为App.framework(非debug模式),编译static const int Moo = 88;为App.framework(猜测此行代码为JIT/AOT模式切换标记)
  • 重新导入Flutter引擎的对应模式版本(debug/profile/release)
  • 编译flutter资源(flutter_asserts),如果是debug 资源中会包含JIT模式的代码快照
  • 向iOS app包中嵌入资源,框架,签名

这一节大部分都贴代码了,如果是简单讲过程可能不是很好理解,详细的大家还是直接读脚本吧。如果看不懂脚本,看注释也是能够了解个大概。

方案

依赖以及过程都理清楚了,最后是时候说方案了。
回头看一起期望

  • 非flutter开发人员可完全脱离Flutter环境
  • flutter开发人员仍按照原有的依赖方式

到了这里,我们还是希望能够做的更好一点,就是能够实现两种模式的切换。大概画了一个图,大家将就看一下。


混编方案

方案大概的解决方法就是:

  • 完全脱离Flutter环境:(图中实线流程部分)
    利用脚本将所有的依赖编译结果从Flutter工程中剥离出来,放到iOS工程目录下。iOS native直接依赖此目录,不再编译,即可以脱离Flutter环境了。(环境可以直接是release,因为脱离Flutter的环境不会去调试Flutter代码的。)

  • 直接依赖Flutter工程:(图中虚线流程部分)
    直接依赖时,pod对Flutter的依赖都直接指向了Flutter工程;另外就是xcode_backend.sh会去重新编译Flutter代码,Flutter资源并嵌入app;Flutter引擎也会重新嵌入相应模式的版本。

方案存在的问题

直接依赖Flutter工程的方式,这个大同小异,都是直接或间接指向Flutter工程。这里重点讨论完全脱离Flutter环境的方案。

以咸鱼为代表的远程Flutter方案
Flutter远程依赖

咸鱼团队自己也提到存在以下问题

  1. Flutter工程更新,远程依赖库更新不及时。
  2. 版本集成时,容易忘记更新远程依赖库,导致版本没有集成最新Flutter功能。
  3. 同时多条线并行开发Flutter时,版本管理混乱,容易出现远程库被覆盖的问题。
  4. 需要最少一名同学持续跟进发布,人工成本较高。
    鉴于这些问题,我们引入了我们团队的CI自动化框架,从两方面来解决:
    (关于CI自动化框架,我们后续会撰文分享)
    一方面是自动化,通过自动化减少人工成本,也减少人为失误。
    另一方面是做好版本控制, 自动化的形式来做版本控制。
    具体操作:
    首先,每次需要构建纯粹Native工程前自动完成Flutter工程对应的远程库的编译发布工作,整个过程不需要人工干预。
    其次,在开发测试阶段,采用五段式的版本号,最后一位自动递增产生,这样就可以保证测试阶段的所有并行开发的Flutter库的版本号不会产生冲突。
    最后,在发布阶段,采用三段式或四段式的版本号,可以和APP版本号保持一致,便于后续问题追溯。
我们的方案
直接把Flutter放在原生工程中

这个方案相比咸鱼的方案解决了原生依赖Flutter库版本号的问题。放在原生之中的Flutter依赖直接归为原生管理,不需要独立的版本。这个依赖拿到的是Flutter开发成员发布的代码,一般情况下都是对应分支的最新flutter代码编译产物。
如iOS的dev对应Flutter的dev,齐头并进,版本管理上就会简单的多。

但是同样会有Flutter依赖更新不及时等这些其他问题,有待进一步调研和实践。


延伸阅读:Flutter试用报告


参考文章:
闲鱼Fultter混合工程持续集成的最佳实践

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

推荐阅读更多精彩内容