Chapters

Hide chapters

Real-World Android by Tutorials

First Edition · Android 10 · Kotlin 1.4 · AS 4

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

20. Release Optimizations
Written by Kolin Stürt

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, ProGuard.

ProGuard versus R8

Android Studio comes with two main optimizers: ProGuard and R8. ProGuard has been the de facto for Android for a long time, while R8 is a more recent addition. 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.

Developers like to use the “latest and greatest” features. This is great when you’re learning or experimenting with a small personal app or startup company. But those latest and greatest features are often underdeveloped. For enterprise-level production apps, you’ll want time-tested, robust solutions.

In this case, the ProGuard optimizer has more than 15 years of development, while the Android team for R8 is young. That means ProGuard has more optimizations and more correct debug information from optimized stack traces. It has more support for backporting and is still faster than R8.

While optimizer you choose is ultimately up to you. In the future, the choice will be one of philosophical debate.

In recent versions of Android Studio, R8 is the default optimizer. Head to gradle.properties and you’ll see the following:

android.enableR8=false
android.enableR8.libraries=false

Adding those lines to a project disables R8 and uses ProGuard, instead. For this chapter, you’ll use those settings to work with ProGuard.

ProGuard 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.

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:

  • okhttp3
  • Can’t find referenced class org.sl4j
  • library class android.content.Intent depends on program class org.xmlpull.v1.XmlPullParser

The first step to solving these problems is to silence issues for code you’re not using.

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.

If you know you aren’t going to use a feature that makes Android Studio complain, you can just ignore it. In this app, you know you’re not using Java’s XML stream feature. For the XML stream compile error, it’s safe to ignore the issues.

To do this, add the following to the end of proguard-rules.pro:

-dontwarn javax.xml.stream.**

That takes care of the easy problems where you already know the solution. Next up are the okhttp errors.

Solving the okhttp errors

When you’re faced with unknown errors, the first step to solve them is to research them online. Popular libraries often publish ProGuard/R8 rules on their sites, so you’ll start your research there.

A quick search brings you to https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro. From there, you can also find rules at https://github.com/square/okio/blob/master/okio/src/jvmMain/resources/META-INF/proguard/okio.pro.

Now that you have the rules, add them to proguard-rules.pro:

# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*
-dontwarn okio.**

# OkHttp platform used only on JVM and when Conscrypt dependency is available.
-dontwarn okhttp3.**
-dontwarn org.conscrypt.ConscryptHostnameVerifier

This made it pretty easy to add the rules you need to ignore warnings for these classes. But if you’re working with a less popular library, finding a solution might take a bit more time.

You’ll handle the sl4j error next, which will show you what to do in that case.

Solving the sl4j error

Head to the Bubble Picker library’s GitHub page at https://github.com/igalata/Bubble-Picker to see if there’s any documentation about using the library with ProGuard. In the previous cases, the README page had ProGuard information, but this library doesn’t.

You’ll have to dig a little deeper. So next, select Issues.

In the search field, remove is:open and add sl4j, then press Enter.

Here’s some good luck – issue #61, https://github.com/igalata/Bubble-Picker/issues/61, looks like the same issue, with suggestions to add some don’t warn exceptions for ProGuard.

Add the following to the end of proguard-rules.pro to ignore warnings for org.slf4j:

-dontwarn org.slf4j.**

Select Build ▸ Make Project and you’ll see that most of the errors you’ve addressed are gone.

In forums, you might see suggestions to use -dontwarn *, but that’s very bad practice. It translates to: don’t warn all. It will fix irrelevant warnings, but also ignore critical ones indicating something’s actually wrong.

It’s better to tell ProGuard not to optimize code that’s problematic instead of ignoring the warnings. You’ll do that next.

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. When you built the project, there was one error left, related to xmlpullparser. This interface is part of the Android API and doesn’t include solutions in GitHub pages or issues.

For these kinds of tasks, check for solutions in forums, like Stack Overflow. In this case, searching for the error leads to https://stackoverflow.com/questions/5333830/android-proguard-error-with-org-xmlpull-v1-xmlpullparser. Someone has already come up with a solution.

Add the following lines to the file to try it out:

-dontwarn org.kobjects.**
-dontwarn org.ksoap2.**
-dontwarn org.kxml2.**
-dontwarn org.xmlpull.v1.**

-keep class org.kobjects.** { *; }
-keep class org.ksoap2.** { *; }
-keep class org.kxml2.** { *; }
-keep class org.xmlpull.** { *; }

Inside the curly braces, you told ProGuard to match any method name. The format is the same as the don’t warn rules. It’s best practice to use explicit keep rules, rather than keeping the entire class.

Go back to the line you previously added:

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

Here, instead of preserving the entire library, you only kept the names of the PublicSuffixDatabase sub-package. These allow you to write more advanced rules. For example, if you need to keep the class members of any class that extends protobuf.GeneratedMessageLite, such as the encrypted shared preferences, you could write the following:

-keepclassmembers class * extends com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite {
  <fields>;
}

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.

An Android Library (AAR) has a transparent method that retrieves published keep rules automatically. See https://bit.ly/2O7gglz to learn how to take advantage of this.

Select Build ▸ Make Project. Now, it builds successfully!

Figure 20.3 — Build Successful
Figure 20.3 — Build Successful

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.4 — The APK Is Smaller Now
Figure 20.4 — The APK Is Smaller Now

Now that your project builds, the next step is to run the app to make sure everything still works.

Fixing runtime errors

Build and run the app. Uh, oh — the app crashes with a ClassNotFoundException!

Figure 20.5 — Crash at Runtime
Figure 20.5 — Crash at Runtime

This time, searching online won’t find anything useful. You’ll fix the problem without the help of online research.

Note that several methods in your stack trace are obfuscated – the names are changed and minified. This is one of ProGuard’s key features.

Check the output log to narrow down what the problem is. In the Run tab, you can see that it has something to do with the User object.

It’s good practice to add sufficient logging in your catch statements, nullability checks and error states. With ProGuard, this is crucial. When a problem occurs, it will help lead you or other developers to the root of the issue, especially when ProGuard obfuscated the method names in the stack traces, as it’s done here.

Open User.kt and notice the annotations, such as @Root and @field. These annotations are for SimpleXML, which works by loading XML entities presented at runtime, then instantiating Kotlin counterparts. Kotlin can only do this by using introspection and reflection — features of the language that inspect objects and call methods dynamically at runtime.

ProGuard looks at the static version of your app, but doesn’t actually run it, so it can’t know which methods are reachable using introspection and reflection. You can see that ProGuard takes long class names and replaces them with smaller names. If something tries to reference a name at runtime with a constant string, it can’t because the name changed.

You can often tell this is happening when you see either ClassNotFoundException or NoSuchMethodException. You need to tell ProGuard to keep the sections that use reflection.

Adding annotations

The Annotations Support Library lets you add @Keep to methods and classes you want to preserve. This is a great feature because it acts like documentation. The ProGuard information sits above your method, as opposed to being in a separate file. Adding @Keep to a class will preserve the entire class. Adding @Keep to a method or field will keep the name of the method or field as-is.

In User.kt and Users.kt, add a @Keep annotation to the top of the object definitions, like this:

@Keep
@Root(name = "user", strict = false)
...

and

@Keep
@Root(name = "users", strict = false)
...

Build and run. You’ll now see the login screen and animals again.

For analysis, you can deobfucate optimized stack traces with a mappings file. You’ll learn how to do that in Chapter 22, “App Analysis”.

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.

Another thing you can do is exclude groups and modules that you’re sure you won’t use. Navigate to the list of dependencies in the app build.gradle. Replace the line at the end for simplexml with the following:

implementation ('com.squareup.retrofit2:converter-simplexml:2.7.1') {
  exclude group: 'xpp3', module: 'xpp3'
  exclude group: 'stax', module: 'stax-api'
  exclude group: 'stax', module: 'stax'
}

Sync Gradle, then build the app. Everything still works, but you’ve excluded those parts of simplexml that you’re not using. 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

  • You can choose between ProGuard and R8 in gradle.properties.
  • 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.