likes
comments
collection
share

Flutter Android 混合项目在打渠道包中引发的问题

作者站长头像
站长
· 阅读数 10

1. 问题的环境和具体的问题

最近在尝试将 Flutter 接入到原生的 Android 项目中,使用 Flutter 和 Android 混合开发的模式,对于一些纯展示性的页面准备用 Flutter 去开发,需要调用原生 API 的还是用原生去实现。在接入的过程中都没有遇到什么问题,最后提测我打渠道包的时候噩梦来了,打出来的渠道包一启动就崩溃了。

先说下我选择混合项目的结构是如何的,Flutter 作为一个单独的项目创建有自己单独的 Git 仓库,Android 原生有自己的 Git 仓库。项目需要将 Android 和 Flutter 项目 clone 到一个文件夹中,Flutter 相当于 Android 中的一个 Module 去运行。选择这种方式是因为 Flutter 项目之后也可以让 iOS 去使用,我们修改提交代码相对也比较方便。最后有个关键就是我使用的 walle 打渠道包,才会出现之后的问题。

再说我遇到的问题,既然崩溃马上去找崩溃日志,崩溃内容如下:

Revision: '0'
ABI: 'arm'
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: '[FATAL:flutter/shell/common/shell.cc(139)] Check failed: vm. Must be able to initialize the VM.'
r0  00000000  r1  000012c7  r2  00000006  r3  ffb0da08
r4  ffb0da1c  r5  ffb0da00  r6  000012c7  r7  0000016b
r8  ffb0da08  r9  ffb0da18  r10 ffb0da38  r11 ffb0da28
ip  000012c7  sp  ffb0d9d8  lr  e9c06b0d  pc  e9c06b20
backtrace:
#00 pc 00062b20  /apex/com.android.runtime/lib/bionic/libc.so (abort+172) (BuildId: 41db4c4af737992d2f4f0c0e5090b769)

这里面有个最重要的信息就是 Check failed: vm. Must be able to initialize the VM,于是马上就去 Google 这个关键字,通过多个文章分析得到的结果是 flutter_assets 这个文件夹没有打包进 APK 的 assets 中。自己去查看打出来的包确实是这个问题。就用他们提供的方法去解决。

方法1:Android 工程的主 module 必须是 app 才可以,我的主 module 确实是 app,所以不是这块出现的问题。

方法2:Flutter 的 Issues#27028 [github.com/flutter/flu…] 有提到这个,需要在 fluuter module 的 build.gradle (flutter module/.android/Flutter/build.gradle) 中配置的渠道信息。我去试了下还是有问题。

2. 我的解决方案

看到有人分析修改 fluttersdk 的 flutter.gradle 可以解决他们对于的一些问题,我找到这个文件它的目录是 flutter\packages\flutter_tools\gradle 下的 flutter.gradle。可能我们的 fluttersdk 版本不一致导致这个文件的内容不同,我没办法根据他们的提示去做修改,只能自己看注释摸索了。最重要的一段代码我贴出来如下:

// Flutter host module project (Add-to-app).
        String hostAppProjectName = project.rootProject.hasProperty('flutter.hostAppProjectName') ? project.rootProject.property('flutter.hostAppProjectName') : "app"
        Project appProject = project.rootProject.findProject(":${hostAppProjectName}")
        assert appProject != null : "Project :${hostAppProjectName} doesn't exist. To custom the host app project name, set `org.gradle.project.flutter.hostAppProjectName=<project-name>` in gradle.properties."
        // Wait for the host app project configuration.
        appProject.afterEvaluate {
            assert appProject.android != null
            project.android.libraryVariants.all { libraryVariant ->
                Task copyFlutterAssetsTask
                appProject.android.applicationVariants.all { appProjectVariant ->
                    Task appAssembleTask = getAssembleTask(appProjectVariant)
                    if (!shouldConfigureFlutterTask(appAssembleTask)) {
                        println '日志1'
                        return
                    }
                    // Find a compatible application variant in the host app.
                    //
                    // For example, consider a host app that defines the following variants:
                    // | ----------------- | ----------------------------- |
                    // |   Build Variant   |   Flutter Equivalent Variant  |
                    // | ----------------- | ----------------------------- |
                    // |   freeRelease     |   release                      |
                    // |   freeDebug       |   debug                       |
                    // |   freeDevelop     |   debug                       |
                    // |   profile         |   profile                     |
                    // | ----------------- | ----------------------------- |
                    //
                    // This mapping is based on the following rules:
                    // 1. If the host app build variant name is `profile` then the equivalent
                    //    Flutter variant is `profile`.
                    // 2. If the host app build variant is debuggable
                    //    (e.g. `buildType.debuggable = true`), then the equivalent Flutter
                    //    variant is `debug`.
                    // 3. Otherwise, the equivalent Flutter variant is `release`.
                    String variantBuildMode = buildModeFor(libraryVariant.buildType)
                    if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
                        println '日志2'
                        return
                    }
                    if (copyFlutterAssetsTask == null) {
                        copyFlutterAssetsTask = addFlutterDeps(libraryVariant)
                    }
                    Task mergeAssets = project
                        .tasks
                        .findByPath(":${hostAppProjectName}:merge${appProjectVariant.name.capitalize()}Assets")
                    assert mergeAssets
                    mergeAssets.dependsOn(copyFlutterAssetsTask)
                }
            }
        }

可以看到这段代码就是用来 mergeAssets 的,我们的 assets 文件没有合并进去就是这块的一些代码没有执行。就加日志判断是不是提前 return 了,在所有 return 处加上日志。运行后打印了日志1,表示在日志1处 return 了,接下来我们就分析 shouldConfigureFlutterTask(appAssembleTask) 方法了,它的代码如下:

    // TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560
    /**
     * In AGP 4.0, the Android linter task depends on the JAR tasks that generate `libapp.so`.
     * When building APKs, this causes an issue where building release requires the debug JAR,
     * but Gradle won't build debug.
     *
     * To workaround this issue, only configure the JAR task that is required given the task
     * from the command line.
     *
     * The AGP team said that this issue is fixed in Gradle 7.0, which isn't released at the
     * time of adding this code. Once released, this can be removed. However, after updating to
     * AGP/Gradle 7.2.0/7.5, removing this hack still causes build failures. Futher
     * investigation necessary to remove this.
     *
     * Tested cases:
     * * `./gradlew assembleRelease`
     * * `./gradlew app:assembleRelease.`
     * * `./gradlew assemble{flavorName}Release`
     * * `./gradlew app:assemble{flavorName}Release`
     * * `./gradlew assemble.`
     * * `./gradlew app:assemble.`
     * * `./gradlew bundle.`
     * * `./gradlew bundleRelease.`
     * * `./gradlew app:bundleRelease.`
     *
     * Related issues:
     * https://issuetracker.google.com/issues/158060799
     * https://issuetracker.google.com/issues/158753935
     */
    private boolean shouldConfigureFlutterTask(Task assembleTask) {
        def cliTasksNames = project.gradle.startParameter.taskNames
        if (cliTasksNames.size() != 1 || !cliTasksNames.first().contains("assemble")) {
            return true
        }
        def taskName = cliTasksNames.first().split(":").last()
        if (taskName == "assemble") {
            return true
        }
        if (taskName == assembleTask.name) {
            return true
        }
        if (taskName.endsWith("Release") && assembleTask.name.endsWith("Release")) {
            return true
        }
        if (taskName.endsWith("Debug") && assembleTask.name.endsWith("Debug")) {
            return true
        }
        if (taskName.endsWith("Profile") && assembleTask.name.endsWith("Profile")) {
            return true
        }
        return false
    }

这里我们看到它是根据我们的 taskName 和 assembleTask.name 去做的各种判断,于是我就打印这两个名称,发现 taskName=assembleReleaseChannels,assembleTask.name=assembleRelease,就是说永远都不会返回 false。 这里只有返回了 true 我们的 mergeAssets task 才会执行的。

这个 taskName 就是 walle 打渠道包需要执行的 task assembleReleaseChannels。那我首先想到一种解决方案

方案1:将下面代码加入到 shouldConfigureFlutterTask 方法的 return false 前面,执行 assembleReleaseChannels task 后发现问题解决了。

if (taskName.endsWith("ReleaseChannels") && assembleTask.name.endsWith("Release")) {
        return true
    }

但是用这个方案有个弊端就是所有人的 fluttersdk 都需要修改一下,如果打渠道包是由 CI/CD 执行,机器上的 fluttersdk 也要做修改,还有就是 sdk 升级修改了这个文件可能还要重复修改太复杂了,能不能通过我们项目里面配置 task 的名称,于是就有了第二种方法。

方案2:刚开始我想的是修改 walle 的task 名称,去看了下 walle 的插件代码,名称是如下设置的:

ChannelMaker channelMaker = project.tasks.create("assemble${variantName}Channels", ChannelMaker);

结尾写死的 Channels。clone 下来修改一下在项目中引入也不是不行,就是有点麻烦。之后 walle 更新了还要再拉代码。能不能在项目中修改引入插件的 task 名称呢。我是各种搜索都没有找到相应的方法。

最后的思路是创建一个新的 task 就叫 assembleChannelsRelease 必须以 Release 结尾的名称,依赖 walle 的 assembleReleaseChannels,如下:

task assembleChannelsRelease(dependsOn: 'assembleReleaseChannels')

执行 task 后再看 APK 的 assets,问题终于解决了。

3. 最后的解决方法

在 APP 的 build.gradle 中添加 task assembleChannelsRelease(dependsOn: 'assembleReleaseChannels')

4. Flutter 为什么要加一个 shouldConfigureFlutterTask 方法的判断

我们看它的注释可以大概了解到 在 AGP 4.0 中,存在的一个问题是,Android 的 Lint 工具任务依赖于构建 libapp.so 的 JAR 任务。构建发布版需要调用生成 debug 版的 JAR 任务,但 Gradle 没有生成 debug 版,因此出现了问题。Flutter 应用程序构建依赖于生成 libapp.so 的 JAR 任务的执行,因此当应用程序构建到发布版时,需要调用生成 libapp.so release 版本的 JAR 任务才能正确地构建应用程序。

但是这跟我们合并 assets 没有关系。可能是为了在执行一些检查、单元测试和代码扫描等构建任务时不执行合并节省时间。这个方法在别的地方也会有调用。这块还没有看懂是为什么。之后有空了再研究。

5. 总结

Check failed: vm. Must be able to initialize the VM 这个错误最主要的原因是 flutter_assets 这个文件夹没有打包进 APK 的 assets 中。 我们通过分析也得知不止通过 walle 打渠道会出现这个问题,要是修改了打包的 task 名称,不以它规定的字符串结尾都会出现问题。 重要的是找到问题的根源,就是分析 flutter\packages\flutter_tools\gradle 下的 flutter.gradle,通过加入日志分析最后找到解决方案。

参考链接:

  1. juejin.cn/post/685960…
  2. github.com/flutter/flu…
  3. www.jianshu.com/p/9b96999fc…