有赞技术 有赞coder 



一、背景

目前准备试水 Flutter,但是多数 native 开发是不了解 Flutter,因此需要设计一种比较“舒服”的集成方式。


二、混编方案

2.1 方案考量

  • 如果直接采用 Flutter 工程结构来作为日常开发,那这部分 Native 开发也需要配置Flutter环境, 相当程度的了解 Flutter 一些技术,成本比较大;

  • 同时如果工程耦合,对于开发过程也是很难受的。

基于以上两点思考,针对 Android iOS 有如下方案:

2.2 Android

先看下官方的集成方式:

  1. # setting.gradle

  2. setBinding(new Binding([gradle: this]))

  3. evaluate(new File(


  4.        '../managementcenter/.android/include_flutter.groovy'

  5. ))

# build.gradledependencies {  implementation project(':flutter')  ...........}

这种方式使得工程强耦合,虽然便于开发调试,但是违背了第一点,大多数 native 同学都需要配置 Flutter 环境, 成本很大。

2.3 iOS

2.3.1 官方 iOS 混编方案简介

  • 在native项目 Podfile中通过 eval binding特性注入 podhelper.rb脚本,在 pod install/update 时执行此脚本,脚本主要处理:

  • Pod本地依赖Flutter引擎(Flutter.framework) 与Flutter插件注册表(FlutterPluginRegistrant)

  • Flutter插件通过 flutter packagesget指令安装后生成的 .flutter-plugins文件解析,然后Pod本地依赖所有的插件

  • 在pod install执行完的钩子 post_install中,获取当前pod target工程对象,导入 Generated.xcconfig配置,其中都为环境变量的配置,主要为后续的 xcode_backend.sh脚本执行做准备

  • 在构建阶段 BuildPhases中注入构建是需要执行的 xcode_backend.sh脚本,脚本主要完成Flutter产物的构建并将其添加到对应的native工程中去,后续会进一步介绍此脚本

2.3.2 优点

  • 无缝开发,配置好后就可以只在 Flutter 工程内进行业务开发,无缝同步到 native 工程中

  • 不需要单独拆分组件,免去管理组件的版本及发布成本

2.3.3 缺点

  • 非常耦合,需要修改原有 native 工程配置,需要添加特定脚本去编译 Flutter

  • 需要修改原有 pod 的 xcconfig 配置

  • 所有团队开发成员都必须要配置 Flutter 开发环境才能编译成功

2.4 小结

基于以上思考,同时考虑到某个 Flutter 业务模块可能会引入到不同的 App 中,同时考虑到某个业务实现方式方面的解耦(某个业务可能用 native, flutter, weex 开发),有以下方案(中间产物库每个 Flutter 业务模块都是独立的):

Android:


iOS:



三、Flutter产物结构

3.1 Android


3.2 iOS


关于编译模式了解更多可参考查看 Flutter 的编译模式


四、Flutter 产物收集

4.1 Android

在 Android 端集成 Flutter 较为简单,只需要获取到上文所讲的 Flutter 产物即 aar 文件。但是由于插件文件散落每次获取比较麻烦所以目前简单用脚本收集。

