Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

栏目: IOS · Android · 发布时间: 5年前

内容简介:对于构建Flutter类型应用,因其开发语言Dart、虚拟机、构建工具与平时我们开发Native应用不同且平台虚拟机也不支持,所以需要Flutter SDK来支持,如构建Android应用需要Android SDK一样,下载Flutter SDK通常有两种方式:

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

本文原作者: zhengxiaoyong ,原文发布于 Android 晓说 (Bytes_)。

一、简述

对于构建Flutter类型应用,因其开发语言Dart、虚拟机、构建 工具 与平时我们开发Native应用不同且平台虚拟机也不支持,所以需要Flutter SDK来支持,如构建Android应用需要Android SDK一样,下载Flutter SDK通常有两种方式:

  1. 在官网下载构建好的zip包,里面包含完整的Flutter基础Api,Dart VM,Dart SDK等

  2. 手动构建,Clone Flutter源码后,运行 flutter --packages get 或其它具有检测类型的命令如 builddoctor ,这时会自动构建和下载Dart SDK以及Flutter引擎产物

在团队多人协作开发下,这种依赖每个开发本地下载Flutter SDK的方式,不能保证Flutter SDK的版本一致性与自动化管理,在开发时如果Flutter SDK版本不一致,往往会出现Dart层Api兼容性或Flutter虚拟机不一致等问题,因为每个版本的Flutter都有各自对应的Flutter虚拟机,构建产物中会包含对应构建版本的虚拟机。Flutter工程的构建需要Flutter标准的工程结构目录和依赖于本地的Flutter环境,每个对应Flutter工程都有对应的Flutter SDK路径,Android在 local.properties 中,IOS在 Generated.xcconfig 中,这个路径会在Native工程本地依赖Flutter工程构建时读取,并从中获取引擎、资源和编译构建Flutter工程,而调用 flutter 命令时构建Flutter工程则会获取当前 flutter 命令所在的Flutter SDK路径,并从中获取引擎、资源和编译构建Flutter工程,所以 flutter 命令构建环境与Flutter工程中平台子工程的环境变量一定得保持一致,且这个环境变量是随 flutter 执行动态改变的,团队多人协作下这个得保证,在打包Flutter工程的正式版每个版本也应该有一个对应的Flutter构建版本,不管是本地打包还是在打包平台打包。

我们知道Flutter应用的工程结构都与Native应用工程结构不一样,不一致地方主要是Native工程是作为Flutter工程子工程,外层通过 Pub 进行依赖管理,这样通过依赖下来的 Flutter Plugin/Package 代码即可与多平台共享,在打包时Native子工程只打包工程代码与 Pub 所依赖库的平台代码,Flutter工程则通过 flutter_tools 打包 lib 目录下以及 Pub 所依赖库的Dart代码。回到正题,因工程结构的差异,如果基于现有的Native工程想使用Flutter来开发其中一个功能模块,一般来说混合开发至少得保证如下特点:

  1. 对Native工程无侵入

  2. 对Native工程零耦合

  3. 不影响Native工程的开发流程与打包流程

  4. 易本地调试

显然改变工程结构的方案可以直接忽略,官方也提供了一种Flutter本地依赖到现有Native的方案,不过这种方案不加改变优化而直接依赖的话,则会直接影响了其它无Flutter环境的开发同学的开发,影响开发流程,且打包平台也不支持这种依赖方式的打包。

再讲讲Flutter SDK,平时进行Flutter开发过程中,难免避免不了因Flutter SDK的Bug亦或是需要改Flutter SDK中平台链接的脚本代码导致直接改动或者定制Flutter SDK,这种方式虽然可以解决问题或定制化,不过极其不推荐,这种方式对后续Flutter SDK的平滑升级极不友好,且带来更多的后期维护成本。

接下来,本文主要是介绍如何对上述问题解决与实现:

  1. Flutter SDK版本一致性与自动化管理

  2. 无侵入Flutter SDK源码进行BugFix或定制化

  3. Flutter混合开发组件化架构

  4. Flutter混合开发工程化架构

二、Flutter四种工程类型

Flutter工程中,通常有以下几种工程类型,下面分别简单概述下:
1. Flutter Application
标准的Flutter App工程,包含标准的Dart层与Native平台层
2. Flutter Module
Flutter组件工程,仅包含Dart层实现,Native平台层子工程为通过Flutter自动生成的隐藏工程
3. Flutter Plugin
Flutter平台插件工程,包含Dart层与Native平台层的实现
4. Flutter Package
Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget

三、Flutter工程Pub依赖管理

Flutter工程之间的依赖管理是通过 Pub 来管理的,依赖的产物是直接源码依赖,这种依赖方式和IOS中的Pod有点像,都可以进行依赖库版本号的区间限定与Git远程依赖等,其中具体声明依赖是在 pubspec.yaml 文件中,其中的依赖编写是基于 YAML 语法, YAML 是一个专门用来编写文件配置的语言,下面是一个通过Git地址远程依赖示例:

dependencies:
  uuid:
    git:
      url: git://github.com/Daegalus/dart-uuid.git
      ref: master

声明依赖后,通过运行 flutter packages get 命名,会从远程或本地拉取对应的依赖,同时会生成 pubspec.lock 文件,这个文件和IOS中的 Podfile.lock 极其相似,会在本地锁定当前依赖的库以及对应版本号,只有当执行 flutter packages upgrade 时,这时才会更新,同样 pubspec.lock 文件也需要作为版本管理文件提交到Git中, 而不应gitignore。

1. Pub依赖冲突处理

对于 Pub Pod 这种依赖管理工具对于发生冲突时处理冲突的能力与Android的 Gradle 依赖管理相比差了一大截,所以当同一个库发生版本冲突时,只能我们自己手动进行处理,而且随着开发规模的扩大,肯定会出现传递依赖的库之间的冲突。

Pub依赖冲突主要有两种:

  1. 当前依赖库的版本与当前的Dart SDK环境版本冲突

  2. 传递依赖时出现一个库版本不一致冲突

第一种会在 flutter packages get 时报错并提示为何出现冲突且最低需要的版本是多少,如下:

The current Dart SDK version is 2.1.0-dev.5.0.flutter-a2eb050044.

Because flutter_app depends on xml >=0.1.0 <3.0.1 which requires SDK version <2.0.0, version solving failed.                        
pub get failed (1)
这个可以直接根据提示进行依赖库的版本升级解决。

而第二种则比较复杂点,假如有A、B、C三个库,A和B都依赖C库,如果A的某个版本依赖的C和B版本依赖的C版本不一致,则会发生冲突,而如何解决这种冲突呢?有两种方式:

1、首先把A和B库的版本都设为 any 任意版本,如下:

dependencies:
    A: any
    B: any

此时再通过 flutter packages get 时,则不会提示有版本冲突报错,因为 Pub 已经自动选取了让C库版本一致的A、B库的版本号,此时打开同级目录下的 pubspec.lock 文件,搜索A、B两个库,则会有对应无冲突的版本号,最后再把这两个版本号分别替换掉 any 版本,这个版本冲突就解决了。

2、通过版本覆盖进行解决。

2. Pub依赖版本覆盖

Pub 依赖管理中,既然支持传递依赖,同样也提供了一种版本覆盖的方式,意为强制指定一个版本,这和Android中 Gradle force 有点相似,同样版本覆盖方式也可以用于解决冲突,如果知道某一个版本肯定不会冲突,则可直接通过版本覆盖方式解决:

dependency_overrides:
  A: 2.0.0

四、Flutter链接到Native工程原理

官方提供了一种本地依赖到现有的Native工程方式,具体可看 官方wiki:Flutter本地依赖 (https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps) ,这种方式太依赖于本地环境和侵入Native工程会影响其它开发同学,且打包平台不支持这种方式的打包,所以肯定得基于这种方式进行优化改造,这个后面再说,先说说Native两端本地依赖的原理。

1. Android

Android中本地依赖方式为:

  1. settings.gradle 中注入 include_flutter.groovy 脚本

  2. 在需要依赖的module中 build.gradle 添加 project(':flutter') 依赖

对于Android的本地依赖,主要是由 include_flutter.groovy flutter.gradle 这两个脚本负责Flutter的本地依赖和产物构建。

1. include_flutter.groovy

settings.gradle 中注入时,分别绑定了当前执行Gradle的上下文环境与执行 include_flutter.groovy 脚本,该脚本只做了下面三件事:

  1. include FlutterModule中的 .android/Flutter 工程

  2. include FlutterModule中的 .flutter-plugins 文件中包含的Flutter工程路径下的android module

  3. 配置所有工程的 build.gradle 配置执行阶段都依赖于 :flutter 工程,也即它最先执行配置阶段

其中.flutter-plugins文件,是根据当前依赖自动生成的,里面包含了当前Flutter工程所依赖(直接依赖和传递依赖)的Flutter子工程与绝对路径的K-V关系,子工程可能是一个Flutter Plugin或者是一个Flutter Package,下面是.flutter-plugins中的一段内容示例:

.flutter-plugins:

url_launcher=/Users/Sunzxyong/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-4.0.2/

2. flutter.gradle

该脚本位于Flutter SDK中,内容看起来很长,其实主要做了下面三件事:

  1. 选择符合对应架构的Flutter引擎(flutter.so)

  2. 解析上述 .flutter-plugins 文件,把对应的android module添加到Native工程的依赖中(上述的include其实为这步做准备)

  3. Hook mergeAssets/processResources Task,预先执行FlutterTask,调用 flutter 命令编译Dart层代码构建出 flutter_assets 产物,并拷贝到 assets 目录下

有了上述三步,则直接在Native工程中运行构建即可自动构建Flutter工程中的代码并自动拷贝产物到Native中。

2. IOS

IOS中本地依赖方式为:

  1. 在Podfile中通过 eval binding 特性注入 podhelper.rb 脚本,在pod install/update时会执行它

  2. 在IOS构建阶段 Build Phases 中注入构建时需要执行的 xcode_backend.sh 脚本

