This is the 20th day of my participation in the Genwen Challenge

This article is part 7 of jakewharton’s series on D8 and R8.

  • R8 Optimization: Value Assumption
  • Originally written by jakewharton
  • Translator: Xiao Wei

In the first two articles, we looked at some of the optimizations R8 made for the data flow handling of variables, such as whether variables are always empty or not, and then related code optimizations to remove useless judgment branches.

Another optimization of R8 is to track usage ranges where variables may be empty. If any of the criteria are always true, unused dead-codes and redundant judgment branches are eliminated and optimized.

In the sample code at the end of the previous article, the args variable is called by first and then checked for space before printing.

final class Nulls {
  public static void main(String[] args) {
    System.out.println(first(args));
    if (args == null) {
      System.out.println("null!"); }}public static String first(String[] values) {
    if (values == null) throw new NullPointerException("values == null");
    return values[0]; }}Copy the code

The value of the args parameter in the above example may be null or non-null.

System.out.println(first(args/* [null, non-null] */));
if (args/* [null, non-null] */= =null) {
  System.out.println("null!");
}
Copy the code

In this case, R8 cannot do any optimization for this condition because the ARGS parameter may be null or non-null. However, if the input args argument is already checked in the first function and an exception is thrown if the argument is empty, then the method calling args after the first function can be optimized.

System.out.println(first(args/* [null, non-null] */));
if (args/* [non-null] */= =null) {
  System.out.println("null!");
}
Copy the code

As shown above, args is always non-empty after checking, so the judgment condition is always false, so dead-code can be optimized out.

System.out.println(first(args/* [null, non-null] */));
if (false) {
  System.out.println("null!");
}
Copy the code

After passing the check in the first function, the subsequent use of the args argument is not empty. Note that checking whether integer data is positive or negative is not optimized by R8. So, is there a way to manually help R8 determine other types of ranges?

1. We are working on the Assumption that we are working on the same assumptions.

R8 uses the same configuration syntax as Proguard to simplify migration. After the migration, however, you can use some r8-specific flags. Let’s take a quick look at one flag: -assumeValues.

Using the -assumeValues flag, you can specify that when R8 processes a particular field or method, the value of the field or the return value of the method is assumed to be in a specific value or range, so that R8 can judge against the assumed value and simulate some constant conditions.

class Count {
  public static void main(String... args) {
    count = 3;
    sayHi();
  }

  private static int count = 1;

  private static void sayHi(a) {
    if (count < 0) {
      throw new IllegalStateException();
    }
    for (int i = 0; i < count; i++) {
      System.out.println("Hi!"); }}}Copy the code

In the above example, there is a static field count that controls Hi! Print times of. After Compiling (compiled), dexing (dex packed) with R8, we looked at the bytecode and found that count < 0 was still there.

$ javac *.java

$ cat rules.txt
-keepclasseswithmembers class * {
  public static void main(java.lang.String[]);
}
-dontobfuscate

$ java -jar r8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    --pg-conf rules.txt \
    *.class

