swift调用icloud_ios 从assets加载图片

Liangliang Hu

19年加入流利说,毕业于安徽工业大学的 iOS 开发者,热衷于各种新技术新理念。

目前主要领域是 iOS 和 Flutter。平时喜欢唱歌、看电影、美食。

是一名专注于 Cooking 的工程师。

前言

Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。Flutter 可以与现有的代码一起工作。在全世界,Flutter 正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。

秉持着不断学习和创建的理念,我们也开始尝试 Flutter,并且去深入探索。流利说团队一直走在技术前沿,流利说少儿英语 App 从 19年7月份 开始正式引入 Flutter 。引入之路,我们首先遇到的问题就是 Flutter 的集成,本文主要是以 iOS 视角阐述。

调研

目前 Flutter 的常见集成方式是两种:

  • 源码集成
  • 产物集成

源码集成

Flutter 官方推荐的集成方式就是源码集成

以下这部分摘自官方文档:

1. Add the following lines to your `Podfile`:

flutter_application_path = 'path/to/my_flutter/'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

2. For each Xcode target that need to embed Flutter, call `install_all_flutter_pods(flutter_application_path)`.

target 'MyApp' do
    install_all_flutter_pods(flutter_application_path)
end
  target 'MyAppTests' do
    install_all_flutter_pods(flutter_application_path)
end

1. Run `pod install`
2. Disable bitcode for your targets

swift调用icloud_ios_02

其中核心就是 App.framework 和 Flutter.framework

我们展开看一下:

swift调用icloud_ios 从assets加载图片_03

swift调用icloud_ios 从assets加载图片_04

App.framework,dart业务源码相关文件,在 Debug 模式下就是一个很小的空壳,在 Release 模式下包含全部业务逻辑。其核心就是 flutter_assets。这是 Flutter 依赖的静态资源,如字体,图片等。

Flutter.framework,此处看不明朗。它包含了 Flutter 的库和 engine 部分,以及 Embedder ( Embedder 是一个嵌入层,即把 Flutter 嵌入到各个平台上去)。位于 Flutter 仓库的/bin/cache/artifacts/engine/ios*下,默认从 google 仓库拉取。

主要问题:
  • 所有涉及的 Native 开发人员都需要安装 Flutter 环境,并且需要保证所有人的 Flutter 版本一致。
  • 通过 Cocoapods 将 Flutter 引入现有工程时不能保证所有人的路径一致。我们可以通过 submodule 的方式保证这个问题,但是 submodule 的更新过于频繁,因为 Flutter 工程的任何改动都需要及时同步,这对维护来说带来了很大的不便。
  • Flutter 工程需要依赖 Native 工程来编译,并且影响了 Native 工程 的开发流程与打包流程。

产物集成

其实源码集成的后续就是产物化。经过 pod install 之后会生成对应的 Flutter.framework 和 App.framework,以及可能用到的一些 plugins。Flutter 所有相关内容就是一个 Pod

而产物化其实就是通过其他方式获取到上述的 "Pod",放入到合适的位置,这样侵入性较低。但是我们还需要考虑到调试的问题,不需要进行 Flutter 调试的开发不需要关心这个细节。那么简单来说,我们的目标就是:

  • 对 Native 工程无侵入
  • 易本地调试
  • 不影响 Native 工程的开发流程与打包流程

分析

我们分析 pod install 和过程以及调试的过程,发现有两个重要的脚本。

  • podhelper.rb
  • xcode_backend.sh

小窥一下。

  • podhelper.rb
def install_all_flutter_pods(flutter_application_path = nil)
  flutter_application_path ||= File.join('..', '..')
  install_flutter_engine_pod  ## flutter engine,即 Flutter.framework
  install_flutter_plugin_pods(flutter_application_path) ## flutter plugins
  install_flutter_application_pod(flutter_application_path) ## flutter_assets,即App.framework
end
  • xcode_backend.sh
RunCommand mkdir -p -- "${derived_dir}/App.framework"

# Build stub for all requested architectures.
local arch_flags=""
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} \
    -fembed-bitcode-marker \
    -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" -)"
StreamOutput " ├─Assembling Flutter resources..."
  RunCommand "${FLUTTER_ROOT}/bin/flutter"     \
    ${verbose_flag}                                                         \
    build bundle                                                            \
    --target-platform=ios                                                   \
    --target="${target_path}"                                               \
    --${build_mode}                                                         \
    --depfile="${build_dir}/snapshot_blob.bin.d"                            \
    --asset-dir="${derived_dir}/App.framework/${assets_path}"               \
    ${precompilation_flag}                                                  \
    ${flutter_engine_flag}                                                  \
    ${local_engine_flag}                                                    \
    ${track_widget_creation_flag}

