我们现在有如下一段简单的 Java 程序,它使用了 Java8 语法

// Java8.java

package com.example.androidlibrary;

import android.util.Log;

public class Java8 {
    
    public interface Consumer {
        void accept(int i);
    }
    
    public static void repeat(int upper, Consumer consumer) {
        for (int i = 0; i < upper; i++) {
            consumer.accept(i);
        }
    }
    
    public static void main(String[] args) {
        repeat(100, (i) -> Log.i("Java8", String.valueOf(i)));
    }
}

我们接着使用 javac 将其编译成为 .class 文件

$ javac -cp /home/lenox/Android/Sdk/platforms/android-34/android.jar com/example/androidlibrary/Java8.java
$ tree .
.
└── com
    └── example
        └── androidlibrary
            ├── Java8$Consumer.class
            ├── Java8.class
            └── Java8.java

4 directories, 3 files
Note

我们这里使用的Java版本为 8

$ java -version
openjdk version "1.8.0_412"
OpenJDK Runtime Environment (Zulu 8.78.0.19-CA-linux64) (build 1.8.0_412-b08)
OpenJDK 64-Bit Server VM (Zulu 8.78.0.19-CA-linux64) (build 25.412-b08, mixed mode)

不管是 Java 还是 Kotlin, ART 或者 Dalvik 都无法直接运行其通过 Java/Kotlin Compiler 编译后的 .class 字节码, 需要通过某些编译器将其转换成为 .dex 字节码。

1.0.0 ~ 3.0.0 版本的AGP默认使用 DX 作为此转换工具,这是 Android 最初的转换工具,我们先看看它在哪?

$ echo $ANDROID_HOME
/home/lenox/Android/Sdk
$ find $ANDROID_HOME -type f -name dx
/home/lenox/Android/Sdk/build-tools/30.0.3/dx

然后我们使用 DX 将其转换成 .dex