脚本收集主要是依靠项目里 .flutter_plugins 文件,该文件会记录 flutter 项目中引用的插件名以及本地路径等,因此可以通过该路径抓取插件的 aar 文件。

  1. from shutil import copyfile

  2. import os

  3. import requests


  4. # 抓取文件类型

  5. BuildRelease = True

  6. aarType = "-release.aar" if BuildRelease else "-debug.aar"


  7. pluginFilePath = '../.flutter-plugins'


  8. # 当前项目的flutter.aar

  9. currentFlutterPath = '../.android/Flutter/build/outputs/aar/'


  10. # 输出地址

  11. outputFilePath = os.path.abspath('flutter_aar.py').replace("flutter_aar.py", "aars/")


  12. endPath = 'android/build/outputs/aar/'


  13. def collect_aar(plugins):

  14.    all_collection_success = True

  15.    if os.path.exists(outputFilePath):

  16.        print('copy aar to: ' + outputFilePath)

  17.    else:

  18.        print('target path: ' + outputFilePath + ' not exist')

  19.        os.makedirs(outputFilePath)

  20.        print('create target path: ' + outputFilePath)


  21.    for key, value in plugins.items():

  22.        aar_path = value + key + aarType

  23.        try:

  24.            copyfile(aar_path, outputFilePath + key + aarType)

  25.            print('copy flutter aar success at path: ' + aar_path)

  26.        except IOError:

  27.            all_collection_success = False

  28.            print('copy flutter aar error at path: ' + aar_path)

  29.    pass


  30. file_object = open(pluginFilePath, 'r')

  31. try:

  32.    plugin_map = {}

  33.    for line in file_object:

  34.        array = line.split('=')

  35.        plugin_map[array[0]] = array[1].replace('\n', '') + endPath


  36.    plugin_map['flutter'] = currentFlutterPath

  37.    collect_aar(plugin_map)

  38. finally:

  39.    file_object.close()

目前该python脚本只抓取 Release 的 aar 文件,如果需要获取 debug 的可以手动修改:

BuildRelease = False

执行抓取脚本 ./flutter_aar.sh

  1. #!/usr/bin/env bash


  2. cd ..

  3. cd .android

  4. echo "start clean"

  5. ./gradlew clean

  6. echo "start assembleRelease"

  7. ./gradlew assembleRelease

  8. cd ..

  9. cd android-build


  10. echo "clean old aar file"

  11. rm -rf aars


  12. echo "start copy aar file"

  13. # 只抓取release

  14. python flutter_aar.py


  15. echo "copy aar file finish"

脚本执行完 Flutter 产物 aar 文件统一生成在根目录下 android-build 文件夹中。


4.2 iOS

通过查看 Flutter 编译脚本 xcode_backend.sh 和测试单独引入编译产物,发现其实 只要拥有Flutter的编译产物,宿主项目就可以接入Flutter的功能。

4.2.1 脚本简单分析

  • engine/Flutter.framework Flutter 核心库拷贝 -> Flutter.framework

  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


  • debug 模式下 Dart 业务代码编译(JIT) -> App.framework

  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" -)"


  • 非 debug 模式下 Dart 业务代码编译(AOT) -> App.framework

  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}


  • 资源文件等打包 -> flutter_assets

  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}/App.framework/flutter_assets"                     ${precompilation_flag}      ${local_engine_flag}      ${track_widget_creation_flag}


4.2.2 方案分析

设计


  • 插件统一编译成.a库,添加对应头文件

  • App.framework 及 engine/Flutter.framework 添加

  • 目前初期 demo 将上述生成的产物统一放入到私有库当中,然后 native 宿主工程 pod 依赖此库,只需要在使用 Flutter 代码的地方 import 对应的头文件即可正常使用

