前言

在 Android 开发中经常会遇到需要打包多个架构的包,还需要另外打一个 universal 的包,咱还需要在构建的同时为不同的架构输出不同的文件名,那么在 Flutter 上应该怎么做呢?

网上搜了一番发现许多信息都是过时的版本上的方法了,于是记录一下目前最新版本的方法,以及提供一个方便自己控制的使用 Dart 来构建项目的方法。

下面先贴一下咱的版本,避免版本不对而造成没必要的踩坑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ .\android\gradlew.bat -v

------------------------------------------------------------
Gradle 7.5
------------------------------------------------------------

Build time: 2022-07-14 12:48:15 UTC
Revision: c7db7b958189ad2b0c1472b6fe663e6d654a5103

Kotlin: 1.6.21
Groovy: 3.0.10
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 1.8.0_241 (Oracle Corporation 25.241-b07)
OS: Windows 10 10.0 amd64
1
2
3
4
5
$ flutter --version
Flutter 3.7.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 62bd79521d (6 months ago) • 2023-03-30 10:59:36 -0700
Engine • revision ec975089ac
Tools • Dart 2.19.6 • DevTools 2.20.1

其实应该主要关注一下 Gradle 和 Groovy 版本就可以了,Flutter 版本有可能会影响新建项目的默认 Gradle,不过归根结底只要 Gradle 和 Groovy 还是兼容的版本,本文的方法理论上就可行。

Gradle 构建

要用 Gradle 构建,只需要在模块级别的 build.gradle 下的 android 中添加下面的代码即可:(android 内其他不相关部分已省略)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
android {
// ...

// split apk for every abi
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk true
}
}

println("Gradle $gradle.gradleVersion")
println("Apk output:")
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "YourAppName-v${defaultConfig.version}-${variant.buildType.name}.apk"
}
}
}

上面的例子是启用了分架构打包,并且更改了输出的文件名,这样就可以在输出文件名里带上你的 App 名称了,而不是 Flutter 项目中默认的 app(因为 Gradle 打包默认输出名是模块名称)。可以根据自己的需求修改 outputFileName ,也可以编写一些逻辑代码来调整输出的名称。

不过在上面的代码里,分架构打包和输出更名实际上不能一起使用!原因当然是不同的架构下打包的名称都一样,如果需要为不同架构指定不同名称的话,将下面的代码加入 variant.outputs.all 里:

1
2
3
4
5
6
def abi = ""
// universal building or split building
if (output.getFilters() != null && output.getFilters().size() > 0) {
abi = "-" + output.getFilters().get(0).getIdentifier()
}
outputFileName = "YourAppName-v${defaultConfig.versionName}${abi}-${variant.buildType.name}.apk"

这里就为不同的架构指定了不同的名称,同样可以根据自己需求修改。不过众所周知,Flutter 构建的文件是在 build\app\outputs\flutter-apk 下的,但上面的方法永远只会生成在 build\app\outputs\apk\release,不过你 duck 放心,因为它们里面的 apk 是一样的……真的!特地算过哈希了啦(

Dart 构建

其实咱不怎么懂 Gradle,在配置的时候还是挺难受的,但是构建这个过程本质上不就是用另一个程序帮咱们调用编译器嘛,所以突发奇想用 Dart 来构建,刚开始还觉得似乎有点不合适,但仔细想想 JS 现在就是用 JS 来构建自己(虽然因为性能问题前端界的工具链已经在被 native 语言洗牌了)。而在 Dart 界甚至还有用 Dart 编写 Dart 的 codegen,这样看来就「合适得不得了」了 23333,更何况能用自己更熟悉的语言来构建显然更方便。下面就会讲如何在不动 Gradle 配置的情况下用 Dart 构建并实现上面的两个需求。

构建

对于分架构构建,其实是可以在不使用 Gradle 的情况下实现的,只需要运行 flutter build apk --release --split-per-abi 即可,但它并不会打包 universal 包。所以如果需要 universal 包的话,还需要运行 flutter build apk --release

对于使用 Dart 执行一个命令行,可以使用 Process,拿到 Process 对象后,可以读取它的 stdout 以及 exitCode 等。需要注意的是如果不读取 stdout 的话,程序就会一直等待。下面是咱封装的一个执行命令行函数,它不关心程序的正常输出,但如果程序非正常退出的话,会将 stderr 中的内容打印出来并退出程序。

1
2
3
4
5
6
7
8
9
Future<void> _execCmd(String cmd) async {
final process = await Process.start(cmd, [], runInShell: true);
process.stdout.listen((event) {});
final exitCode = await process.exitCode;
if (exitCode != 0) {
await process.stderr.pipe(stderr);
throw Exception();
}
}

不过上面的代码有一个小小的问题,就是中文会乱码,如果你有中文输出的需求的话,可以使用下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Future<String> _readSystemOutput(Stream<List<int>> output) async {
final sb = StringBuffer();
await for (final str in systemEncoding.decoder.bind(output)) {
sb.write(str);
}
return sb.toString();
}

Future<void> _execCmd(String cmd) async {
final process = await Process.start(cmd, [], runInShell: true);
process.stdout.listen((event) {});
final exitCode = await process.exitCode;
if (exitCode != 0) {
throw Exception(_readSystemOutput(process.stderr));
}
}

因为 Process文档中提到:

1
2
3
4
5
/// The encoding used for decoding `stdout` and `stderr` into text is
/// controlled through [stdoutEncoding] and [stderrEncoding]. The
/// default encoding is [systemEncoding]. If `null` is used no
/// decoding will happen and the [ProcessResult] will hold binary
/// data.

因此需要使用 systemEncoding 来解码一下~

有了这个工具函数之后,咱们就只需要调用 flutter 命令就可以啦:

1
2
3
4
5
6
7
void main() async {
await _execCmd('flutter build apk --release --split-per-abi');

// if you have configured split abi and `universalApk true` in Gradle,
// you can delete next line to build faster.
await _execCmd('flutter build apk --release');
}

构建完成后就可以根据自己的需求给输出的文件更名了,所有的逻辑都跟直接编写一个普通的 Dart 程序是一样的。

获取版本

如果需要在文件名带上应用的版本号,那要怎么办呢?至少架构是在 Flutter 构建完成之后就可以从文件名中获取到的,但 Flutter 可没有告诉咱们构建的应用版本号。不过其实版本号是咱们告诉 Flutter 的!没想到吧 想想在哪里告诉 Flutter 的呢?没错,在 pubspec.yaml 里,所以咱们直接从这个 yaml 文件里读取版本号进行处理就可以啦。

需要注意的是 Flutter 的版本字段可以有一个 +,加号前的是 versionName,加号后的是 versionCode,注意区分!因此这里需要一点小处理,最终的工具函数如下:(需要安装 yaml 这个包)

1
2
3
4
5
6
7
8
9
10
11
String _getVersion(File yaml) {
final pubspec = loadYaml(yaml.readAsStringSync());
final versionStr = pubspec['version'] as String;
String version;
if (versionStr.contains('+')) {
version = versionStr.split('+')[0];
} else {
version = versionStr;
}
return version;
}