Resource Index Inlining

The Problem of Resource Index

Android 在构建的过程中,会为每个模块(库、应用)生成一份资源索引,诸如:R$id.classR$layout.class 等等,这对于开发者来说,在代码里引用资源十分的方便。

对于 library 模块来说,R 文件中的索引值并非常量值,以至于 library 的类中引用 R 索引值的方式其实是调用 R 类的 field 来实现的,这也是为什么在 library 工程中不能在 switch-case 语句或者 Annotatio 中使用资源索引的原因。以至于 ButterKnife 创造了独有的 R2 来解决这个问题。

Android 系统中定义了 10 多种资源类型,假设每个模块使用了 5 种资源类型,就会生成 6 个对应的 class 文件(包括 R.class),由于工程结构的复杂度普遍上升,在 APP 工程中直接或间接引用的 library 少则几十,多则上百,假设 APP 中引用了 100library,那对应的 R 文件至少是 500 个以上,无论是类数量、字段数量都是巨大的浪费,毕竟单个 dex65535 的限制,虽然有 multi-dex 技术,但多一个 dex 就会为安装、冷启动增加不必要的性能开销。

Unnecessary R Removal

对于 Android 工程来说,通常,libraryR 只是 applicationR 的一个子集,所以,只要有了全集,子集是可以通通删掉的,而且,applicationR 中的常量字段,一旦参与编译后,就再也没有利用价值(反射除外)。在 R 的字段,styleable 字段是一个例外,它不是常量,它是 int[]。所以,删除 R 之前,我们要弄清楚要确定哪些是能删的,哪些是不能删的,根据经验来看,不能删的索引有:

  1. ConstraintLayout 中引用的字段,例如:

    <android.support.constraint.Group
        android:id="@+id/group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:constraint_referenced_ids="button4,button9" />
    
    1
    2
    3
    4
    5
    6

    其中,R.id.button4R.id.button9 是必须要保留的,因为 ContraintLayout 会调用 TypedArray.getResourceId(int, int) 来获取 button4button9id 索引。

    总结下来,在 ConstraintLayout 中引用其它 id 的属性如下:

    • constraint_referenced_ids
    • layout_constraintLeft_toLeftOf
    • layout_constraintLeft_toRightOf
    • layout_constraintRight_toLeftOf
    • layout_constraintRight_toRightOf
    • layout_constraintTop_toTopOf
    • layout_constraintTop_toBottomOf
    • layout_constraintBottom_toTopOf
    • layout_constraintBottom_toBottomOf
    • layout_constraintBaseline_toBaselineOf
    • layout_constraintStart_toEndOf
    • layout_constraintStart_toStartOf
    • layout_constraintEnd_toStartOf
    • layout_constraintEnd_toEndOf 因此,Booster 采用了解析 xml 的方式,从 xml 中提取以上属性。
  2. 其它通过 TypedArray.getResourceId(int, int)Resources.getIdentifier(String, String, String) 来获取索引值的资源

    针对这种情况,需要对字节码进行全盘扫描才能确定哪些地方调用了 TypedArray.getResourceId(int, int)Resources.getIdentifier(String, String, String),考虑到增加一次 Transform 带来的性能损耗,Booster 提供了通过配置白名单的方式来保留这些资源索引。

Unnecessary Field Removal

由于 Android 的资源索引只有 32 位整型,格式为:PP TT NNNN,其中:

  • PPPackage ID,默认为 0x7f
  • TTResource Type ID,从 1 开始依次递增;
  • NNNNName ID,从 1 开始依次递增;

为了节省空间,在构建 application 时,所有同类型的资源索引会重排,所以,library 工程在构建期间无法确定资源最终的索引值,这就是为什么 library 工程中的资源索引是变量而非常量,既然在 application 工程中可以确定每个资源最终的索引值了,为什么不将 library 中的资源索引也替换为常量呢?这样就可以删掉多余的 field 了,在一定程度上可以减少 dex 的数量,收益是相当的可观。

在编译期间获取索引常量值有很多种方法:

  1. 反射 R 类文件
  2. 解析 R 类文件
  3. 解析 Symbol List (R.txt)

经过 benchmark 测试发现,解析 Symbol List 的方案性能最优,因此,在 Transform 之前拿到所有资源名称与索引值的映射关系,然后在 Transform 的过程中将 getfield 指令替换成 ldc 指令即可。

Getting Started

开启资源索引内联只需要引入 booster-transform-r-inlineopen in new window 即可,如下所示:

buildscript {
    ext {
        kotlin_version = "1.5.31"
        booster_version = "4.16.3"
    }
    repositories {
        mavenLocal()
        mavenCentral()
        google()
        jcenter()
        maven { url 'https://oss.sonatype.org/content/repositories/public/' }
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"

        /* 👇👇👇👇 引用这个模块 👇👇👇👇 */
        classpath "com.didiglobal.booster:booster-transform-r-inline:$booster_version"
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Ingoring Specified Resources

booster-transform-r-inlineopen in new window 支持通过属性的方式来忽略指定的资源。

属性说明
booster.transform.r.inline.ignores忽略的资源限定符(逗号分隔,支持通配符)

Configuring by gradle.properties

booster.transform.r.inline.ignores=android/*,androidx/*
1

Configuring by Command Line

$ ./gradlew assembleDebug -Pbooster.transform.r.inline.ignores=android/*,androidx/*
1