脚本编写
  1.  echo "==b清理flutter历史编译==="

  2.  flutter clean


  3.  echo "===重新生成plugin索引==="

  4.  flutter packages get


  5.  echo "===生成App.framework和flutter_assets==="

  6.  flutter build ios --debug


  7.  echo "===获取所有plugin并找到头文件==="

  8.  while read -r line

  9.  do

  10.      if [[ ! "$line" =~ ^// ]]; then

  11.          array=(${line//=/ })

  12.          plugin_name=${array[0]}

  13.          cd .ios/Pods

  14.          echo "生成lib${plugin_name}.a..."

  15.          /usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' -target ${plugin_name} BUILD_DIR=../../build/ios -sdk iphoneos -quiet

  16.          /usr/bin/env xcrun xcodebuild build -configuration Debug ARCHS='x86_64' -target ${plugin_name} BUILD_DIR=../../build/ios -sdk iphonesimulator -quiet

  17.          echo "合并lib${plugin_name}.a..."

  18.          lipo -create "../../build/ios/Debug-iphonesimulator/${plugin_name}/lib${plugin_name}.a" "../../build/ios/Release-iphoneos/${plugin_name}/lib${plugin_name}.a" -o "../../product/lib${plugin_name}.a"

  19.          echo "复制头文件"

  20.          classes=${array[1]}ios/Classes

  21.          for header in `find "$classes" -name *.h`; do

  22.              cp -f $header "../../product/"

  23.          done

  24.      else

  25.          echo "读取文件出错"

  26.      fi

  27.  done < .flutter-plugins


  28.  echo "===生成注册入口的二进制库文件==="

  29.  for reg_enter_name in "FlutterPluginRegistrant"

  30.  do

  31.      echo "生成libFlutterPluginRegistrant.a..."

  32.      /usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' -target FlutterPluginRegistrant BUILD_DIR=../../build/ios -sdk iphoneos

  33.      /usr/bin/env xcrun xcodebuild build -configuration Debug ARCHS='x86_64' -target FlutterPluginRegistrant BUILD_DIR=../../build/ios -sdk iphonesimulator

  34.      echo "合并libFlutterPluginRegistrant.a..."

  35.      lipo -create "../../build/ios/Debug-iphonesimulator/FlutterPluginRegistrant/lib$FlutterPluginRegistrant.a" "../../build/ios/Release-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a" -o "../../product/libFlutterPluginRegistrant.a"

  36.      echo "复制头文件"

  37.      classes="../Flutter/FlutterPluginRegistrant/Classes"

  38.      for header in `find "$classes" -name *.h`; do

  39.          cp -f $header "../../product/"

  40.      done

  41.  done

  • 后续规划

  • 脚本优化,添加自动pod库检测及上传

  • App.framework/Flutter.framework 体积太大,放到git仓库不太友好,考虑后续上传到CDN,然后在pod安装的时候预先执行脚本把两个产物拉下来


五、Flutter产物上传

5.1 Android

上面产物搜集完成后,需要上传 maven 仓库,方便集成以及版本控制:

  1. apply plugin: 'youzan.maven.upload'


  2. zanMavenUpload {

  3.    version = '0.0.2'

  4.    childGroup = "flutter-apub"

  5. }


  6. uploadItems {

  7.    "fluttertoast" {

  8.        targetFile = file('../../android-build/aars/fluttertoast-release.aar')

  9.    }


  10.    "image_picker" {

  11.        targetFile = file('../../android-build/aars/image_picker-release.aar')

  12.    }


  13.    ............

  14. }

因此引用链如下:


  • Android


  • iOS


六、总结

以上比较全面的描述了有赞的 Flutter 混编方案,目前有赞已经在内部使用的App上使用 Flutter 开发了一些页面作为试点。后续会考虑在线上 App 试点,目前正在进行 Flutter 基础库的搭建,之后会专门有文章分享。


相关阅读:

官方 iOS 混编方案:https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps

Flutter的编译模式:https://stephenwzl.github.io/2018/07/30/flutter-compile-mode/


©著作权归作者所有:来自51CTO博客作者mob604756f318e7的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. AFM:具有优异储钾性能的氮/磷共掺杂空心多孔碗状碳负极
  2. Wiley人物专访—马里兰大学莫一非教授
  3. 在 Delphi 中使用微软全文翻译的小例子
  4. 【论文翻译】为什么网络需要自动驾驶?(IBN提高篇)
  5. ODL分布式集群底层实现分析
  6. 从分层角度HACK网络
  7. 深度解析vBRAS演进之路
  8. CSS样式规则-CSS结构的特点
  9. Python实现二叉树的三种深度遍历方法!

随机推荐

  1. golang快不快
  2. golang 产生随机数有多少种方法
  3. golang nil什么意思
  4. golang byte是什么
  5. golang 断言是什么
  6. golang lua怎么用
  7. golang gin怎么安装
  8. golang中的defer关键字什么时候生效
  9. golang leaf用的多吗
  10. golang gf怎么使用