[译] R8优化:类反射和强制内联

1,616 阅读5分钟

这是我参与更文挑战的第 24 天,活动详情查看: 更文挑战

原文出自 jakewharton 关于 D8R8 系列文章第 11 篇。

上一篇关于 R8 的文章介绍了自动消除重复代码的方法 Outline,但是这篇文章不契合方法常量操作 Class Constant Operations那片文章的结尾描述,所以让我们回到正轨吧。

R8 会将 MyActivity.class.getSimpleName() 这类的调用直接优化成 MyActivity。这种写法在日志标记的场景中出现的,您可以编写该表达式而不是字符串文本,这样即使在混淆之后也始终反映实际的类名。而 MyActivity.class 文本是固定的场景中非常有效,但在实例上使用时不起作用。

1. Instance reflection 示例反射

处理实例时,通过调用 getClass() 而不是 MyActivity.Class 来获取类引用。这种写法尽管不繁琐,但仍然值的反思。

class MyActivity extends Activity {
  @Override void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    String name = this.getClass().getSimpleName();
    Log.e(name, "Hello!");
  }
}

getClass() API 只是 Object 类的普通方法,在字节码中显示为普通的虚拟调用(invoke-virtual)。

[0003d0] MyActivity.onCreate:(Landroid/os/Bundle;)V
0000: invoke-super {v1, v2}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V
0003: invoke-virtual {v1}, Ljava/lang/Object;.getClass:()Ljava/lang/Class;
0006: move-result-object v2
0007: invoke-virtual {v2}, Ljava/lang/Class;.getSimpleName:()Ljava/lang/String;
000a: move-result-object v2

因为 R8 执行整个程序分析,即使 MyActivity 没有标记为 final,但是它知道 MyActivity 没有子类型,因此,它可以用 MyActivity.class 替换对 this.getClass() 的调用。

 [000170] MyActivity.onCreate:(Landroid/os/Bundle;)V
 0000: invoke-super {v1, v2}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V
-0003: invoke-virtual {v1}, Ljava/lang/Object;.getClass:()Ljava/lang/Class;
-0006: move-result-object v2
+0003: const-class v2, Lcom/example/MyActivity;
 0005: invoke-virtual {v2}, Ljava/lang/Class;.getSimpleName:()Ljava/lang/String;
 0008: move-result-object v2

除此之外,Class<?> 引用立即调用 getSimpleName()。因此,上一篇文章中介绍的优化现在可以应用于只生成简单的常量字符串。

但是当这个类是已知明确的,你多久编写一次这个 .getClass()

为了与日志记录的示例保持一致,让我们看看一个示例测试库,它接受一个 Activity 和一个可选的名称以用于日志记录。

class SomeLibrary {
  static SomeLibrary create(Activity activity) {
    return create(activity, activity.getClass().getSimpleName());
  }

  static SomeLibrary create(Activity activity, String name) {
    return new SomeLibrary(activity, name);
  }

  private SomeLibrary(Activity activity, String name) {
    // ...
  }

  void doSomething() {
    Log.d(name, "Starting work!");
    // ...
  }
}

如果未提供名称,则使用 getClass().getSimpleName()Activity 类名推断名称。由于输入不是固定的类文本,因此在编译时不能用字符串替换。

class MyActivity extends Activity {
  private SomeLibrary library;

  @Override void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    library = SomeLibrary.create(this);
  }

  @Override void onResume() {
    library.doSomething();
  }
}

方法体的内联在以前的 R8 文章中一直是一个主要内容,因为它经常解锁那些否则不会应用的优化。这个例子在这方面没有什么不同,但它又是不同的,因为 create(Activity) 方法太大,无法正常内联。对 getClass()getSimpleName()create() 重载的三个方法调用,以及为这些方法指定的参数,超过了内联候选方法允许的最大方法体大小。

2. 强制内联(Inlining)

R8 宣称它的配置规则与 ProGuard(它要替换的工具)的配置规则兼容。但除了尊重 ProGuard 所支持的内容外,它确实有自己的未成文规则。这方面的一个例子可以从 Value Assumption 帖子中看到(ProGuard 后来加入了对该规则的支持!)。虽然没有文档记录,但 R8 支持此规则。

另一个未记录的、特定于 R8可以帮助指导内联的指令是 -alwaysinline 。此指令重写了内联方法体的常规内联限制,否则可能不会考虑这些限制。不幸的是,这个规则没有文档化有一个很好的理由:它是完全不受支持的,应该只用于测试目的。

通过使用 -alwaysinline 指令, create(Activity) 方法可以被强制内联。

-alwaysinline class com.example.SomeLibrary {
  static void create(android.app.Activity);
}

这样就会让 getClass().getSimpleName() 内联移动到每个调用处的位置。

 @Override void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
-  library = SomeLibrary.create(this);
+  library = SomeLibrary.create(this, this.getClass().getSimpleName());
 }

所以在上面的场景中,其中封装类在编译时是已知的。它将被 MyActivity.class 文本替换,然后很快被 MyActivity 字符串文本替换。

 @Override void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
-  library = SomeLibrary.create(this, this.getClass().getSimpleName());
+  library = SomeLibrary.create(this, "MyActivity");
 }

我们再次看到连续优化的威力。别再反省了!

与以前的文章中自动进行内联不同,不支持内联的表达式通过 -alwaysinline 指令在 R8 中可以强制内联,这种使用只有当您知道应用后对字节码的影响时,才应该这样强制内联。在本例中,有可能在编译时无法确定实例,最终导致字节码稍微膨胀。当然,规则的无支持性意味着它可能随时改变或消失。对于稳定的解决方案,Kotlin 的内联函数修饰符具有相同的效果,但仅适用于 Kotlin 调用者。


用类文本替换对 getClass() 的调用是一个非常小的优化。当内联时,它只节省了四个字节,但它最大的贡献是允许应用的其他优化。现在可以消除对 getSimpleName() 等方法的后续调用,更多的可以参照字符串优化这篇文章

在以后的 R8 文章中,我们将回到这个 getClass() 优化以及它所支持的其他优化。但是现在,还有很多其他的 R8 优化,我想在没有承诺下一个特定主题的情况下介绍,所以请继续关注。