对于IOS的本地依赖,主要是由 podhelper.rb xcode_backend.sh 这两个脚本负责Flutter的Pod本地依赖和产物构建。

1. podhelper.rb

因Podfile是通过 ruby 语言写的,所以该脚本也是ruby脚本,该脚本在pod install/update时主要做了三件事:

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

  2. Pod本地源码依赖 .flutter-plugins 文件中包含的Flutter工程路径下的ios工程

  3. 在pod install执行完后 post_install 中,获取当前target工程对象,导入 Generated.xcconfig 配置,这些配置都为环境变量配置,主要为构建阶段 xcode_backend.sh 脚本执行做准备

上述事情即可保证Flutter工程以及传递依赖的都通过pod本地依赖进Native工程了,接下来就是构建了。

2. xcode_backend.sh

Shell 脚本位于Flutter SDK中,该脚本主要就做了两件事:

  1. 调用flutter命令编译构建出产物(App.framework、flutter_assets)

  2. 把产物(*.framework、flutter_assets)拷贝到对应XCode构建产物中,对应产物目录为: $HOME/Library/Developer/Xcode/DerivedData/${AppName}

上述两个静态库 *.framework 是拷贝到 ${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app/Frameworks" 目录下

flutter_assets拷贝到 ${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app" 目录下

在XCode工程中,对应的是在 ${AppName}/Products/${AppName}.app

五、Flutter与Native通信

Flutter与Native通信有三种方式,这里只简单介绍下:

  1. MethodChannel:方法调用

  2. EventChannel:事件监听

  3. BasicMessageChannel:消息传递

Flutter与Native通信都是双向通道,可以互相调用和消息传递。

接下来是本文的重点内容,上述主要是普及下Flutter工程上比较重要的内容以及为下面要讲做准备,当然还有打包模式、构建流程等就不放这里了,后面可以单独开一篇讲。

六、Flutter版本一致性与自动化管理

在团队多人协作开发模式下,Flutter SDK的版本一致性与自动化管理,这是个必须解决的问题,通过这个问题,我们回看Android中Gradle的版本管理模式:

Gradle的版本管理是通过包装器模式,每个Gradle项目都会对应一个Gradle构建版本,对应的Gradle版本在 gradle-wrapper.properties 配置文件中进行配置,如果执行构建时本地没有当前工程中对应的Gradle版本,则会自动下载所需的Gradle版本,而执行构建则是通过 ./gradlew 包装器模式进行执行,这样本地配置的全局Gradle环境与工程环境即可隔离开,对应的项目始终保持同一个Gradle版本的构建

这种包装器模式的版本管理方式,可与每台机器中全局配置的环境保持隔离,在团队多人协作下,也可保持同一个项目工程保持同一个构建版本。

所以,我们沿用Gradle版本管理思想,在每个Flutter工程(包含上述说的四种工程)的根目录加入三个文件:

wrapper/flutter-wrapper.properties
flutterw
flutterw.bat

加入后的项目结构则多了三个文件,如下

<img src="https://raw.githubusercontent.com/Sunzxyong/ImageRepository/master/flutterw.jpg" width="360"/>

上述 flutter-wrapper.properties 为当前工程Flutter SDK版本配置文件,内容为:

distributionUrl=https://github.com/flutter/flutter
flutterVersion=1.0.0

当然有需要可以再增加一些配置,目前这两个配置已经足够了,指定了Flutter的远程地址以及版本号,如果Clone Github上项目比较慢,也可以改为私有维护的镜像地址。

flutterw 为一个Shell脚本,内部对版本管理主要做的事情为:

  1. 读取配置的版本号,校验Flutter SDK版本,不存在则触发下载

  2. 更新Android中 local.properties 和IOS中 Generated.xcconfig 文件中Flutter SDK地址

  3. 最后把命令行传来的参数链接到Flutter SDK中的flutter进行执行

之后构建Flutter工程则用 flutterw 命令:

./flutterw build bundle

而不用本地全局配置的 flutter 命令,避免每个开发同学版本不一致问题,且这种方式对于新加入Flutter开发的同学来说,完全不需要自己手动下载Flutter SDK,只需执行一下 flutterw 任何命令,如 ./flutterw --version ,即可自动触发对应Flutter SDK的下载与安装,实现优雅的自动化管理,这种方式对打包平台来说也为支持Flutter工程的打包提供基础。

七、Flutter混合开发组件化架构

上述说的如果我们要利用Flutter来开发我们现有Native工程中的一个模块或功能,肯定得不能改变Native的工程结构以及不影响现有的开发流程,那么,以何种方式进行混合开发呢?

前面说到Flutter的四种工程模型,Flutter App我们可以直接忽略,因为这是一个开发全新的Flutter App工程,对于Flutter Module,官方提供的本地依赖便是使用Flutter Module依赖到Native App的,而对于Flutter工程来说, 构建Flutter工程 必须得有个 main.dart 主入口,恰好Flutter Module中也有主入口。

于是,我们进行组件划分,通过Flutter Module作为所有通过Flutter实现的模块或功能的聚合入口,通过它进行Flutter层到Nat ive层的 双向关联。而Flutter开发代码写在哪里呢?当然可以直接写在Flutter Module中,这没问题,而如果后续开发了多个模块、组件,我们的Dart代码总不可能全部写在Flutter Module中 lib/ 吧,如果在 lib/ 目录下再建立子目录进行模块区分,这不失为一种最简单的方式,不过这会带来一些问题,所有模块共用一个远程Git地址,首先在组件开发隔离上完全耦合了,其次各个模块组件没有单独的版本号或Tag,且后续模块组件的增多,带来更多的测试回归成本。

正确的组件化方式为一个组件有一个独立的远程Git地址管理,这样各个组件在发正式版时都有一个版本号和Tag,且在各个组件开发上完全隔离,后续组件的增多不影响其它组件,某个组件新增需求而不需回归其它组件,带来更低的测试成本。

前面提到 Flutter Plugin 可以有对应Dart层代码与平台层的实现,所以可以这样设计,一个组件对应一个 Flutter Plugin ,一个 Flutter Plugin 为一个完整的Flutter工程,有独立的Git地址,而这些组件之间不能互相依赖,保持零耦合,所以这些组件都在 业务层 ,可以叫做 业务组件 ,这些业务组件之间的通信和公共服务可以再划分一层 基础层 ,可以叫做 基础组件 ,所有业务组件依赖基础层,而 Flutter Module 作为聚合层依赖于所有 Flutter组件 ,这些Flutter工程之间的依赖正是通过 Pub 依赖进行管理的。

所以,综合上述,整体的组件化架构可以设计为:

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

业务组件与基础组件的定位

对于上面的基础组件比如还可以进行更细粒度的划分,不过不建议划分太多,对于与Native平台层的通信,每个业务组件对应一个 Channel ,当然内部还可以进行更细粒度的 Channel 进行划分,这个 Channel 主要是负责Native层服务的提供,让Flutter层消费。而对于Native层调用Flutter层的Api,应该尽可能少,需要调也只有出现一些值回调时。

因为Flutter的出现最本质的就是一次开发两端运行,而如果有太多这种依赖于平台层的实现,反而出现违背了,最后只是UI写了一份而已。对于平台层的实现也要尽量保持一个原则,即:

尽量让Native平台层成为服务层,让Flutter层成为消费层调用Native层的服务,即Dart调用Native的Api,这样当两端开发人员编写好一致基础的服务接口后,Flutter的开发人员即可平滑使用和开发。

而对于基础组件中的公共服务组件Dart Api层的设计,因为公共服务主要调用Native层的服务,在Flutter中提供公共的Dart Api,作为Native到Flutter的一个桥梁,对于Native的服务,会有很有多种,而对应Api的设计为一个dart文件对应一个种类的服务,整个公共服务组件提供一个统一个对外暴露的Dart,内部的细粒度的Dart实现通过 export 导入,这种设计思想正是Flutter官方Api的设计,即统一对外暴露的Dart为 common_service.dart

library common_service;

export 'network_plugin.dart';
export 'messager_plugin.dart';
...

而上层业务组件调用Api只需要import一个dart即可,这样对上层业务组件开发人员是透明的,上层不需要了解有哪些Api可用:

import 'package:common_service/common_service.dart';

八、Flutter混合开发工程化架构

基本组件化的架构我们搭建好了,接下来是如何让Flutter混合开发进行完整的工程化管理,我们都知道,对于官方的本地依赖这种方式,我们不能直接用,因为这会直接影响Native工程、开发流程与打包流程,所以我们得基于官方这种依赖方式进行优化改造,于是我们衍生出两种Flutter链接到Native工程的方式:

  1. 本地依赖(源码依赖)

  2. 远程依赖(产物依赖)

为什么要有这两种方式,首先本地依赖对于打包平台不支持,现有打包平台的环境,只能支持标准的Gradle工程结构进行打包,且本地依赖对于无需开发Flutter相关业务的同学来说是灾难性的,所以便有了远程依赖,远程依赖直接依赖于打包好的Flutter产物,Android通过Gradle依赖,IOS通过Pod远程依赖,这样对其它业务开发同学来说是透明的,他们无需关心Flutter也不需要知道Flutter是否存在。

对于这两种依赖模式的使用环境也各不一样。

1. 本地依赖

本地依赖主要用于需要进行Flutter开发的同学,通过在对应Native工程中配置文件配置是否打开本地Flutter Module依赖,以及配置链接的本地Flutter Module地址,这样Native工程即可自动依赖到本地的Flutter工程,整个过程是无缝的,同时本地依赖是通过源码进行依赖的,也可以很方便的进行Debug调试。

对于Android中配置文件为本地的

local.properties ,IOS中为本地新建的 local.xcconfig

,两个平台的配置属性保持一致:

FLUTTER_MODULE_LINK_ENABLE=true
FLUTTER_MODULE_LOCAL_LINK=/Users/Sunzxyong/FlutterProject/flutter_module

2. 远程依赖

远程依赖是把Flutter Module的构成产物发布到远程,然后在Native工程中远程依赖,这种依赖方式是默认的依赖方式,这样对其它开发同学来说是透明的,不影响开发流程和打包平台。

上述说到的两种依赖方式,接下来主要说怎么进行这两种依赖方式的工程化管理和定制化。

1. 无侵入Flutter SDK源码进行BugFix和定制化

Flutter SDK在使用时,不免会遇到一些Flutter SDK的问题或Bug,但这些问题通常是在各平台层的链接脚本中出现坑,而如果我们要兼容现有工程和扩展定制化功能,往往会直接修改Flutter SDK源码,这种侵入性的方式极不推荐,这对后续SDK的平滑升级会带来更多的成本。

通常出现Bug或需要定制化的脚本往往是和平台链接时相关的,当然排除需要修改 dart 层Api代码的情况下,这种只能更改源码了,不过这种出bug的几率还是比较小的,比较涉及到SDK的Api层面了。而大概率出现问题需要兼容或进行定制化的几个地方通常为下面几处:

$FLUTTER_SDK
$FLUTTER_SDK
$FLUTTER_MODULE
$FLUTTER_MODULE
$FLUTTER_MODULE
$FLUTTER_MODULE
$FLUTTER_MODULE
$FLUTTER_SDK

而我们需要兼容的Flutter SDK的问题和定制化的点有下面几项:

  1. Android:Flutter SDK中的Flutter引擎不支持 armeabi 架构

  2. Android:Flutter SDK中的 flutter.gradle 链接脚本不支持非 app 名称的Application工程

  3. Android:Flutter SDK中的 flutter.gradle 链接脚本本地依赖存在 flutter_shared 资源文件不拷贝Bug

  4. Android:解决上述几项需要代理 build.gradle 构建脚本,以及在 build.gradle 构建脚本中定制化我们的构建产物收集Task

  5. IOS:Flutter Module中自动生成的.ios中的 podhelper.rb ruby脚本使用了Pod中的 post_install 方法,导致Native工程不能使用或使用了的发生冲突,间接侵入了Native工程与耦合,限制性太强

  6. IOS:Flutter Module中自动生成的 Podfile 文件,需要添加我们自己私有的 Specs 仓库进行定制化

  7. IOS:解决 post_install 问题后,Flutter SDK中的 xcode_backend.sh 链接脚本环境变量的读取问题

为了实现无侵入Flutter SDK,对于上述的这些问题的解决,我们使用代理方式进行Bug的修改和定制化,下面是针对两个平台分别的实现策略。

1. Android

在Android平台上述问题和定制化的解决策略,对于 armeabi 架构的支持,我们可以通过脚本进行自动化,上面讲到 flutterw 的版本自动化管理,同样,我们在里面加段 armeabi 架构的支持脚本,这样做得好处是后续不需要支持了可以直接移除,通过调用 ./flutterw armeabi 即可自动添加 armeabi 架构的引擎。

对于Flutter SDK中的 flutter.gradle 链接脚本的问题兼容,不会直接在源码中进行更改,而是把它拷贝出来,命名为 flutter_proxy.gradle ,然后在代理脚本中进行问题的修复,主要修复点为 flutter_shared 的支持与 app 硬编码名称的兼容,如下:

        Task copySharedFlutterAssetsTask = project.tasks.create(name: "copySharedFlutterAssets${variant.name.capitalize()}", type: Copy) {
            from(project.zipTree(chosenFlutterJar))
            include 'assets/flutter_shared/*'
            into "src/${variant.name}"
        }

再让 copyFlutterAssetsTask 任务依赖于它,而 app 硬编码名称的兼容,则更简单了,通过在Native工程中 local.properties 配置Module名,再在 flutter_proxy.gradle 脚本中加入读取该属性代码:

        String appName = loadRootProjectProperty(project, "FLUTTER_APP_NAME", "app")
        Task mergeAssets = project.tasks.findByPath(":${appName}:merge${variant.name.capitalize()}Assets")

而对于 build.gradle 构建脚本的代理,我们可以通过在执行 Gradle 构建时,通过 -c 命令进行 settings.gradle 的代理,进而代理掉 build.gradle 和指定Module中的 build.gradle 脚本,如下:

cd .android
./gradlew assembleDebug -c ../script/proxy/settings.gradle

而通过代理的 settings.gradle 文件再进行 build.gradle 的代理:

getRootProject().buildFileName = 'build_proxy.gradle'
project(":flutter").buildFileName = "build_proxy.gradle"

其中代理的 Flutter/build.gradle 中的脚本apply会改为修复的Flutter SDK中的脚本代理:

apply from: "${project.projectDir.parentFile.parentFile.absolutePath}/script/proxy/flutter_proxy.gradle"

这样 .android 工程在构建时期可以完全由我们自主控制,包括加入一些产物收集插件、产物发布到远程插件等定制功能。

不过这种方式需要执行构建命令时手动指定代理脚本,对于本地依赖时Native自动构建来说,是不会指定的,所有基于这种方式,我们再优化一下,因为 Flutter Module .android .ios 工程是通过Flutter SDK内部模版自动生成的,只要执行 build|packages get 等命令都会自动生成,首先想到是更改Flutter SDK内部工程模版,在Flutter SDK的 packages/flutter_tools/templates 目录下,不过这与我们无侵入Flutter SDK违背了,所以不能选取这种方式。

回想我们的Flutter SDK版本一致性管理是通过 flutterw 脚本进行自动化的,而最终会链接调用到原生Flutter SDK中的命令,所以,我们可以在 flutterw 中加入脚本,用于在 .android .ios 工程生成后,进行内部脚本文件的替换,把 build.gradle settings.gradle 脚本内容直接替换为我们的代理脚本的内容,这样既不侵入Flutter SDK,在后续维护起来也方便,后续不需要这个功能了,只需要把这段脚本代码注释就好了,随即又恢复原生的构建脚本了, flutterw 脚本执行过程如下:

function main() {
        # ...
        link_flutter "$@"
        inject_proxy_build_script
        # ...
}

inject_proxy_build_script 这个Shell函数会把对应脚本进行我们的脚本替换掉,当前函数内部也有对应判断,因为 flutterw 主要用于Flutter SDK版本一致性管理,这里仅对Flutter Module工程生效。所以这种方式不管是在本地依赖构建下还是通过命令行构建都可以完美支持。

2. IOS

在IOS平台上述问题和定制化的解决策略,对于IOS主要是对 Podfile podhelper.rb 脚本进行支持,而对 Podfile 的支持,这个比较简单,在 Podfile 头部通过脚本注入我们自己私有的 Specs 仓库即可:

source 'https://***/XXSpecs.git'
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
...

这个工作同样在 flutterw 执行后进行兼容,后续不需要了可以直接注释,这个自动注入脚本也仅对Flutter Module工程生效。

podhelper.rb 脚本的兼容,主要是在进行本地依赖时,内部已经用了 post_install 函数,该函是在pod install后执行,这会与Native已经使用了该函数的发生冲突并报错,所以我们通过 flutterw 脚本的执行后默认注释掉该脚本中的 post_install 使用处,但是肯定不能平白无故注释掉,我们要了解这段的作用,其实就是设置环境变量,为后续 xcode_backend.sh 脚本的构建执行做准备,而注释掉怎么用另外一种方式恢复环境变量的设置这个后面再讲,注释后 podhelper.rb 脚本代码片段为:

# 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

最终在 flutterw 自动支持上述处理脚本执行流程为:

function main() {
                # ...
                link_flutter "$@"
        # ...
        podfile_support
        podhelper_support
        collect_ios_product "$@"
}

函数内部判断仅针对Flutter Module工程生效,毕竟其它Flutter Plugin工程不需要这种处理。

2. 本地依赖无侵入流程

我们要做到只通过一个属性配置文件,在配置文件中通过配置开发来打开或关闭本地的Flutter Module链接依赖,只按官方的依赖方式肯定是不行的,不管是Android还是IOS,都会直接侵入Native工程,影响其它无Flutter环境同学的开发且影响打包平台上的打包。所以,肯定得做优化,我们在官方这种依赖方式中加一层,作为 代理层 ,而代理层主要做的工作是判断本地是否有对应的属性配置文件且属性值是否符合本地依赖Flutter Module的条件,如果是则进行本地Flutter Module的依赖,如果不是则Return掉,默认不做任何处理。

所以通过这种代理方式即不影响Native工程原先的开发流程,对其它业务开发同学和打包平台也是透明的。

对于代理层的实现,Android与IOS平台各不一样。

1. Android

Android是通过一个 Gradle脚本 进行自动管理的,这个Gradle脚本主要在 settings.gradle build.gradle 中做 local.properties 配置文件的属性值校验,决定是否开启本地Flutter Module链接的。

2. IOS

IOS则较为复杂一些,因为涉及到 Podfile 中的ruby执行脚本代理与 Build Phases 时期的Shell脚本代理,所以得写两种类型的代理脚本:Ruby和Shell,代理脚本的最终执行还是会调用被代理的脚本,只是在调用前做一层包装逻辑判断。而IOS中本身没有本地配置文件,所以我们新建一个IOS的本地配置文件为 local.xcconfig ,这个配置文件不随版本进行管理,会gitignore掉,于是,在IOS中 Podfile 最终调用的脚本是:

eval(File.read(File.join('./', 'FlutterSupport', 'podhelper_proxy.rb')), binding)

而在 Build Phases 调用的是:

chmod +x "${SRCROOT}/FlutterSupport/xcode_backend_proxy.sh"
"${SRCROOT}/FlutterSupport/xcode_backend_proxy.sh" flutterBuild

而刚刚上面说到的 podhelper.rb 脚本中 post_install 函数被注释掉后怎么用另一种方式进行替换,我们知道这段函数主要就是提供在IOS构建阶段时执行 xcode_backend.sh 的环境变量的,比如会获取 FLUTTER_ROOT 等属性值,这些环境变量由Flutter Module中 Generated.xcconfig 来提供,而如果我们把这个文件的内容通过脚本拷贝到IOS工程下对应构建配置的xcconfig中,如 debug.xcconfig release.xcconfig ,这种方式可行,不过会侵入Native工程,导致Native工程中多了这些变量,而且不优雅,我们要做到的是保证无侵入性。

既然我们已经通过代理脚本进行代理,那么这些环境变量我们完全可以获取出来,通过Shell脚本的特性,子Shell会继承于父Shell中 export 的环境变量值,所以,在代理Shell脚本中再加段下面代码:

function export_xcconfig() {
    export ENABLE_BITCODE=NO
    if [[ $# != 0 ]]; then
        local g_xcconfig=$1/.ios/Flutter/Generated.xcconfig
        if [[ -f "$g_xcconfig" ]]; then
            # no piping.
            while read -r line
            do
                  if [[ ! "$line" =~ ^// ]]; then
                          export "$line"
                fi
            done < $g_xcconfig
        fi
    fi
}

其中注意不能使用管道,管道会在另外一个Shell进程。

3. 远程依赖产物打包流程

Flutter的远程产物依赖,Android是通过 Aar 依赖,IOS是通过 .a .framework 静态库进行依赖,要进行这些远程依赖很简单,关键是如何打包获取这些依赖的产物以及上传到远程,因为按照现有组件化的打包,除了聚合层Flutter Module中有对应的 flutter-debug.aar App.framework flutter_assets 等产物的生成,其中业务组件和基础组件中,也有对应的打包产物,这些打包产物会对应各自平台打包不同类型产物,Android还是 aar ,而IOS则是 .a 静态库了,下面就分别讲下Android与IOS的打包流程。

1. Android

Android的打包比较简单,通过在Flutter Module中的 .android 子工程下执行 ./gradlew assembleRelease ,则会在对应Flutter中Android子工程的build目录下输出对应 aar 产物,而重点是怎么获取依赖的各组件(Flutter Plugin)中的产物,则是通过 .flutter-plugins 文件,该文件是在 packages get 时自动生成的,里面包含了该Flutter工程通过 Pub 所依赖的库,我们可以解析这个文件,来获取对应依赖库的产物。

2. IOS

IOS上的打包相比Android来说更复杂一些,我们借助 .ios/Runner 来打包出静态库等产物,所以还需要设置签名,通过在Flutter Module中直接执行 ./flutterw build ios --release ,该命令会自动执行 pod install ,所以我们不必再单独执行它,IOS中构建出的产物获取也相对繁琐些,除了获取Flutter的相关产物,还需要获取所依赖的各组件的静态库以及头文件,需要获取的产物如下:

Flutter.framework

App.framework

FlutterPluginRegistrant

flutter_assets

所有依赖的Plugin的 .a 静态库以及头文件

其中 Flutter.framework 为Flutter引擎,类似Android中的 flutter.so ,而 App.framework 则是Flutter中Dart编译后的产物(Debug模式下它仅为一个空壳,具体Dart代码在flutter_assets中,Release模式下为编译后的机器指令),FlutterPluginRegistrant是所有插件Channel的注册表,也是自动生成的, flutter_assets 含字体等资源,剩下一些 .a 静态库则是各组件在IOS平台层的实现了。

而收集IOS产物除了在 .ios/Flutter 目录下收集 *.framework 静态库和 flutter_assets 外,剩下的就是收集 .a 静态库以及对应的头文件了,而这些产物则是在构建 Runner 工程后,在Flutter Module下的。

build/ios/$variant-iphoneos

目录下, variant 对应所构建变体名,我们还是通过解析 .flutter-plugins 文件,来获取对应所依赖Flutter插件的名称,进而在上述的输出目录下找到对应的 .a 静态库,但是对应的头文件而不在对应 .a 静态库目录下,所以对于头文件单独获取,因为解析了 .flutter-plugins 获取到了KV键值对,对应的V则是该Flutter插件工程地址,所以头文件我们从里面获取。

最后还需要获取 FlutterPluginRegistrant 注册表的静态库以及头文件。

3. 产物收集与传递依赖

对于通过Flutter Module聚合层构建出来的产物,我们进行收集后再聚合到单独的产物输出目录下,当然这一切都是通过脚本自动做掉的。

在Android上,通过Gradle插件Hook assembleTask

        collectAarTask.dependsOn assembleTask
        assembleTask.finalizedBy collectAarTask

这样当执行完 ./gradlew assemble${variant} 命令后则会自动进行产物收集。

在IOS上,通过 flutterw 脚本,在构建完后判断构建命令是否是IOS构建命令,进而自动收集构建后的产物:

function collect_ios_product() {
    if [[ $# != 0 && $# > 2 ]]; then
        if [[ "$1" = "build" && "$2" = "ios" ]]; then
                # do collect...
        fi
    fi  
}        

对应 .a 静态库和头文件的收集关键脚本代码如下:

        while read -r line
        do
            if [[ ! "$line" =~ ^// && ! "$line" =~ ^# ]]; then
                array=(${line//=/ })
                local library=$product_dir/${array[0]}/lib${array[0]}.a
                if [[ -f "$library" ]]; then
                    local plugin=$dest_dir/plugins/${array[0]}
                    rm -rf $plugin
                    mkdir -p $plugin
                    cp -f $library $plugin
                    local classes=${array[1]}ios/Classes
                    for header in `find "$classes" -name *.h`; do
                           cp -f $header $plugin
                    done
            else
                echo "The static library $library do not exist!"
                fi
            fi
        done < $flutter_plugins

如下是Android与IOS的打包后产物收集后的目录结构如下:

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

对于传递依赖的支持,我们知道单独的 aar 文件以及通过podspec声明这些静态库产物,是会丢失传递依赖的,丢失传递依赖可能导致我们Native工程中没有使用到的一些三方库,而Flutter工程中引用了,然后App运行Crash,而保证传递依赖的方式,则是Android发布到远程Maven,最后通过远程依赖,上述产物只是本地依赖,IOS则是解析所有Flutter插件中的 podspec 文件,把它还原为JSON格式,通过解析 dependencies 对象,获取对应的依赖库命名以及版本号,最后在IOS远程产物的 podspec 配置文件中添加这些依赖。

对于IOS的远程依赖,我们知道单独建一个独立的Git仓库就可以解决,通过配置好 podspec ,即可在IOS Native端进行远程依赖,但是像 Flutter.framework App.framework 这种大文件,如果直接上传到Git仓库中有些不太友好,比如可以上传到CDN中,然后通过 podspec spec.prepare_command 特性,在pod库安装时候预先执行一段脚本把这两个产物拉下来,对于目前来说,可以先传到Git中,这样比较直观与可控,便于版本的管理。

4. IOS远程产物的依赖以及x86的兼容

对于IOS远程产物的依 赖方式,因我们需要同时方便于本地调试以及远程打包,即pod库需要同时包含x86以及arm的产物,在编 译构建时自动选择需要的远程产物类型进行依赖,所以IOS最终的远程依赖pod仓库结构如下所示:

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

在IOS工程中的依赖方式则是通过构建类型进行依赖,对应代码为:
<code>  if $flutter_dependency_mode == 0 then</code><code>      puts "======> Flutter Local Dependency."</code><code>      eval(File.read(File.join('./', 'Flutter', 'flutter_podhelper.rb')), binding)</code><code> else</code><code>      puts "======> Flutter Remote Dependency."</code><code>      pod 'FlutterRepo-Debug', '1.0', :configurations => ['Debug', 'Dev']</code><code>      pod 'FlutterRepo-Release', '1.0', :configurations => ['Release']</code><code> end</code>
以上解决了本地打包以及远程打包的问题,因为Flutter的Release产物并不支持x86,这样避免了如果只依赖Release一种产物会导致IOS工程在模拟器上运行不起来问题。
对于上述Debug构建类型依赖的产物,并非是Flutter的 --debug 编译产生的产物,而是通过 build ios --debug --simulator 的模拟器产物(x86)与 build ios --release 的Release产物进行合成的,这样一来本地构建的Debug包既支持真机也支持模拟器,同时支持x86与arm,这样不影响端侧非Flutter业务同学的正常开发。
x86与arm的产物合并,可以通过 lipo 命令进行,如下:
lipo -create $app_framework_1 $app_framework_2 -output $merge_framework/App.framework/App      

5. Flutter混合开发工程化整体流程

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

九、后序
对于现有工程使用Flutter进行混合开发,坑点还是有的,比如性能、页面栈管理等方面,只是目前还未踩到,加上目前Flutter上一些基础库不成熟,对于项目内的重要页面以及动态化强度比较高的页面,目前还是不建议使用Flutter进行开发,如果要使用也须做好降级方案,相反可以使用稍微轻量级点的页面,且在设计时对于Flutter与Native层的通信,应该让Flutter作为消费层消费Native层提供的服务,Native端应做尽量少的改动,最好仅增加一处页面路由的拦截器代码,在拦截器中通过Native与Flutter页面的映射关系,把Native的页面路由跳转替换为Flutter页面路由,这样可以保证Native与Flutter的零耦合

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk

"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等,我们希望以此促进中国开发者彼此间的相互启发、学习和技术交流。

  点击屏末  |   | 了解更多 " 开发者说 · DTalk" 活动详情与参与方式

Flutter 混合开发组件化与工程化架构 | 开发者说·DTalk


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PHP高级编程

PHP高级编程

Jesus Castagnetto / 机械工业出版社 / 2001-3 / 78.00元

本书介绍PHP的基本知识与高级特一起来看看 《PHP高级编程》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具