使用 R8 压缩您的应用

image

作者 / Google 软件工程师 SørenGjesse 和 Christoffer Adamsen

人们更倾向于安装并保留较小和安装占用空间更小的应用,在新兴市场中尤为明显。有了 R8 编译器,您可以通过压缩、混淆和优化,更全面的缩小应用体积。

本文我们将对 R8 的特性进行一个简要的介绍,并介绍可预期的代码缩减程度以及如何在 R8 中启用这些功能。

R8 的压缩特性

R8 通过下面 4 项特性来减少 Android 应用大小:

  • 摇树优化 (Tree shaking): 使用静态代码分析来查找和删除无法访问的代码和未实例化的类型;
  • 优化 : 通过删除无效代码,选择性内联,移除未使用的参数和类合并来优化代码大小;
  • 重命名标识,即混淆处理 : 使用短名称以及缩短包命名空间;
  • 减少调试信息 : 规范化调试信息并压缩行号信息。

为什么需要 R8 压缩

开发应用时,所有代码都应有目的并在应用中实现相应功能。不过,大多数应用都会使用 JetpackOkHttpGuavaGsonGoogle Play 服务 等第三方库,并且用 Kotlin 编写的应用始终包含 Kotlin 标准库。当您使用这其中的某个第三方库时,您的应用中通常只使用其中很小一部分。若不压缩,所有库代码都会保留在您的应用中。

您的代码大小也可能比实际需要的大,因为冗长的代码有时可以提高可读性和可维护性: 例如,您可能会尽量使用有意义的变量名和建造者模式 (builder pattern) 来帮助其他人更容易检查和理解您的代码。但是这些模式会加大代码量。通常,您自己编写的代码有很大的压缩空间。

启用 R8 来压缩您的应用

要在 release build 上启用 R8 压缩,需要在应用的主 build.gradle 文件中将 minifyEnable 属性设置为 true,如下所示:

android {
  buildTypes {
    release {
      minifyEnabled true
    }
  }
}

别被 minifyEnable 这个名字所迷惑,它会启用 R8 的代码缩减功能。

R8 能缩减多少应用大小?

R8 可以大大减小应用的大小。例如,去年的 Google I/O 应用大小为 18.55 MB,压缩前包含 150,220 个方法和 3 个 DEX 文件。压缩后,应用大小缩小到 6.45 MB,包含 45,831 个方法和 1 个 DEX 文件。R8 缩减了 65% 的 DEX 文件大小 (测量数据来自 Android Studio 3.5.1 和 IOSched 示例应用)。

基本压缩算法

为简单起见,我们写了一个基于 Java 编程语言的程序作为参考:

class com.example.JavaHelloWorld {
  private void unused() {
    System.out.println("Unused");
  }

  private static void greeting() {
    System.out.println("Hello, world!");
  }

  public static void main(String[] args) {
    greeting();
  }
}

程序的入口是 static void main 方法,我们使用以下 keep 规则 指定该方法:

-keep class com.example.JavaHelloWorld {
      public static void main(java.lang.String[]);
}

R8 缩减算法的运作方式如下:

  • 首先,它从程序常见的入口点跟踪所有可访问的代码。这些入口点由 R8 keep 规则定义。例如,在此 Java 代码示例中,R8 会在 main 方法处开始运行。
  • 在该示例中,R8 从 main 方法跟踪到 greeting 方法。greeting 方法是在运行时被调用的,因此跟踪在此处停止。
  • 跟踪完成后,R8 使用摇树优化来删除未使用的代码。在此示例中,摇树删除了未使用的方法,因为 R8 的跟踪过程检测到从任何已知的入口都无法到达该方法。
  • 接下来,R8 将标识重命名为较短的名称,这些名称在 DEX 文件中占用较少的空间。在示例中,R8 可能会将 greeting 方法重命名为短名称 a:
class com.example.JavaHelloWorld {

  private static void a() {
    System.out.println("Hello, world!");
  }

  public static void main(String[] args) {
    a();
  }
}
  • 最后,应用代码优化。缩减代码大小的内联是其一。在此示例中,将方法 a 的主体直接迁移到 main 中,代码会显得更简洁:
class com.example.JavaHelloWorld {

  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

如您所见,处理后的代码比原始代码短得多。

使用 R8 压缩应用前的准备工作

正如独立的 Java 程序一样,Android 应用有许多常见的入口点: Activity (活动)Service (服务)Content Provider (内容提供者)Broadcast Receiver (广播接收者)aapt2 工具通过基于 Android Manifest 文件生成 keep 规则来为您处理这些入口点。

除了这些熟知的入口点,Android 应用还需要其他标准的 keep 规则。这些规则由 Android Gradle 插件提供,您可以在配置构建时指定该默认配置文件:

android {
  buildTypes {
    release {
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
    }
  }
}

应用代码中的反射

反射 (Reflection) 会导致 R8 在跟踪代码时无法识别到代码的入口点。第三方库也可能用到反射,并且由于第三方库实际上是您的应用的一部分,您 (作为应用开发者) 将负责这些库以及您自己的代码中使用的反射。第三方库可能附带了它们自己的规则,但是切记,有些库不一定是为 Android 编写的,抑或是未考虑缩减问题,因此它们可能需要其他配置。

以一个 Kotlin 类为例,该类具有一个名为 name 的字段和一个 main 方法,该方法创建一个实例并将该实例序列化为 JSON:

class Person(val name: String)

fun printJson() {
   val gson = Gson()
   val person = Person("Søren Gjesse")
   println(gson.toJson(person))
}

缩减代码后,运行程序将输出一个空的 JSON 对象 {}。这是因为 R8 仅将字段名视为写入 (在 Person 构造函数中),但从未读取,因此 R8 会将其移除。最后 Person 丢失了字段值,造成空的 JSON 对象。但是,该字段由 Gson 序列化读取,而 Gson 使用反射的方式来执行此操作,因此 R8 无法看到此字段已被读取。

要保留名称字段,请在您的 proguard-rules.pro 文件中添加一个保留规则 -keep:

-keep class com.example.myapplication.Person {
    public java.lang.String name;
}

此规则告诉 R8 不要处理 Person 类中的 name 的字段。将其放置在适当位置后,运行代码即可得到预期的 JSON 对象 {"name": "SørenGjesse"}

最后,在配置项目时,请确保将 proguard-rules.pro 文件添加到 build.gradle 配置中:

android {
  buildTypes {
    release {
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                    'proguard-rules.pro'
    }
  }
}

了解更多

有兴趣更深入了解 R8 压缩器如何运作吗?请参考 R8 开发者文档 了解更多!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容