podhelper.rb 中是安装了所需要的 Flutter.framework App.framework和 plugins

xcode_backend.sh 中是重新 build 这些,将所需要的资源文件导入到执行的位置。

接下来我们就围绕这两个环节进行。

第一步:build flutter iOS

  1. flutter_get_packages
flutter packages get
  1. build_flutter_app
RunCommand "${FLUTTER_COMMAND}" --suppress-analytics                    \
      --verbose                                                             \
      build aot                                                             \
      --output-dir="build/aot"                                              \
      --target-platform=ios                                                 \
      --target="${target_path}"                                             \
      --release                                                             \
      --ios-arch="${ARCHS_ARM}"
RunCommand "${FLUTTER_COMMAND}" --suppress-analytics                    \
        --verbose                                                           \
        build bundle                                                        \
        --target-platform=ios                                               \
        --target="${target_path}"                                           \
        --release                                                           \
        --depfile="${BUILD_PATH}/snapshot_blob.bin.d"                       \
        --asset-dir="${BUILD_PATH}/flutter_assets"
RunCommand cp -rf -- "${BUILD_PATH}/flutter_assets" "${BUILD_PATH}/App.framework"
RunCommand cp -r -- "${BUILD_PATH}/App.framework" "${PRODUCT_APP_PATH}"
RunCommand cp -r -- "${flutter_framework}" "${PRODUCT_APP_PATH}"
RunCommand cp -r -- "${flutter_podspec}" "${PRODUCT_APP_PATH}"
RunCommand cp -r -- "${flutter_license}" "${PRODUCT_APP_PATH}"

3.flutter_copy_plugins

local flutter_plugin_registrant="FlutterPluginRegistrant"
local flutter_plugin_registrant_path=".ios/Flutter/${flutter_plugin_registrant}"
echo "copy 'flutter_plugin_registrant' from '${flutter_plugin_registrant_path}' to '${PRODUCT_PATH}/${flutter_plugin_registrant}'"
RunCommand cp -rf -- "${flutter_plugin_registrant_path}" "${PRODUCT_PATH}/${flutter_plugin_registrant}"

第二步:upload flutter ios

1.update flutter-archive

2.copy flutter resources

3.upload -- git/cocoapods

关于 upload 的方式不打算更多描述了。方式有很多。我们目前简单采用了远端仓库的方法去管理。

我们现在的 Podfile 长这样:

platform :ios, '10.0'
workspace 'xxx'
project 'xxx'
project 'xxx'

flutter_application_path = xxx
flutter_pod_helper_path = xxx

FLUTTER_APP_URL = xxx
FLUTTER_VERSION_REQUIREMENT = 'develop'
FLUITTER_RELEASE_PATH = xxx

use_frameworks!

target 'xxx' do
  project 'xxx'
	eval(File.read(File.join(flutter_pod_helper_path, 'xxx.rb')), binding)
end

swift调用icloud_swift调用icloud_05

只需要 pod install,即可获得对应产物。

上述两步只是对 Flutter 的 xcode_backend.sh 做初步翻译。具体的需要根据实际业务来做。目前我们在 Flutter 工程中将这套产物化和 CI 结合,已经大大提高了效率!

打包流程:

swift调用icloud_ios 从assets加载图片_06

目前只是获取到了产物,但我们的目标是要方便调试,只有产物是不够的。flutter attach也有很大局限性。

我们最终的方案是源码和产物的双支持。

最终方案

源码和产物的双支持。

首先,上述的产物化是我们的重要一环,对于非 Flutter 开发人员,只需要根据设定的方式 pod install 引入即可,他们不需要 Flutter 环境,这也是我们的实行初衷。但是对于 Flutter 开发人员,我们需要支持源码调试。

我们知道,集成 Flutter 产物时通过 pod install 的,又或者说是基于 cocoapods 的。

eval(File.read(File.join(flutter_pod_helper_path, 'xxx.rb')), binding)

Podfile 中的这一句决定了我们是采用哪个路径的 ruby 去加载framework。我们可以选择源码对应的路径,也可以选择产物对应的路径。但是我们知道 Podfile 是不能接受参数的。这似乎无法实现我们的自定义。(强烈不推荐调试时直接修改 Podfile,提交代码时很可能会将自己的配置推到远端,造成垃圾代码。Podfile 也是不能被忽略的)

存在问题:如何在不改动 Podfile 的情况下 install 到我们指定的库??

利用 ruby 的特性

Podfile 是一个内部 DSL( DSL 是 Domain Specific Language 的缩写。DSL 通常需要跟某个宿主语言配合,不可单独使用。)。CocoaPods 是 Ruby 编写的,Podfile 也使用 Ruby 语法,是 Ruby 去解释 Ruby。

