Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

20. Release Optimizations
Written by Antonio Roa-Valverde

App development today favors small apps rather than large ones. This supports popular trends, like entry-level devices and the “internet of things”. Furthermore, smaller apps download, install and run faster, which is important for your business. This chapter will help you keep your apps as small as possible.

In this chapter, you’ll learn how to prepare a build for release. You’ll learn about the optimizations that ProGuard performs and how to translate to a certain level of obfuscation. This adds a minimal layer of security to help prevent reverse engineering or tampering with your app.

In the process, you’ll learn:

  • How to use APK Analyzer.
  • How to leverage optimization rules.
  • How to fix compile and runtime errors.

Using APK Analyzer

APK Analyzer is a tool that inspects your finalized app and determines what contributes to its size. It presents a breakdown of your app’s files. You can see what takes up the most space, along with the total method and reference counts.

Launch the analyzer by selecting Build ▸ Analyze APK, which opens a dialog for your file system. If it isn’t already selected, navigate to your debug folder and select app-debug.apk. Click OK to open APK Analyzer.

Figure 20.1 — Using APK Analyzer
Figure 20.1 — Using APK Analyzer

Note the file size of the current APK. You’ll use this tool again later in the chapter to see the result of your changes.

Enabling an Optimizer

Next, you’ll use an optimizer to evaluate your app size.

Enabling an optimizer is simple. In your app build.gradle, replace buildTypes with the following:

buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
  debug {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
}

Setting minifyEnabled to true enables an optimizer — in this case, R8.

ProGuard Versus R8

Android Studio comes with an optimizer by default: R8. ProGuard was the de facto tool for Android for a long time, while R8 is the newer Google alternative. They’re compatible with each other and perform similar operations to optimize Java bytecode. Both remove unused code, such as methods, fields and classes, and attempt to optimize code for performance.

The optimizer looks at the entry points of your app and maps out the code that the app can reach. It removes the rest, and replaces the names of classes and methods with shorter ones, making for a much smaller APK size!

The trade-off is that using any optimizer results in slower build times. The most common problem you’ll face when enabling Proguard starts with compile errors.

Note: R8 is Google’s recommendation when using a code optimizer. It is also the default option in Android Gradle Plugin 7.*. Through the rest of the chapter notice that when mentioning ProGuard, you’ll be in fact using R8. The main reason for this naming is that R8 works with the ProGuard rule format. If you are interested in a sound comparison between both tools, check out this blog post from the authors of Proguard: https://www.guardsquare.com/blog/comparison-proguard-vs-r8-october-2019-edition.

Even though R8 is now the default tool, you can still use ProGuard in your project if you want to. You’ll find all the needed Gradle configuration in this guide: https://www.guardsquare.com/manual/setup/gradleplugin.

Fixing Compilation Errors

As optimizers do their work, they often mistakenly obfuscate and remove code that they think you’re not using — even when you are. Therefore, as you go along, you’ll need to test that everything still works with ProGuard enabled. The earlier you find problems in the build, the easier it will be to fix them. :]

Sync Gradle, then build and run. Notice that there are already compiler errors:

Figure 20.2 — Compilation Errors
Figure 20.2 — Compilation Errors

The compiler problems include two warnings. Click on the header to reveal more information in Android Studio:

