Additional rule types

R8 lets you add rules that affect the optimization of your app, apart from keep rules. Add these rules in the same proguard-rules.pro file where you maintain your keep rules.

The rules fall into the following categories:

  • Assumptions
    • -assumevalues
    • -assumenosideeffects
  • Other optimizations
    • -convertchecknotnull
    • -maximumremovedandroidloglevel

Assumptions

These rules tell R8 that it can make specific assumptions about specific code behavior at runtime.

-assumevalues

The -assumevalues rule tells R8 that the value of a field, or a method's return value, is always a specific constant or falls within a defined range at runtime. -assumevalues is intended for things like flag values which at build time are known to have specific values at runtime.

R8's standard static analysis might not be able to determine the runtime values of members. With -assumevalues, you tell R8 to assume the specified value or range when optimizing the code. This lets R8 perform aggressive optimizations.

The syntax for -assumevalues is similar to the syntax for keeping a member_specification, but additionally includes a return clause as follows:

<member_specification> return <value> | <range>

The <value> and <range> arguments support the following values and types:

  • Special values: true, false, null, @NonNull
  • Primitive values: int
  • Static field references (including enum fields)

To define a range, use the inclusive min..max format. For example, the following snippet shows that the variable CUSTOM_VAL accepts 26 to 2147483647:

-assumevalues public class com.example.Foo {
    public static int CUSTOM_VAL return 26..2147483647;
}

You can use this rule in the following situations:

  • For libraries: To ensure that when apps are optimized all the local debugging hooks are removed from public library code.
  • For apps: To remove things like debug code from a release app. It's preferable to use build variants and variants of specific source sets or constants, but if variant source sets don't work for your case, or if you need a stronger guarantee that the code paths are fully removed, use -assumevalues.

The following example shows a class where R8 removes debug tools from the optimized version of an app:

package com.example;

public class MyConfig {
    // This field is initialized to false but is overwritten by a resource
    // value or other mechanism in the final build process. R8's static analysis
    // might see the initial 'false' but the runtime value is known to be
    // 'true'.
    public static final boolean IS_OPTIMIZED_VERSION = false;
}

// In another class:
public void initFeatures() {
    if (MyConfig.IS_OPTIMIZED_VERSION) {
        System.out.println("Starting optimized features...");
        android.util.Log.d(TAG, "Starting optimized features...");
        initOptimizedService();
    } else {
        android.util.Log.d(TAG, "Starting debug/logging features...");
        initDebugTools();
    }
}

The following rule shows how to tell R8 that the variable IS_OPTIMIZED_VERSION is always expected to be set to true.

-assumevalues class com.example.MyConfig {
    public static final boolean IS_OPTIMIZED_VERSION return true;
}

-assumenosideeffects

The -assumenosideeffects rule tells R8 that it can assume that specified members have no side effects. R8 can completely remove calls to such methods that have no return values or that return a fixed value.

The syntax for -assumenosideeffects is similar to the syntax for keeping a member_specification.

The following sample shows how to tell R8 that all public static methods named log within the DebugLogger class should have no side effects, which lets it remove calls to these methods.

-assumenosideeffects class com.example.DebugLogger {
    public static void log(...);
}

Other optimizations

These are some more advanced optimizations that are not enabled by default. When enabling them you allow R8 to optimize code as instructed, in addition to default optimizations.

-convertchecknotnull

You can use the -convertchecknotnull rule to optimize null checks. This applies to any method which takes an object parameter and throws if the object is null, similar to a standard Kotlin assertion. The exception type and message aren't necessarily the same, but the conditional crashing behavior is.

If a -convertchecknotnull rule matches a given method, each call to that method is replaced by a call to getClass() on the first argument. The calls to getClass() serve as a replacement null check and let R8 remove any extra arguments of the original null check, such as expensive string allocations.

The syntax for -convertchecknotnull is as follows:

-convertchecknotnull <class_specification> {
   <member_specification>;
}

For example, if you have class Preconditions with the method checkNotNull as follows:

class Preconditions {
    fun <T> checkNotNull(value: T?): T {
        if (value == null) {
            throw NullPointerException()
        } else {
            return value
        }
    }
}

Use the following rule:

-convertchecknotnull class com.example.package.Preconditions {
  void checkNotNull(java.lang.Object);
}

The rule converts all calls to checkNotNull() to calls to getClass on the first argument. In this example, a call to checkNotNull(bar) is replaced by bar.getClass(). If bar were null, bar.getClass() would throw a NullPointerException, achieving a similar null-checking effect but more efficiently.

-maximumremovedandroidloglevel

This rule type removes Android logging statements (like Log.w(...) and Log.isLoggable(...)) at or below a certain log level.

The syntax for maximumremovedandroidloglevel is as follows:

-maximumremovedandroidloglevel <log_level> [<class_specification>]

If you don't provide the optional class_specification, R8 applies log removal to the entire app.

The log levels are as follows:

Log label

Log level

VERBOSE

2

DEBUG

3

INFO

4

WARNING

5

ERROR

6

ASSERT

7

For example, if you have the following code:

class Foo {
  private static final String TAG = "Foo";
  void logSomething() {
    if (Log.isLoggable(TAG, WARNING)) {
      Log.e(TAG, "Won't be logged");
    }
    Log.w(TAG, "Won't be logged");
    Log.e(TAG, "Will be logged");
  }
}

With the following rule:

# A level of 5 corresponds to a log level of WARNING.
-maximumremovedandroidloglevel 5 class Foo { void logSomething(); }

The optimized code is as follows:

class Foo {
  private static final String TAG = "Foo";
  void logSomething() {
    Log.e(TAG, "Will be logged");
  }
}

If you provide multiple maximum log levels for the same method, R8 uses the minimum level. For example, given the following rules:

-maximumremovedandroidloglevel 7 class ** { void foo(); }
-maximumremovedandroidloglevel 4 class ** { void foo(); }

then the maximum removed log level for foo() is 4.