Flutter iOS 混合工程自動化


問題

Flutter提供的混編方案直接依賴於Flutter工程和Flutter環境,非Flutte團隊成員沒法脫離Flutter環境進行開發,團隊合做成本加劇。ios

指望

Flutter默認的混編方式:不光依賴於flutter工程中的flutter產物,還依賴於flutter SDK中的xcode_backend.sh腳本。咱們但願可以作到當項目混編的時候,沒有開發flutter的團隊成員可以徹底脫離flutter,不須要flutter項目代碼和安裝flutter環境;而寫flutter的團隊成員可以按照原有的混編方式以方便開發和調試。git

帶着這個目標,咱們來一步一步分析混編過程。github

理清依賴

iOS項目都依賴了Flutter的哪些東西

Flutter生成的iOS項目

看圖,看圖,這個是Flutter編譯生成的Runner工做空間。iOS依賴的Flutter產物都在這個Flutter文件夾中。 依次來介紹一下這些傢伙:shell

  • .symlinks Flutter的三方包package,是各個文件夾的索引,指向了本地的pub緩存區的包。每個包裏面都包含一個iOS的本地pod倉庫,在包的iOS文件夾中。於是Flutter包的依賴方式直接pod導入便可。數組

  • App.framework 由Flutter項目的Dart代碼編譯而成,僅僅是framework。集成的時候能夠本身作成本地pod庫也能夠直接拷貝進app包,而後簽名。xcode

  • AppFrameworkInfi.plist Flutter的一些可有可無的配置信息,忽略緩存

  • engine Flutter渲染引擎,也是一個本地pod倉庫ruby

  • flutter_assets Flutter的資源文件,圖片等,集成時拷貝進app包便可bash

  • 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)
複製代碼
  1. 添加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試用報告](www.jianshu.com/p/b32538e68…

Flutter是如何轉換成iOS應用程序包的?


參考文章: 閒魚Fultter混合工程持續集成的最佳實踐


筆者和朋友作了淘寶優惠券公衆號,購物領券省錢,幫忙關注一下。

微信公衆號
相關文章
相關標籤/搜索