$ $ANDROID_HOMEDex [000148] count.main :([Ljava/lang/String;)V 0000: const/4 v2,#int 30001: sput v2, LCount; .count:I 0003: sget v2, LCount; .count:I 0005: if-ltz v2, 0017 0007: const/4 v2,#int 00008: sget v0, LCount; .count:I 000a: if-ge v2, v0, 0016 000c: sget-object v0, Ljava/lang/System; .out:Ljava/io/PrintStream; 000e: const-string v1,"Hi!"0010: invoke-virtual {v0, v1}, Ljava/io/PrintStream; .println:(Ljava/lang/String;) V 0013: add-int/lit8 v2, v2,#int 10015: goto 0008 0016: return-void 0017: new-instance v2, Ljava/lang/IllegalStateException; 0019: invoke-direct {v2}, Ljava/lang/IllegalStateException; .<init>:()V 001c: throw v2Copy the code

As you can see from the bytecode above, R8 is optimized to inline sayHi in the main function. Give count a value of 3 at 0000-0001, then read the value of count at 0003-0005 to see if it is less than 0, and throw an exception at 0017 if it is less than 0. Instead, the loop is performed at 0007-0015, with a return at 0016.

To get R8 to remove the < 0 judgment, you need to analyze how the entire program interacts with count. While we could do this in this small example, in a real program, the task would be very complex.

Because this is application code under our control, we know more about the count fields that R8 cannot infer. Add the -assumeValues flag to rules.txt to give R8 the expected range of the value count.

-keepclasseswithmembers class * { public static void main(java.lang.String[]); } -dontobfuscate +-assumevalues class Count { + static int count return 0.. 2147483647; +}Copy the code

Just like the null logic, R8 can also determine the range of count.

if (count/ * * / [0.. 2147483647] < 0) {
  throw new IllegalStateException();
}
for (int i = 0; i < count/ * * / [0.. 2147483647]; i++) {
  System.out.println("Hi!");
}
Copy the code

Because we specify the range of count, it is always positive, so < 0 is redundant, so it becomes dead-code and is optimized.

if (false) {
  throw new IllegalStateException();
}
Copy the code

We specify R8 to compile with the new obfuscation file.

[000128] Count.main:([Ljava/lang/String;)V
0000: const/4 v2, #int 30001: sput v2, LCount; .count:I 0003: sget v2, LCount; .count:I 0005: const/4 v2,#int 00006: sget v0, LCount; .count:I 0008: if-ge v2, v0, 0014 000a: sget-object v0, Ljava/lang/System; .out:Ljava/io/PrintStream; 000c: const-string v1,"Hi!"000e: invoke-virtual {v0, v1}, Ljava/io/PrintStream; .println:(Ljava/lang/String;) V 0011: add-int/lit8 v2, v2,#int 1
0013: goto 0006
0014: return-void
Copy the code

In the bytecode above, 0000-0001 is an assignment, 0005-0013 is a loop, and 0014 is a return.

2. Side-effects

In the bytecode of the example above, even though the value of index 0003 is never actually used (it is overridden to 0 next), it is still read and loaded. In previous articles, we learned that R8 removes useless code, such as using static members in the example above, but why isn’t it optimized here?

When R8 optimizes code based on -assumeValues, it explicitly preserves method or field reads of values, because this field may have some other effect on method calls that, if removed, may change the function’s function. A reading of a field can also cause a static class to be loaded. If we change the -assumeValues tag to the -assumenosideEffects tag for compilation the code at 0003 will be optimized away.

3. Build.VERSION.SDK_INT

As an Android developer, we usually get the VERSION number of the running device based on build.version.sdk_int and then change the implementation of some functionality in the application or class library.

if (Build.VERSION.SDK_INT >= 21) {
  System.out.println("21+ :-D");
} else if (Build.VERSION.SDK_INT >= 16) {
  System.out.println("16 + : -)")}else {
  System.out.println("Pre-16 :-(");
}
Copy the code

Also, we use -assumeValues to set preset values so that R8 can remove useless checks.

-assumevalues class android.os.Build$VERSION {
  int SDK_INT return21.. 2147483647; }Copy the code

By setting the range, some of the above criteria are redundant.

if (Build.VERSION.SDK_INT/ * * / [21.. 2147483647]> =21) {
  System.out.println("21+ :-D");
} else if (Build.VERSION.SDK_INT/ * * / [21.. 2147483647]> =16) {
  System.out.println("16 + : -)")}else {
  System.out.println("Pre-16 :-(");
}
Copy the code

According to the range we specify, two of the above checks are always true.

if (true) {
  System.out.println("21+ :-D");
} else if (true) {
  System.out.println("16 + : -)")}else {
  System.out.println("Pre-16 :-(");
}
Copy the code

The first judgment branch is always true, so the following judgment branches are redundant, leaving only the first judgment:

System.out.println("21+ :-D");
Copy the code

During the daily coding we developed, there was no case where the API version was lower than the minimum SDK version. The Android Lint tool will be checked with obsoletesdkint (you should set it to Error!). .

These conditions are more common in libraries because they tend to support a larger range of apis than using applications, ensuring that the library is used across all VERSIONS of the API.

3. AndroidX Core

Whether you know it or not, SDK_INT interpretation exists almost everywhere in your App. Because AndroidX (formerly known as the Compat library) exists in almost any App, it does version checking via SDK_INT. The lowest version AndroidX supports is API 14, which should be compatible with many apps.

// ViewCompat.java
public static boolean hasOnClickListeners(@NonNull View view) {
  if (Build.VERSION.SDK_INT >= 15) {
    return view.hasOnClickListeners();
  }
  return false;
}
Copy the code

Whether or not you use aN API, they are conditional, and generally making compatible code requires even more conditional.

// ViewCompat.java
public static int getMinimumWidth(@NonNull View view) {
  if (Build.VERSION.SDK_INT >= 16) {
    return view.getMinimumWidth();
  }

  if(! sMinWidthFieldFetched) {try {
      sMinWidthField = View.class.getDeclaredField("mMinWidth");
      sMinWidthField.setAccessible(true);
    } catch (NoSuchFieldException e) { }
    sMinWidthFieldFetched = true;
  }
  if(sMinWidthField ! =null) {
    try {
      return (int) sMinWidthField.get(view);
    } catch (Exception e) { }
  }
  return 0;
}
Copy the code

Although few, if any, applications actually require implementations prior to API 16, legacy implementations after the first IF are still in Apk. Even some compatibility implementations require entire classes to support.

// DrawableCompat.java
public static Drawable wrap(@NonNull Drawable drawable) {
  if (Build.VERSION.SDK_INT >= 23) {
    return drawable;
  } else if (Build.VERSION.SDK_INT >= 21) {
    if(! (drawableinstanceof TintAwareDrawable)) {
      return new WrappedDrawableApi21(drawable);
    }
    return drawable;
  } else {
    if(! (drawableinstanceof TintAwareDrawable)) {
      return new WrappedDrawableApi14(drawable);
    }
    returndrawable; }}Copy the code

As you can see from the example above, WrappedDrawableApi21 is used if the minimum SDK is less than 23, and WrappedDrawableApi14 is used if the minimum SDK is less than 21.

There are over 850 SDK_NIT checks for each API in the AndroidX core library, and more in the AndroidX library. We usually use static helpers in our apps for checking, but these apis are usually used by other libraries such as RecyclerView, Fragment, CoordinatorLayout and all versions of AppCompat.

Using -assumeValues allows R8 to remove and optimize unused methods, resulting in fewer classes, fewer methods, fewer fields, and less code.

4. Zero-overhead Abstraction (0)

These articles are all about the impact of R8 on code optimization, and of course this article is about R8. We introduced checking on AndroidX with SDK_INT. If we set the minimum SDK high enough, R8 will eliminate the conditions in Compat.

import android.os.Build;
import android.view.View;

class ZeroOverhead {
  public static void main(String... args) {
    View view = new View(null);
    setElevation(view, 8f);
  }
  public static void setElevation(View view, float elevation) {
    if (Build.VERSION.SDK_INT >= 21) { view.setElevation(elevation); }}}Copy the code

If the above code had been set in minimum SDK 21 and the range of SDK_INT with -assumeValues, we could see that the optimized setElevation method was left with the body of the method.

$ javac *.java

$ cat rules.txt
-keepclasseswithmembers class * {
  public static void main(java.lang.String[]);
}
-dontobfuscate
-assumevalues class android.os.Build$VERSION {
  int SDK_INT return21.. 2147483647; } $ java -jar r8.jar \ --lib$ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    --pg-conf rules.txt \
    *.class

$ $ANDROID_HOME/build-tools/28.0.3/dexdump -d classes.dex [00013c] zerooverhead. main:([Ljava/lang/String;)V 0000: new-instance v1, Landroid/view/View; 0002: const/4 v0,#int 00003: invoke-direct {v1, v0}, Landroid/view/View; .<init>:(Landroid/content/Context;) V 0006: sget v0, Landroid/os/Build$VERSION; .SDK_INT:I 0008: const/high16 v0,#int 1090519040000a: invoke-virtual {v1, v0}, Landroid/view/View; .setElevation:(F)V 000d: return-voidCopy the code

After R8 processing, the static method setElevation has disappeared from the bytecode, and in the main method, the view. setElevation method is called directly at 000a.

When useless judgment conditions are removed by setting -assumevalues, it is easy for the static method setElevation to reach the threshold for inline methods. When the extra method calls and conditions are no longer in play, you can completely eliminate the cost of those extra method calls and conditions.

5. No Configuration Necessary

If you read the Post on VM-Specific workarounds, you’ll remember that D8 and R8 have a — min-API flag, Set this tag to specify the minimum SDK version when the Android Gradle Plugin (AGP) calls D8 or R8. The build.version.sdk_int rule provided in R8 VERSION 1.4.22 (corresponding to AGP 3.4 beta 1) contains the — min-API flag.

-assumevalues public class android.os.Build$VERSION {
  public static int SDK_INT return <minApi>..2147483647;
}
Copy the code

The tool doesn’t have to know about this R8 feature or enable it manually with the Minimum SDK version, but instead enables it by default so that everyone gets smaller Apk and better runtime performance.

6. Summary

Defining a range for SDK_INT is the most compelling demonstration of value assumptions to date, and it now has a positive impact on Apk when enabled by default. Will the iseditmode () tag to false is another useful default values, but issuetracker.google.com/issues/1117… An exception may occur. Other examples may vary from application to application or vary in performance depending on the library used.

The next article in this series will cover some optimization of R8 applied to constant values.