前言 在 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 { 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 = "" 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
的文档 中提到:
因此需要使用 systemEncoding
来解码一下~
有了这个工具函数之后,咱们就只需要调用 flutter
命令就可以啦:
1 2 3 4 5 6 7 void main() async { await _execCmd('flutter build apk --release --split-per-abi' ); 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; }