> Task :app:minifyDebugWithR8
AGPBI: {"kind":"warning","text":"Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in /home/user/adva-materials/20-release-optimizations/projects/starter/app/build/outputs/mapping/debug/missing_rules.txt.\n",

...

Missing class com.google.firebase.messaging.TopicOperation$TopicOperations (referenced from: void com.google.firebase.messaging.TopicOperation.<init>(java.lang.String, java.lang.String))
Missing class javax.xml.stream.Location (referenced from: javax.xml.stream.Location org.simpleframework.xml.stream.StreamReader$Start.location and 2 other contexts)
Missing class javax.xml.stream.XMLEventReader (referenced from: javax.xml.stream.XMLEventReader org.simpleframework.xml.stream.StreamReader.reader and 4 other contexts)
Missing class javax.xml.stream.XMLInputFactory (referenced from: javax.xml.stream.XMLInputFactory org.simpleframework.xml.stream.StreamProvider.factory and 2 other contexts)
Missing class javax.xml.stream.events.Attribute (referenced from: javax.xml.stream.events.Attribute org.simpleframework.xml.stream.StreamReader$Entry.entry and 7 other contexts)
Missing class javax.xml.stream.events.Characters (referenced from: javax.xml.stream.events.Characters org.simpleframework.xml.stream.StreamReader$Text.text and 2 other contexts)
Missing class javax.xml.stream.events.StartElement (referenced from: javax.xml.stream.events.StartElement org.simpleframework.xml.stream.StreamReader$Start.element and 3 other contexts)
Missing class javax.xml.stream.events.XMLEvent (referenced from: void org.simpleframework.xml.stream.StreamReader$Start.<init>(javax.xml.stream.events.XMLEvent) and 4 other contexts)

There seems to be issues with Firebase and javax.xml.stream.

Adding “don’t warn” Rules

Don’t warn rules tell Android Studio to ignore warnings. This is dangerous, but if you know for sure that you’re not using part of the code, it can come in handy.

Don’t warn rules work by specifying the package name. * is a wildcard – it doesn’t include sub-packages, whereas ** includes sub-packages. The rules for ProGuard go in proguard-rules.pro.

Sometimes, R8 is giving you some hints about the missing rules that you need to apply, like in this case. Open the following generated file in your app build path: app/build/outputs/mapping/debug/missing_rules.txt. You’ll find the following there:

# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn com.google.firebase.messaging.TopicOperation$TopicOperations
-dontwarn javax.xml.stream.Location
-dontwarn javax.xml.stream.XMLEventReader
-dontwarn javax.xml.stream.XMLInputFactory
-dontwarn javax.xml.stream.events.Attribute
-dontwarn javax.xml.stream.events.Characters
-dontwarn javax.xml.stream.events.StartElement
-dontwarn javax.xml.stream.events.XMLEvent

You just need to copy the lines starting with -dontwarn to your proguard-rules.pro file.

Build and run the project. Great, now those warnings are gone!

Adding Keep Rules

Keep rules tell ProGuard not to obfuscate certain parts of your code. Some options are:

  • keep: Preserves entire classes and class members.
  • keepclassmembers: Preserves just the class members.
  • keepclasseswithmembers: Preserves all classes that have a specified member.

Some other options you can use include keepnames, keepattributes, keep class and keep interface.

The rules are written in a specific template format, which you can find at https://www.guardsquare.com/en/products/proguard/manual/usage#classspecification.

When you first opened proguard-rules.pro, there was some boilerplate code at the top, which consisted mostly of commented-out lines that Android Studio provides, as well as a few enabled lines:

-keep class kotlin.reflect.jvm.internal.** { *; }
-keep class kotlin.Metadata { *; }
-dontwarn com.google.crypto.tink.**

This code allows you to use reflection with cryptography.

Note: If you’re sharing your code, write keep rules as you write your code. Then, be sure to publish them on your site, GitHub or GitLab README page so other developers can easily use your code without any problems.

If you use an AAR that includes some predefined ProGuard rules, R8 will apply them when compiling your project. You can learn more about this mechanism in the official Google documentation: https://developer.android.com/studio/build/shrink-code#configuration-files.

Run your APK Analyzer again. You’ll notice the APK size is much smaller now. That’s because ProGuard has removed all the code you’re not using.

Figure 20.3 — The APK Is Smaller Now
Figure 20.3 — The APK Is Smaller Now

Enabling More Optimizations

At this point, you’ve successfully applied optimizations for your app. However, there are a few more steps you can take for your release version.

ProGuard provides an advanced optimization profile. By default, it isn’t used because it can cause build and runtime errors. You enable advanced optimizations by swapping the default proguard-android.txt with proguard-android-optimize.txt.

To experiment with this in your app, navigate to the app build.gradle. Replace the proguardFiles line in the debug section with the following:

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

The build time will be much longer because ProGuard will perform more analysis and optimizations inside and across methods. You’ll also need to spend more time making sure your app still works as expected after the change.

Sync Gradle, then build the app. Everything still works! You should continue to remove assets and resources that your project doesn’t need.

Shrinking Resources

As long as you’ve set minifyEnabled in the optimizer, you can enable the resource shrinker, which removes unused resources after the code shrinker does its job. It will also remove resources in libraries that you include. To make sure it knows which resources your app uses, remove unused library code to make the resources in the library unreferenced.

You can compress resources that your app does use with the help of a PNG crusher. The PNG crusher should be on by default, but because build types don’t always define this correctly, it’s best to add it explicitly.

To enable both resource shrinking and PNG crushing, add the following to your build.gradle:

    buildTypes {
        release {
            ...
            shrinkResources true
            crunchPngs true
            ...

Other options for resources are to use vector-drawable XML files or to convert your images to a format that allows smaller compression, such as WebP. You can find instructions at https://developer.android.com/studio/write/convert-webp#convert_images_to_webp.

NDK Optimizations

If you’ve been working with NDK, you’ll have an Android.mk file under the project’s jni directory. This file tells the compiler how it should optimize native code. Changing the option is as simple as appending a line in the file, as follows:

LOCAL_CFLAGS  := -O3 

The number after the -O refers to the level of optimization. There are four basic levels:

  • O0: The default option for debug builds, this performs no optimizations. This setting reduces compile time and makes debugging easier because it produces expected results.
  • O1: The default level for release builds. This is the first level of optimization that attempts to reduce code size.
  • O2: This enables all supported compiler optimizations that don’t involve a space-to-speed trade-off. It improves the performance of the generated code, but takes longer to compile.
  • O3: The most aggressive optimization level. It enables the following options: -finline-functions, -funswitch-loops, -ftree-vectorize, -fpredictive-commoning, -fgcse-after-reload, -ftree-partial-pre, -fvect-cost-model and -fipa-cp-clone.

Optimizers like ProGuard may have issues when you call a method from JNI (Java Native Interface). You can often find solutions in the JNI training article, here: https://developer.android.com/training/articles/perf-jni#faq-why-didnt-findclass-find-my-class.

Congratulations, you now know all about the main release optimizations you can do for your app.

A Few Things To Keep in Mind…

The makers of ProGuard, GuardSquare, also have a commercial solution called DexGuard. It minimizes code, but offers more protection regarding its side effect of obfuscation. DexGuard encrypts the classes and strings as well as assets and resource files. It also provides app and device integrity checking, which is important to keep spammers out of your app.

If you want to use ProGuard’s obfuscation to protect proprietary code, this is a good choice. You can find more information at https://www.guardsquare.com/en/products/dexguard.

This chapter focused on release optimizations. You should not use them in place of the code profiling and code tuning stages of your lifecycle. During development, you shouldn’t forget about concepts like putting the nominal case first in a flow control case or breaking out of loops early. You should always use good coding practices when it comes to memory management and performance.

The optimizations you’ve applied in this chapter change your code. You should perform them as part of the end of a development phase, before your app goes to quality assurance. If QA finds problems and you make changes to the optimization configuration, the change needs to be thoroughly tested.

When it comes to developer testing and debugging, it helps to compare the before and after states of your app, and actually look at what the optimizer did to your compiled code. This ensures that the optimizations did what you expected.

For example, if you’re looking for a way to obfuscate or protect the code, just adding optimization might not work. In fact, it makes the logic even more visible in some cases — by unrolling loops, for instance. That’s why it’s always good to check the result of your changes and check how your compiled app looks in the App Store. You’ll learn more about that in Chapter 22, “App Analysis”.

Key Points

  • R8 is the default code shrinker and minification tool in Android.
  • Don’t warn rules ignore warnings and errors.
  • Keep rules allow you to keep the optimizer from touching specific code.
  • Instead of keeping entire classes or large parts of code, keep only the minimum code you need, giving you better optimizations.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.