Using R8 to reduce APK size in Android

Using R8 to reduce APK size in Android

Application size plays an important role in deciding whether your application will reach the next billion users or not. In most of the cases, the users have very less amount of space in their mobile phones. So, if your APK size is big then you will end up losing your users. It becomes an important task to reduce the APK size as much as possible.

Smaller APK size is always better.

In this blog, we will learn how to reduce the APK size by using R8 in our application. The following topics will be covered in this blog:

  • What is R8 Shrinking?
  • How to enable R8 Shrinking in your app?
  • How does Shrinking work?
  • Closing note

So, let's get started.

What is R8 Shrinking?

R8 shrinking is a process in which we reduce the amount of code of our application and by doing so, the APK size automatically gets reduced. So, reduced APK sized applications are more likely to be kept in people's phone. All we need to do is reduce the class files(i.e. the code) written in our Android application.

To reduce the APK size, we have three different techniques:

  1. Shrinking or Tree Shaking: Shrinking is the process of removal of unreachable code from our Android project. It performs some static analysis to get rid of unreachable code and removes the uninstantiated object.
  2. Optimization: This is used to optimize the code for size. It involves dead code removal, unused argument removal, selective inlining, class merging, etc.
  3. Identifier Renaming: In this process, we obfuscate the class name and other variable names. For example, if the name of the class is "MyAwesomeClass", then it will be obfuscated to "a" or something else but smaller in size.

But many of you might be thinking that if I am making an application then why would I include some extra code if I am not going to use it? And as a result, if I am not including that unwanted code then how will R8 shrink the application size? So, what's the reason behind using this R8 shrinking?

The answer is simple:

  • Third-party library: Every application requires some kind of third-party libraries. But in your application, you might be using a small part of that third-party library and rest of the part is not useful for you. So, why to keep those lines of code? Without any shrinking, all the library codes will be retained in your app. Even if you are writing you app in Kotlin, then you have to include Kotlin Standard Library and this, in turn, increase your APK size.
  • Code optimization: Even if your application doesn't have unwanted lines of code, you can still optimize the code written by you by using some optimization techniques like dead code removal, unused argument removal, selective inlining, class merging, etc.
Note: You must be thinking that these things are already handled by Proguard, then why to use R8? The answer is simple, R8 works with Proguard rules and R8 shrinks the code faster while improving the output size.

How to enable R8 Shrinking in your app?

To enable R8 shrinking in your application, set the minifyEnabled to true in your app's main build.gradle file.

android {
    ...
    buildTypes {
        release {
            minifyEnabled false
        }
    }
}

Try to find the APK size by making a single Activity application and then by using R8 shrinking and after that check for the APK size by including some third-party library before and after using R8 shrinking. You will notice the difference in APK size.

But how does this shrinking work? Let's find.

How does Shrinking work?

The algorithms used for APK size reduction traces the unreachable code and it removes those codes from the APK. This is done with the help of entry-points. So, R8 starts with this entry-point and goes down to all the reachable code and in the end, it removes the unreachable code. To define this entry-points, we have something called the keep rules that we generally write the R8 configuration in the proguard-rule file. Let's have an example of the same:

class com.mindorks.JavaHelloWorld {
    //unreachable code
    private void iAmUnused() {
        System.out.println("I am of no use :( ");
    }

    //reachable code
    private static void reachableExample() {
        System.out.println("Hello, MindOrks :) ");
    }

    //main function
    public static void main(String[] args) {
        reachableExample();
    }
}

And in the rule, we add:

//This is the entry point 
-keep class com.mindorks.JavaHelloWorld {
    public static void main(java.lang.String[]); 
}

In the above code, when the R8 shrinking starts, then the R8 will start tracing from the entry point i.e. the main function will be called and from the main function, the reachableExample() method will be called and finally after printing "Hello, MindOrks :)" the tracing will be stopped. So, the iAmUnused() method is unreachable and it will be removed. Also, at the same time, the name of the method rechableExample() will get reduced to something shorter like a() or b() . And finally, R8 can inline the code of the reachableExample() in the main function. So, the final code will be:

class com.mindorks.JavaHelloWorld {
    
    //main function
    public static void main(String[] args) {
        System.out.println("Hello, MindOrks :) ");
    }
}

Similarly, in Android, we can have various entry points like Activities, Services, Content Providers, Broadcast receivers and to handle these entry points, aapt2 tool is used. For example, in our manifest file, we define MainActivity or other activity as our entry point.

Apart from these entry points provided by aapt2 tools, there are several other things that are required for the Android Platform and these are provided by the Android Studio in the configuration file. Add the below line in your app's main build.gradle file:

minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')

But, there is one problem, when your code contains some reflection code then it is not recognized by R8 by tracing the code. So, it removes that code also and it should not do that. For example,

class Company(val name: String)

fun printJson() {
    val myGson: Gson()
    val compnay = Company("MindOrks")
    print(myGson.toJson(compnay))
}

//output: {}

In the above code, the output is an empty JSON object. This is because the filed "name" is written but never read. Here Gson uses reflection techniques, so R8 doesn't see that this field is actually read and removes the code. So, to avoid this you have to have some keep rules that will tell the R8 to keep the value named "name" and don't delete that. Generally, we put all these keep rules in a file called proguard-rules.pro and add the below lines in your app's main build.gradle file:

minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 
        'proguard-rules.pro'

You can read more about Proguard from here .

Closing note

In this blog, we learned how to reduce the APK size by using R8 in our application. As the app size matters a lot. So, use R8 to reduce that app size and it is easy to implement.

Hope you learned something new today.

Have a look at our Android tutorials here .

Do share this blog with your fellow developers to spread the knowledge. You can read more blogs on Android on our bloggi ng website .

Apply Now: MindOrks Android Online Course and Learn Advanced Android

Happy Learning :)

Team MindOrks!