$ /home/lenox/Android/Sdk/build-tools/30.0.3/dx --dex --debug --output . com/example/androidlibrary/*.class
Uncaught translation error:
com.android.dx.cf.code.SimException: ERROR in com.example.androidlibrary.Java8.main:([Ljava/lang/String;)V: invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13)
	at com.android.dx.cf.code.Simulator.fail(Simulator.java:947)
	at com.android.dx.cf.code.Simulator.checkInvokeDynamicSupported(Simulator.java:848)
	at com.android.dx.cf.code.Simulator.access$700(Simulator.java:43)
	at com.android.dx.cf.code.Simulator$SimVisitor.visitConstant(Simulator.java:711)
	at com.android.dx.cf.code.BytecodeArray.parseInstruction(BytecodeArray.java:780)
	at com.android.dx.cf.code.Simulator.simulate(Simulator.java:117)
	at com.android.dx.cf.code.Ropper.processBlock(Ropper.java:789)
	at com.android.dx.cf.code.Ropper.doit(Ropper.java:744)
	at com.android.dx.cf.code.Ropper.convert(Ropper.java:349)
	at com.android.dx.dex.cf.CfTranslator.processMethods(CfTranslator.java:309)
	at com.android.dx.dex.cf.CfTranslator.translate0(CfTranslator.java:150)
	at com.android.dx.dex.cf.CfTranslator.translate(CfTranslator.java:102)
	at com.android.dx.command.dexer.Main.translateClass(Main.java:779)
	at com.android.dx.command.dexer.Main.access$2700(Main.java:85)
	at com.android.dx.command.dexer.Main$ClassTranslatorTask.call(Main.java:1901)
	at com.android.dx.command.dexer.Main$ClassTranslatorTask.call(Main.java:1886)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:750)
...at bytecode offset 00000002
locals[0000]: [Ljava/lang/String;
stack[top0]: int{0x00000064 / 100}
...while working on block 0000
...while working on method main:([Ljava/lang/String;)V
...while processing main ([Ljava/lang/String;)V
...while processing com/example/androidlibrary/Java8.class

1 error; aborting

转换失败了,异常信息告诉我们某个字节码指令最低要求的SDK版本为26, 也就是说只能在 Android 8.0 (Oreo) 版本及其以上的设备上使用。OK, 我们按照他的要求添加 --min-sdk-version 26

$ /home/lenox/Android/Sdk/build-tools/30.0.3/dx --dex --debug --min-sdk-version 26 --output . com/example/androidlibrary/*.class
$ tree
.
├── classes.dex
└── com
    └── example
        └── androidlibrary
            ├── Java8$Consumer.class
            ├── Java8.class
            └── Java8.java

4 directories, 4 files

转换成功且 Dex 正常生成了,但是这违背了向下兼容的原则,到目前为止,我们的应用通常要求的最低SDK版本为21, 那低于26的设备怎么使用Java8语法呢?

Desugar? 以前的设备已经无法改变,我们只能通过另外一个转换工具将 Java8 的语法重写成 Java6 的兼容版本,D8 诞生了。

2018年3月发布的AGP3.1.0,Android Gradle Plugin 将使用 D8 取代传统的 DX 作为其默认的 Dex 编译器,与之前 DX 编译器相比,D8 的编译速度更快,输出的 DEX 文件更小,同时具有相同或更好的应用程序运行时性能。我们先看看它在哪?

$ find $ANDROID_HOME -type f -name d8
/home/lenox/Android/Sdk/build-tools/30.0.3/d8

然后我们使用 D8 将其转换成 .dex

$ /home/lenox/Android/Sdk/build-tools/30.0.3/d8 --debug --classpath /home/lenox/Android/Sdk/platforms/android-34/android.jar --output . com/example/androidlibrary/*.class
$ tree
.
├── classes.dex
└── com
    └── example
        └── androidlibrary
            ├── Java8$Consumer.class
            ├── Java8.class
            └── Java8.java

4 directories, 4 files

这里我们没有指定 --min-api, 默认值为 1, 这意味这我们可以在任何版本的Android设备上使用这个 Dex

对比 DX(前) 和 D8(后) 生成的 Dex结构

img

img

Java8 的支持方式明显不同

AGP-3.4.0版本开始,Android Gradle Plugin 将使用 R8 替代 Proguard 执行编译时代码优化, R8 也是一款将 .class 字节码转换为 .dex 字节码的工具,相比 D8 而言,他是一款正对整个程序的代码裁减,混淆,优化工具,也就是 shrink, obfuscation, optimization, 是 Proguard 的替代品,它将生产出优化后的 .dex 字节码,体积更小

- Shrink: 他将检测并安全移除无用的代码(classes, fields, methods, and attributes)和资源
- Obfuscation: 混淆,即缩短类和成员的命名,以减少DEX文件的大小
- Optimiaztion: 优化,他将检查并重写你的代码以提升运行时性能,并且可以进一步减少DEX文件的大小

R8工具并不能直接在Android SDK里找到,我们可以通过这个文档进行下载安装

这里我们调整了一些 Java8 类中的代码,增加了一段 if else 代码块

 package com.example.androidlibrary;
 
 import android.os.Build;
 import android.util.Log;
 
 public class Java8 {
 
     public interface Consumer {
         void accept(int i);
     }
 
     public static void repeat(int upper, Consumer consumer) {
         for (int i = 0; i < upper; i++) {
             consumer.accept(i);
         }
     }
 
     public static void main(String[] args) {
+         final String tag;
+         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)  {
+             tag = ">=21";
+         } else {
+             tag = "<21";
+         }
         repeat(100, (i) -> Log.i(tag, String.valueOf(i)));
     }
 }

使用 javac 重新编译

$ javac -cp /home/lenox/Android/Sdk/platforms/android-34/android.jar com/example/androidlibrary/Java8.java
$ tree
.
└── com
    └── example
        └── androidlibrary
            ├── Java8$Consumer.class
            ├── Java8.class
            └── Java8.java

4 directories, 3 files

接着创建一个混淆配置文件(proguard.cfg)供 R8 使用, 是的, R8 使用的是 Proguard 的配置格式去配置全程序的优化策略

-keepclasseswithmembers class * {
    public static void main(...);
}
-assumevalues class android.os.Build$VERSION {
    int SDK_INT return 21;
}

上面的混淆规则告诉 R8 保留带有main方法的类及其成员,并假定在混淆期间,android.os.Build$VERSION类的SDK_INT字段的值为21

然后我们执行 R8 编译

$ echo $R8_HOME
/home/lenox/r8
$ java -cp $R8_HOME/build/libs/r8.jar com.android.tools.r8.R8 --release --output . --pg-conf proguard.cfg --lib /home/lenox/Android/Sdk/platforms/android-34/android.jar com/example/androidlibrary/*.class
$ tree
.
├── classes.dex
├── com
│   └── example
│       └── androidlibrary
│           ├── Java8$Consumer.class
│           ├── Java8.class
│           └── Java8.java
└── proguard.cfg

4 directories, 5 files

查看生成的DEX的结构,相比 D8 , R8重写了我们的代码并移除了2个类,使得DEX体积更小

img

接着我们查看 Java8 这个类的 .dex 字节码

.class public Lcom/example/androidlibrary/Java8;
.super Ljava/lang/Object;
.source "SourceFile"


# direct methods
.method public static main([Ljava/lang/String;)V
    .registers 3

    sget p0, Landroid/os/Build$VERSION;->SDK_INT:I

    const/4 p0, 0x0

    :goto_3
    const/16 v0, 0x64

    if-ge p0, v0, :cond_13

    .line 0
    invoke-static {p0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;

    move-result-object v0

    const-string v1, ">=21"

    invoke-static {v1, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

    add-int/lit8 p0, p0, 0x1

    goto :goto_3

    :cond_13
    return-void
.end method

R8 在混淆配置文件中得知此次编译过程中 android.os.Build$VERSION类的SDK_INT字段的值为21,所以他重写了代码,移除了永远不会执行的 else, 即以下条件恒定为 true

final String tag;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
   tag = ">=21";
} else {
   tag = "<21";
}