我们可以将编译参数通过环境变量的形式让 Podfile 去读取。比如 FLUTTER_TYPE

env FLUTTER_TYPE='debug' pod install

这似乎是解决了我们的问题?但是我们注意到源码集成中有这样一句:

install_all_flutter_pods(flutter_application_path)

它需要根据我们源码的路径来安装 framework。这又是一个问题?难道我们又写入环境变量吗?

这样做似乎有点不太灵活?而且 Build Phases 中 xcode_backend.sh 也需要区分环境,那里又不是 ruby 实现。

另外,即使是采用了环境变量的形式,不能保证我们每次运行工程的话都存在对应的环境变量的值,不能每次 build 都需要 pod install一下。

最终我们采用落盘变量配置的方式。

方案落定

flutter-debug.sh

#!/bin/sh

# stop script on error
set -e

EchoError() {
    echo "\033[31m$@\033[0m" 1>&2
    exit -1
}

flutter_path=$@

if [ ! -d "$flutter_path" ]; then
 	EchoError "No valid flutter_path: $flutter_path"
fi

env FLUTTER_TYPE='debug' env FLUTTER_APPLICATION_PATH="$flutter_path" pod install
  • 需要切换到 flutter-debug 环境时,只需要运行
flutter-debug.sh $my_flutter_project_path
  • 需要切换 flutter-release 环境时,只需要执行默认的 pod install即可。

flutter-build.sh

#!/bin/sh

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

set -e
set -u

echo "********************************************"
echo "********    Start build flutter.   ********"
echo "********************************************"

if [[ ! -f "${SRCROOT}/../flutterConfig" ]]; then
	EchoError "No valid flutterConfig. Please run 'pod install' or './flutter-debug.sh your_flutter_path'"
fi

flutterBuild=`awk '/FLUTTER_DEBUG/{ print $0 }'  "${SRCROOT}/../flutterConfig" |awk -F '=' '{print $2}' `

if [[ "$flutterBuild" =~ 'true' ]]; then
	echo "Flutter debug is true."

	flutterApplicationPath=`awk '/FLUTTER_APPLICAION_PATH/{ print $0 }'  "${SRCROOT}/../flutterConfig" |awk -F '=' '{print $2}' `
	flutterPath="${SRCROOT}/../${flutterApplicationPath}"
	flutterEnvironmentPath="$flutterPath/.ios/Flutter/flutter_export_environment.sh"

	echo "Export flutter environment."
  source "$flutterEnvironmentPath"
  "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
fi

在 Build Phases 中添加 Script,引入 flutter_build.sh

swift调用icloud_ios_07

Podfile

platform :ios, '10.0'
workspace 'xxx'
project 'xxx'

flutter_application_path = ENV['FLUTTER_APPLICATION_PATH']
flutter_pod_helper_path = 'xxx'

FLUTTER_APP_URL = 'xxx'
FLUTTER_VERSION_REQUIREMENT = 'develop' ## 支持branch tag commit
FLUITTER_RELEASE_PATH = 'xxx'

use_frameworks!

eval(File.read(File.join(flutter_pod_helper_path, 'flutter-pod-helper.rb')), binding)

target 'xxx' do
  project 'xxx'

if ENV['FLUTTER_TYPE'] == 'debug'
    install_all_flutter_pods(flutter_application_path)
end
end

flutterConfig

此配置文件需要添加到 .gitignore ,这是产生的配置文件,不应该提交到远端。

echo flutterConfig >> .gitignore

flutter-pod-helper.rb

# 安装
def install_flutter_framework
case ENV['FLUTTER_TYPE']
when 'debug'
        flutter_application_path = ENV['FLUTTER_APPLICATION_PATH']
        flutter_application_input = "FLUTTER_APPLICAION_PATH=#{flutter_application_path}"
        system 'echo "FLUTTER_DEBUG=true" > flutterConfig'
        system "echo '#{flutter_application_input}' >> flutterConfig"
        current_flutter_version = check_flutter_version()
        raise "No valid flutter version exists. Please install or check flutter" unless current_flutter_version
# 加载源码 -- debug
        load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
else
        system 'echo "FLUTTER_DEBUG=false" > flutterConfig'
# 安装产物 -- release
        install_release_flutter_app()
end
end

# 开始安装 flutter framework
install_flutter_framework()

这个脚本内容过多,这里只展示部分。

安装:

swift调用icloud_ios_08

总结

Flutter 更新迭代很快,本方案适用 v1.9.1+hotfix.6 版本。不管使用什么方案都需要做到高效。我们的 Flutter 之路才刚刚开始,还有很多的东西需要我们去探索!路漫漫其修远兮!