跳转至

Android 性能优化技术月报 | 2024 年 10 月

每个月都会有一些 Android 性能优化相关的优质内容发布,然而,碎片化阅读使得这些知识难以形成完整体系,且容易被遗忘。为解决这些问题,我决定尝试使用技术月报的形式,总结我在最近一个月内查阅的 Android 性能优化相关的优质内容。

月报的主要内容包括:整理展示我在最近一个月所查阅的 Android 性能优化领域的最新技术动态、精选博客,精选视频等内容。

精选博客

B 站 Android 代码库的演进历程

这篇文章讲述 B 站 Android 代码库的演进历程。从早期的单仓库,到后来的多仓库,再到如今的大仓。单仓库简单但后期有代码混乱和编译慢等问题;多仓库有隔离性等优点,但维护复杂、代码重复等缺点凸显;大仓是单仓库进化版,有工具链辅助。总之,选择方案要适合团队业务,没有通用架构。

<root dir>
    ├── build.gradle
    ├── settings.gradle 
    ├── app/
    │    ├── app-a
    │    │    ├── src 
    │    │    └── build.gradle
    │    ├── app-b
    │    │    ├── src
    │    │    └── build.gradle   
    │    ├── build.gradle
    │    └── settings.gradle
    │
    ├── common/
    │    ├── common-a
    │    │    ├── src
    │    │    └── build.gradle
    │    ├── build.gradle
    │    └── settings.gradle
    │
    ├── framework/
    │    ├── lib-a
    │    │    ├── src
    │    │    └── build.gradle
    │    ├── build.gradle
    │    └── settings.gradle
    │
    └── MyApp
         ├── src
         └── build.gradle

可能是 AGP8 编译最快的方案

AGP8 的变更应该很多人都知道了,移除了Transform API,所以很多 class 操作类的插件代码都需要改了。

TheRouter在开发的时候就支持了AGP8,使用的是toTransform()这个方法替代老版本的 API,这么做以后有个问题,就是编译速度会非常非常慢,尤其是工程里代码量越大,编译速度越慢。

本文提出多种解决思路,如用 AsmClassVisitorFactory 结合 toTransform 或 toGet 方法,TheRouter 还做了内存缓存优化,保障产物结果。

project.plugins.withType(AppPlugin::class.java) {
    // Queries for the extension set by the Android Application plugin.
    // This is the second of two entry points into the Android Gradle plugin
    val androidComponents =
        project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
    // Registers a callback to be called, when a new variant is configured
    androidComponents.onVariants { variant ->
        val taskProvider = project.tasks.register<ModifyClassesTask>("${variant.name}ModifyClasses")

        // Register modify classes task
        variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT)
            .use(taskProvider)
            .toTransform(
                ScopedArtifact.CLASSES,
                ModifyClassesTask::allJars,
                ModifyClassesTask::allDirectories,
                ModifyClassesTask::output
            )
    }
}

从DDD视角探讨代码复用的成本及效益

  • 类和函数存在的意义困惑:作者从工作经历中对类和函数是否只为复用而产生困惑,随着业务发展,复用的类和函数可能因大量复用和适应不同场景而充满 if...else...,引发对其存在意义的思考。
  • DRY 原则与重复代码的权衡:设计模式的 DRY 原则并非绝对好,在复杂工程中,DRY 的函数被大量引用可能导致逻辑复杂、修改风险高;重复代码反而可能降低后续修改风险,可根据业务需要灵活整合。
  • 复用的成本与效益权衡:复用需要权衡成本与效益。成本包括了解可复用构件、对接接口、测试及排查问题等。效益分为降低开发成本和提升软件产品核心竞争力,如通过整合业务中台快速支撑新业务上线,复用成功模块提升产品竞争力。
  • 深浅模块与复用成本角度:深模块接口简单但功能深刻,复用合算;浅模块接口复杂功能少,复用成本可能高于好处。以文件系统和数据库为例说明深模块的优势,以及项目中浅模块代码的低复用价值。
  • 效益角度谈复用与 DDD 子域划分:从效益角度,复用可提升产品核心竞争力,如 Supercell 游戏公司和钉钉的案例。DDD 对子域划分提供复用策略,核心子域应尽可能复用提升竞争力,支持子域和通用子域根据特点采用不同复用策略。
  • 成功设计与业务理解:强调成功的设计来自对业务问题的深刻理解,DDD 能帮助从业务分析视角看待复用的成本和效益,做出更好的决策。

QuickJS的垃圾回收算法

文章主要介绍了 QuickJS 的垃圾回收算法,具体内容如下:

  • 常见 GC 算法简介
  • 垃圾评定算法
    • 引用计数法(RC):在对象头添加计数器,记录对象被引用情况,引用时计数器 ++,释放时 --,当计数器为 0 时回收对象,但存在循环引用导致内存泄漏的问题。
    • 可达性分析:以 GC Roots(如当前栈上变量、全局变量等)为起点遍历对象,未被遍历到的为垃圾。
  • 垃圾回收算法
    • GC 标记清除法:先通过可达性分析标记存活对象,再清除未标记对象。
    • GC 复制算法:将对象分配空间分为两块,申请内存在 from 空间,回收时将存活对象复制到 to 空间,回收 from 空间并交换两块空间身份。
    • GC 标记压缩法:前两步同标记清除,之后增加内存整理工作减少碎片。
    • 分代算法:根据经验将 heap 分为年轻代和老年代,大部分时间只遍历年轻代,多次 GC 未回收的年轻代对象晋升到老年代,内存整体触发阈值时回收整块内存。
  • QuickJS 的垃圾回收算法
    • 引用计数算法(RC)实现:在对象头添加ref_count计数器,创建对象时设为 1,引用时 ++,释放时 --,ref_count<=0 时回收对象。
  • 引用计数的问题:循环引用会导致内存泄漏,如对象 A 引用 B,B 引用 C,C 又引用 A 时,三个对象计数器永远 >=1。
  • QuickJS 解决循环引用的方法:内存上涨触发阈值时,通过JS_RunGC函数执行解环操作,包括遍历gc_obj_list标记对象、处理子对象计数器并将入度为 1 的对象挂到tmp_obj_list,重新遍历恢复标记和计数器,最后回收tmp_obj_list上的对象。

最后更新: January 5, 2025