Flutter Android如何延迟加载代码

本文同步自个人博客Flutter Android如何延迟加载代码,转载请注明出处。

Motivation

在 add-to-app 的多引擎场景下,随着业务变多,代码量也会持续增长,首次创建引擎的耗时会越来越明显。我们 APP 的 libapp.so 已经有 90+MB,在低端机上首次加载大约需要 2 秒,并且增加内存占用。

但在 add-to-app 场景里,并不是所有 Flutter 页面都会在启动时立刻展示,通常是进入某个 Flutter 页面时才真正需要加载对应代码。因此,在初始化阶段一次性加载全部代码显然不太合理。

那有没有办法把暂时不需要的代码模块延迟加载呢?答案是肯定的。Flutter 官方支持 Deferred-components,这个能力本质上是基于 Dart 的 deferred import:构建时会把部分 Dart AOT 代码拆分成独立的 loading units(一般后缀为part.so),运行时再按需加载。这样就可以避免在初始化阶段一次性加载全部代码。

不过,官方这套能力默认主要是为 Google Play Dynamic Feature Modules 设计的,并且局限了在Google Play渠道使用。对我们的场景来说,并不一定需要远程下发能力,我们更需要的是"本地代码按需加载",而不是在初始化时一次性把所有代码都加载进来。

开启Deferred-components

Flutter 支持通过 Dart 的 deferred 关键字做延迟加载,但在 Android 上,官方默认方案是基于 Google Play Dynamic Feature Modules,并且完整发布流程主要围绕 AAB(App Bundle)展开。

不过深入代码后可以发现,是否进入 Deferred-components 的构建流程,本质上是通过配置项控制的。只要相关配置生效,Flutter tool 就会进入 Deferred-components 的构建路径,生成 split AOT 产物。

见:
github.com/flutter/flu...

实验下来,开启这个能力的关键点主要有两个:

  • 在 android/gradle.properties 中添加 deferred-components=true
  • 在 pubspec.yaml 中添加 deferred-components:

配置完成后,即使构建 APK,也可以开启 Deferred-components 的相关能力。

以下是官方 gallery 构建 APK 的效果

实现DeferredComponentManager

如何实现"本地延迟加载"?可以参考官方 PlayStoreDeferredComponentManager 的实现思路,把 Google Play Core 相关逻辑去掉,仅保留本地查找和加载 .so 的部分。

下面是一个可行的示例:

kt 复制代码
class LocalDeferredLibraryManager(
    private val context: Context
) : DeferredComponentManager {

    private val flutterApplicationInfo: FlutterApplicationInfo = ApplicationInfoLoader.load(context)

    private var flutterJNI: FlutterJNI? = null

    override fun setJNI(flutterJNI: FlutterJNI?) {
        this.flutterJNI = flutterJNI
    }

    override fun setDeferredComponentChannel(channel: DeferredComponentChannel?) {

    }

    override fun installDeferredComponent(
        loadingUnitId: Int,
        componentName: String?
    ) {
        Log.e("Flutter", "LocalDeferredImportManager, installDeferredComponent, loadingUnitId: $loadingUnitId, componentName: $componentName")

        loadDartLibrary(loadingUnitId, componentName);
    }

    override fun getDeferredComponentInstallState(
        loadingUnitId: Int,
        componentName: String?
    ): String? {
        return ""
    }

    override fun loadAssets(loadingUnitId: Int, componentName: String?) {

    }

    override fun loadDartLibrary(loadingUnitId: Int, componentName: String?) {
        Log.e("Flutter", "LocalDeferredImportManager, loadDartLibrary, loadingUnitId: $loadingUnitId")
        if (loadingUnitId < 0) {
            return;
        }

        val aotSharedLibraryName =
            flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";

        // Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
        val abi: String = Build.SUPPORTED_ABIS[0]!!
        val pathAbi = abi.replace("-", "_") // abis are represented with underscores in paths.

        // TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more
        // performant and robust.

        // Search directly in APKs first
        val apkPaths: MutableList<String?> = ArrayList<String?>()

        // If not found in APKs, we check in extracted native libs for the lib directly.
        val soPaths: MutableList<String?> = ArrayList<String?>()

        val searchFiles: Queue<File?> = LinkedList<File?>()

        // Downloaded modules are stored here
        searchFiles.add(context.getFilesDir())

        // The initial installed apks are provided by `sourceDirs` in ApplicationInfo.
        // The jniLibs we want are in the splits not the baseDir. These
        // APKs are only searched as a fallback, as base libs generally do not need
        // to be fully path referenced.
        Log.e("Flutter", "context.applicationInfo.splitSourceDirs: ${context.applicationInfo.splitSourceDirs}")
        for (path in context.applicationInfo.splitSourceDirs ?: emptyArray()) {
            searchFiles.add(File(path))
        }

        while (!searchFiles.isEmpty()) {
            val file = searchFiles.remove()
            if (file != null && file.isDirectory() && file.listFiles() != null) {
                for (f in file.listFiles()) {
                    searchFiles.add(f)
                }
                continue
            }
            val name = file!!.getName()
            // Special case for "split_config" since android base module non-master apks are
            // initially installed with the "split_config" prefix/name.
            if (name.endsWith(".apk")
                && (name.startsWith(componentName!!) || name.startsWith("split_config"))
                && name.contains(pathAbi)
            ) {
                apkPaths.add(file.getAbsolutePath())
                continue
            }
            if (name == aotSharedLibraryName) {
                soPaths.add(file.getAbsolutePath())
            }
        }

        val searchPaths: MutableList<String?> = ArrayList<String?>()

        // Add the bare filename as the first search path. In some devices, the so
        // file can be dlopen-ed with just the file name.
        searchPaths.add(aotSharedLibraryName)

        for (path in apkPaths) {
            searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName)
        }
        for (path in soPaths) {
            searchPaths.add(path)
        }

        for (s in searchPaths) {
            Log.e("Flutter", "searchPath: $s")
        }

        flutterJNI!!.loadDartDeferredLibrary(
            loadingUnitId, searchPaths.toTypedArray<String?>()
        )
    }

    override fun uninstallDeferredComponent(
        loadingUnitId: Int,
        componentName: String?
    ): Boolean {
        return true
    }

    override fun destroy() {

    }
}

结论

这是一个可行方案:复用 Flutter Deferred-components 的整体机制,把 libapp.so 拆分成多个 *.part.so,并通过自定义 DeferredComponentManager 在本地按需加载,从而避免初始化阶段一次性加载全部代码,优化 add-to-app 多引擎场景下的首开耗时及内存占用。

不过,它本质上仍然属于"非官方标准路径"的自定义实现,是否适合落地到项目中,还需要结合自身项目的实现方式以及长期维护成本来评估。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui