Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >R8 编译器: 为 Kotlin 库和应用 "瘦身"

R8 编译器: 为 Kotlin 库和应用 "瘦身"

作者头像
Android 开发者
发布于 2020-11-16 09:29:59
发布于 2020-11-16 09:29:59
1.1K00
代码可运行
举报
文章被收录于专栏:Android 开发者Android 开发者
运行总次数:0
代码可运行

作者 / Morten Krogh-Jespeersen, Mads Ager

R8 是 Android 默认的程序缩减器,它可以通过移除未使用的代码和优化其余代码的方式降低 Android 应用大小,R8 同时也支持缩减 Android 库大小。除了生成更小的库文件,库压缩操作还可以隐藏开发库里的新特性,等到这些特性相对稳定或者可以面向公众的时候再对外开放。

Kotlin 对于编写 Android 应用和开发库来说是非常棒的开发语言。不过,使用 Kotlin 反射来缩减 Kotlin 开发库或者应用就没那么简单了。Kotlin 使用 Java 类文件中的元数据 来识别 Kotlin 语言中的结构。如果程序缩减器没有维护和更新 Kotlin 的元数据,相应的开发库或者应用就无法正常工作。

R8 现在支持维持和重写 Kotlin 的元数据,从而全面支持使用 Kotlin 反射来压缩 Kotlin 开发库和应用。该特性适用于 Android Gradle 插件版本 4.1.0-beta03。欢迎大家踊跃尝试,并在 Issue Tracker 页面 向我们反馈整体使用感受和遇到的问题。

本文接下来的内容为大家介绍了 Kotlin 元数据的相关信息以及 R8 中对于重写 Kotlin 元数据的支持。

Kotlin 元数据

Kotlin 元数据 是存储在 Java 类文件的注解中的一些额外信息,它由 Kotlin JVM 编译器生成。元数据确定了类文件中的类和方法是由哪些 Kotlin 代码构成的。比如,Kotlin 元数据可以告诉 Kotlin 编译器类文件中的一个方法实际上是 Kotlin 扩展函数

我们来看一个简单的例子,以下库代码定义了一个假想的用于指令构建的基类,用于构建编译器指令。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.example.mylibrary

/** CommandBuilderBase 包含 D8 和 R8 中通用的选项 */

abstract class CommandBuilderBase {
    internal var minApi: Int = 0
    internal var inputs: MutableList = mutableListOf()

    abstract fun getCommandName(): String
    abstract fun getExtraArgs(): String

    fun build(): String {
        val inputArgs = inputs.joinToString(separator = " ")
        return "${getCommandName()} --min-api=$minApi $inputArgs ${getExtraArgs()}"
    }
}

fun  T.setMinApi(api: Int): T {
    minApi = api
    return this
}

fun  T.addInput(input: String): T {
    inputs.add(input)
    return this
}
复制代码

然后,我们可以定义一个假想 D8CommandBuilder 的具体实现,它继承自 CommandBuilderBase,用于构建简化的 D8 指令。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.example.mylibrary

/** D8CommandBuilder to build a D8 command. */
class D8CommandBuilder: CommandBuilderBase() {
    internal var intermediateOutput: Boolean = false
    override fun getCommandName() = "d8"
    override fun getExtraArgs() = "--intermediate=$intermediateOutput"
}

fun D8CommandBuilder.setIntermediateOutput(intermediate: Boolean) : D8CommandBuilder {
    intermediateOutput = intermediate
    return this
}
复制代码

上面的示例使用的扩展函数来保证当您在 D8CommandBuilder 上调用 setMinApi 方法的时候,所返回的对象类型是 D8CommandBuilder 而不是 CommandBuilderBase。在我们的示例中,这些扩展函数属于顶层的函数,并且仅存在于 CommandBuilderKt 类文件中。接下来我们来看一下通过精简后的 javap 命令所输出的内容。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ javap com/example/mylibrary/CommandBuilderKt.class
Compiled from "CommandBuilder.kt"
public final class CommandBuilderKt {
public static final  T addInput(T,      String);
public static final  T setMinApi(T, int);
...
}

从 javap 的输出内容里可以看到扩展函数被编译为静态方法,该静态方法的第一个参数是扩展接收器。不过这些信息还不足以告诉 Kotlin 编译器这些方法需要作为扩展函数在 Kotlin 代码中调用。所以,Kotlin 编译器还在类文件中增加了 kotlin.Metadata 注解。注解中的元数据里包含本类中针对 Kotlin 特有的信息。如果我们使用 verbose 选项就可以在 javap 的输出中看到这些注解。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ javap -v com/example/mylibrary/CommandBuilderKt.class
...
RuntimeVisibleAnnotations:
  0: kotlin/Metadata(
   mv=[...],
   bv=[...],
   k=...,
   xi=...,
   d1=["^@.\n^B^H^B\n^B^X^B\n^@\n^B^P^N\n^B...^D"],
   d2=["setMinApi", ...])

元数据注解的 d1 字段包含了大部分实际的内容,它们以 protocol buffer 消息的形式存在。元数据内容的具体意义并不重要。重要的是 Kotlin 编译器会读取其中的内容,并且通过这些内容确定了这些方法是扩展函数,如下 Kotlinp dump 输出内容所示。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ kotlinp com/example/mylibrary/CommandBuilderKt.class
package {

// signature:   addInput(CommandBuilderBase,String)CommandBuilderBase
public final fun  T.addInput(input: kotlin/String): T

// signature: setMinApi(CommandBuilderBase,I)CommandBuilderBase
public final fun  T.setMinApi(api: kotlin/Int): T

...
}

该元数据表明这些函数将在 Kotlin 用户代码中作为 Kotlin 扩展函数使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
D8CommandBuilder().setMinApi(12).setIntermediate(true).build()

R8 过去是如何破坏 Kotlin 开发库的

正如前文所提到的,为了能够在库中使用 Kotlin API,Kotlin 的元数据非常重要,然而,元数据存在于注解中,并且会以 protocol buffer 消息的形式存在,而 R8 是无法识别这些的。因此,R8 会从下面两个选项中择其一:

  • 去除元数据
  • 保留原始的元数据

但是这两个选项都不可取。

如果去除元数据,Kotlin 编译器就再也无法正确识别扩展函数。比如在我们的例子中,当编译类似 D8CommandBuilder().setMinApi(12) 这样的代码时,编译器就会报错,提示不存在该方法。这完全说得通,因为没有了元数据,Kotlin 编译器唯一能看到的就是一个包含两个参数的 Java 静态方法。

保留原始的元数据也同样会出问题。首先 Kotlin 元数据中所保留的类是父类的类型。所以,假设在缩减开发库大小的时候,我们仅希望 D8CommandBuilder 类能够保留它的名称。这时候也就意味着 CommandBuilderBase 会被重命名,一般会被命名为 a。如果我们保留原始的 Kotlin 元数据,Kotlin 编译器会在元数据中寻找 D8CommandBuilder 的超类。如果使用原始元数据,其中所记录的超类是 CommandBuilderBase 而不是 a。此时编译就会报错,并且提示 CommandBuilderBase 类型不存在。

R8 重写 Kotlin 元数据

为了解决上述问题,扩展后的 R8 增加了维护和重写 Kotlin 元数据的功能。它内嵌了 JetBrains 在 R8 中开发的 Kotlin 元数据开发库。元数据开发库可以在原始输入中读取 Kotlin 元数据。元数据信息被存储在 R8 的内部数据结构中。当 R8 完成对开发库或者应用的优化和缩小工作后,它会为所有声明被保留的 Kotlin 类合成新的正确元数据。

来一起看一下我们的示例有哪些变化。我们将示例代码添加到一个 Android Studio 库工程中。在 gradle.build 文件中,通过将 minifyEnbled 置 true 来启用包大小缩减功能,我们更新缩减器配置,使其包含如下内容:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#保留 D8CommandBuilder 和它的全部方法
-keep class com.example.mylibrary.D8CommandBuilder {
  ;
}
#保留扩展函数
-keep class com.example.mylibrary.CommandBuilderKt {
  ;
}
#保留 kotlin.Metadata 注解从而在保留项目上维持元数据
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }

上述内容告诉 R8 保留 D8CommandBuilder 以及 CommandBuilderKt 中的全部扩展函数。它还告诉 R8 保留注解,尤其是 kotlin.Metadata 注解。这些规则仅仅适用于那些被显式声明保留的类。因此,只有 D8CommandBuilder 和 CommandBuilderKt 的元数据会被保留。但是 CommandBuilderBase 中的元数据不会被保留。我们这么处理可以减少应用和开发库中不必要的元数据。

现在,启用缩减后所生成的库,里面的 CommandBuilderBase 被重命名为 a。此外,所保留的类的 Kotlin 元数据也被重写,这样所有对于 CommandBuilderBase 的引用都被替换为对 a 的引用。这样开发库就可以正常使用了。

最后再说明一下,在 CommandBuilderBase 中不保留 Kotlin 元数据意味着 Kotlin 编译器会将生成的类作为 Java 类进行对待。这会导致库中 Kotlin 类的 Java 实现细节产生奇怪的结果。要避免这样的问题,就需要保留类。如果保留了类,元数据就会被保留。我们可以在保留规则中使用 allowobfuscation 修饰符来允许 R8 重命名类,生成 Kotlin 元数据,这样 Kotlin 编译器和 Android Studio 都会将该类视为 Kotlin 类。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-keep,allowobfuscation class com.example.mylibrary.CommandBuilderBase

到这里,我们介绍了库缩减和 Kotlin 元数据对于 Kotlin 开发库的作用。通过 kotlin-reflect 库使用 Kotlin 反射的应用同样需要 Kotlin 元数据。应用和开发库所面临的问题是一样的。如果 Kotlin 元数据被删除或者没有被正确更新,kotlin-reflect 库就无法将代码作为 Kotlin 代码进行处理。

举个简单的例子,比如我们希望在运行时查找并且调用某个类中的一个扩展函数。我们希望启用方法重命名,因为我们并不关心函数名,只要能在运行时找到它并且调用即可。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class ReflectOnMe() {
    fun String.extension(): String {
        return capitalize()
    }
}

fun reflect(receiver: ReflectOnMe): String {
    return ReflectOnMe::class
        .declaredMemberExtensionFunctions
        .first()
        .call(receiver, "reflection") as String
}
复制代码

在代码中,我们添加了一个调用: reflect(ReflectOnMe())。它会找到定义在 ReflectOnMe 中的扩展函数,并且使用传入的 ReflectOnMe 实例作为接收器,"reflection" 作为扩展接收器来调用它。

现在 R8 可以在所有保留类中正确重写 Kotlin 元数据,我们可以通过使用下面的缩减器配置启用重写。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#保留反射的类和它的方法
-keep,allowobfuscation class ReflectOnMe {
  ;
}
#保留 kotlin.Metadata 注解从而在保留项目上维持元数据
-keepattributes RuntimeVisibleAnnotations
-keep class kotlin.Metadata { *; }

这样的配置使得缩减器在重命名 ReflectOnMe 和扩展函数的同时,仍然维持并且重写 Kotlin 元数据。

尝试一下吧!

