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
其中核心就是 App.framework
和 Flutter.framework
。
我们展开看一下:
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
- flutter_get_packages
flutter packages get
- 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
只需要 pod install
,即可获得对应产物。
上述两步只是对 Flutter 的 xcode_backend.sh
做初步翻译。具体的需要根据实际业务来做。目前我们在 Flutter 工程中将这套产物化和 CI 结合,已经大大提高了效率!
打包流程:
目前只是获取到了产物,但我们的目标是要方便调试,只有产物是不够的。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
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()
这个脚本内容过多,这里只展示部分。
安装:
总结
Flutter 更新迭代很快,本方案适用 v1.9.1+hotfix.6 版本。不管使用什么方案都需要做到高效。我们的 Flutter 之路才刚刚开始,还有很多的东西需要我们去探索!路漫漫其修远兮!