欢迎尝试 R8 对于 Kotlin 库项目中 Kotlin 元数据重写的特性,以及在 Kotlin 项目中使用 Kotlin 反射。该特性可以在 Android Gradle Plugin 4.1.0-beta03 及以后的版本中使用。如果在使用过程中遇到任何问题,请在我们的 Issue Tracker 页面中提交问题。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
R8 编译器: 为 Kotlin 库和应用 "瘦身"
R8 是 Android 默认的程序缩减器,它可以通过移除未使用的代码和优化其余代码的方式降低 Android 应用大小,R8 同时也支持缩减 Android 库大小。除了生成更小的库文件,库压缩操作还可以隐藏开发库里的新特性,等到这些特性相对稳定或者可以面向公众的时候再对外开放。
Android 开发者
2022/09/23
9520
Kotlin Vocabulary | 枚举和 R8 编译器
学习或使用一门新的编程语言时,了解这门语言所提供的功能,以及了解这些功能是否有相关联的开销,都是十分重要的环节。
Android 开发者
2020/05/08
1K0
kotlin 和 r8 的量子纠缠 | 类加载机制偷鸡
戏接上文,kotlin升级没想到啊还有一个大坑。我们之前说了我们使用的agp版本是7.0.3,在这个版本的R8竟然会出现kotlin混淆的bug。
逮虾户
2022/10/28
7050
kotlin 和 r8 的量子纠缠 | 类加载机制偷鸡
认识下 Kotlin 反射背后的男人:@Metadata
Kotlin 允许我们对各种 Kotlin 的语法特性进行访问,不过,这里应该有一个问题没有搞清楚:既然 Java 反射对于 Kotlin 的很多特性都无法访问和识别,换句话说,Java 虚拟机也是无法知道他们的,那么 Kotlin 的反射是如何做到这一点的呢?
bennyhuo
2020/02/20
3.5K1
使用 R8 压缩您的应用
作者 / Google 软件工程师 SørenGjesse 和 Christoffer Adamsen
Android 开发者
2021/01/07
1.4K0
【Kotlin】常用的 Kotlin 类 ① ( 嵌套类 | 数据类 | 数据类 copy 函数 | 数据类解构声明 operator fun component1 | 数据类运算符重载 )
嵌套类 指的是 在 类 A 中 定义 类 B , 一般是 类 B 对 类 A 有一定的作用 , 将 类 B 嵌套进 类 A 中 ; 格式如下 :
韩曙亮
2023/03/30
1.1K0
【Kotlin】常用的 Kotlin 类 ① ( 嵌套类 | 数据类 | 数据类 copy 函数 | 数据类解构声明 operator fun component1 | 数据类运算符重载 )
【Kotlin】Kotlin 与 Java 互操作 ① ( 变量可空性 | Kotlin 类型映射 | Kotlin 访问私有属性 | Java 调用 Kotlin 函数 )
在 Java 语言 中 , 任何 引用类型变量 都可以为 空 null ; Java 中 八种 基本数据类型 变量 的 默认值 为 0 或 false ;
韩曙亮
2023/03/30
1.7K0
【Kotlin】Kotlin 与 Java 互操作 ① ( 变量可空性 | Kotlin 类型映射 | Kotlin 访问私有属性 | Java 调用 Kotlin 函数 )
第4章 类与面向对象编程第4章 类与面向对象编程
在前面的章节中,我们学习了Kotlin的语言基础知识、类型系统等相关的知识。在本章节以及下一章中,我们将一起来学习Kotlin对面向对象编程以及函数式编程的支持。
一个会写诗的程序员
2018/08/17
1.8K0
第4章 类与面向对象编程第4章 类与面向对象编程
【Kotlin】Kotlin 与 Java 互操作 ② ( @JvmField 注解字段给 Java | @JvmOverloads 注解修饰函数 | @JvmStatic 注解声明静态成员 )
在 Java 中是 不能直接访问 Kotlin 中的字段 的 , 必须 调用相应的 Getter 和 Setter 方法 , 才能进行访问 ;
韩曙亮
2023/03/30
1.1K0
【Kotlin】Kotlin 与 Java 互操作 ② ( @JvmField 注解字段给 Java | @JvmOverloads 注解修饰函数 | @JvmStatic 注解声明静态成员 )
如何让注解处理器支持 Kotlin?
话说,最近尝试了一下写了个注解处理器,也就是我们常见的 apt,在 Kotlin 当中有个插件叫 kapt,说的就是注解处理器。注解处理器能干什么呢?能帮我们生成一些代码,让我们变懒,让我们的代码变优雅(也许吧)。
bennyhuo
2020/02/20
2.5K0
Kotlin学习笔记(六)-反射
这一节为Kotlin反射,主要是在Kotlin中时用Java-Api来实现反射,使用Kotlin本身支持的反射API进行反射。还有2者的对比。要是对Java的反射不是很熟悉,可以花几分钟的时间先去网上找些Java反射的文章。关于Java的反射并不是这节的主要内,同时反射中也涉及到泛型的知识。其实有很多反射的地方关于泛型我也不敢说完全明白,也在代码中加了很多TODO,希望以后慢慢能熟能生巧,慢慢理解。
g小志
2019/12/20
2.4K0
Kotlin 1.2 新特性
在Kotlin 1.1中,团队正式发布了JavaScript目标,允许开发者将Kotlin代码编译为JS并在浏览器中运行。在Kotlin 1.2中,团队增加了在JVM和JavaScript之间重用代码
xiangzhihong
2018/02/08
1.8K0
Kotlin 1.2 新特性
J 神提问:除以 2 还是右移 1 ?
我一直在尝试将 AndroidX collection library 移植到 Kotlin multiplatform,来测试二进制兼容性,性能,易用性和不同的内存模型。类库中的一些数据结构使用基于数组实现的二叉树来存储元素。在 Java 代码中有许多地方使用 移位操作 来代替二次幂的乘除法。当移植到 Kotlin 时,这些代码会被转化为略显变扭的中缀操作符,这有点混淆了代码意图。
路遥TM
2021/08/31
1.2K0
深入探索 Android 包瘦身(上)
今天分享一篇匠心制作的《深入探索 Android 包体积优化》,内容比较多,因此分篇分享~
陈宇明
2020/12/16
2.1K0
深入探索 Android 包瘦身(上)
美团App瘦身30%的黑暗手段:删.so文件只是开始,R8代码吞噬术才是终极杀招
大家好,我是稳稳,一个曾经励志用技术改变世界,现在为随时失业做准备的中年奶爸程序员,与你分享生活和学习的点滴。
AntDream
2025/03/27
890
美团App瘦身30%的黑暗手段:删.so文件只是开始,R8代码吞噬术才是终极杀招
【错误记录】Kotlin 代码编译时报错 ( Variable ‘name‘ must be initialized | 初始化块定义在所有属性之后 )
如果在 init 初始化块 中 , 使用到了 成员属性 , 有可能出现 编译时报错信息 ;
韩曙亮
2023/03/30
1K0
【错误记录】Kotlin 代码编译时报错 ( Variable ‘name‘ must be initialized | 初始化块定义在所有属性之后 )
Kotlin与Java互操作
互操作就是在Kotlin中可以调用其他编程语言的接口,只要它们开放了接口,Kotlin就可以调用其成员属性和成员方法,这是其他编程语言所无法比拟的。同时,在进行Java编程时也可以调用Kotlin中的API接口。
xiangzhihong
2022/11/30
3.6K0
Kotlin —— 这次入门就不用放弃了
声明:本文是FEELS_CHAOTIC原创,已获其授权发布,未经原作者允许请勿转载
用户2802329
2018/08/07
1.7K0
Kotlin —— 这次入门就不用放弃了
为超越JVM而生?深入理解Kotlin Native的梦想与可能
Kotlin Native 是 Kotlin 多平台生态的关键一环,也是 Kotlin 开发者突破自身发展瓶颈的重要方向。本文依据 Kotlin Native 的源码,结合作者在运用 Kotlin Native 开发多平台应用的实战经验,详细为大家解读 Kotlin Native 在编译时和运行时的实现细节和实践技巧。本文由腾讯 PCG 代码委员会出品,可能是你在全网能看到的关于 Kotlin Native 分析最全面的干货文章。
腾讯云开发者
2024/08/29
1.9K0
为超越JVM而生?深入理解Kotlin Native的梦想与可能
Contract,开发者和 Kotlin 编译器之间的契约
相比 Java,使用 Kotlin 编程的时候,我们和kotlin编译器的交互行为会更多一些,比如我们可以通过inline来控制字节码的输出结果,使用注解也可以修改编译输出的class文件。
技术小黑屋
2020/01/23
5870
推荐阅读
相关推荐
R8 编译器: 为 Kotlin 库和应用 "瘦身"
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验