ProGuard
The ProGuard tool shrinks, optimizes, and obfuscates your code by removing unused code and renaming classes, fields, and methods with semantically obscure names. The result is a smaller sized
.apk
file that is more difficult to reverse engineer.
In the Java world, ProGuard is an optional step of the build process. With Scala on Android it becomes a necessity. You need ProGuard to remove unused classes and methods. The Android toolchain will fail to compile your app if it contains more than 65.536 methods and fields (the so-called 65k limit). This limit is easy to reach when you have the Scala library in your dependencies. Using ProGuard to strip out unused library classes solves this issue. Unfortunately, ProGuard execution is a very time consuming process and its configuration requires a custom syntax.
MultiDex
With Android 5.0 the Android Runtime (ART) replaced the Dalvik Virtual Machine (DVM) as the SDK app execution environment. Already aware of the 65k issue, Google equipped ART with a MultiDex mode that allows one app to contain multiple .dex
files. The .dex
file is the heart of any .apk
. Similar to Java's .class
file, .dex
contains the compiled bytecode of your app. But instead of creating one .class
file for each class (like javac does it), the .dex
file contains the entire byte code of your app (including dependencies). And this is of course the file in question of the 65k limit.
Now, with Android 5.0 the 65k limit still exists, but you may split your bytecode into several .dex
files which theoretically enables you to ignore the limit and to skip ProGuard. This works also for Android devices with an older version than 5.0, because Google provides a support library for the Dalvik Virtual Machine.
Since version 1.3.11, the Android SDK Plugin for SBT does also provide MultiDex support and its usage is demonstrated as a sample application. But you are discouraged to use the MultiDex feature to avoid stripping out the Scala overhead because its usage does not come without issues. MultiDex is intended for large codebases. You should always make sure to strip out the unused Scala library code. This also has the benefit of keeping your .apk
as slim as possible.
Configuration
By default, the Android SDK Plugin for SBT runs ProGuard with a reasonable default configuration whenever you deploy or package your app. If you need to manually enable or disable it, you can use the useProguard in Android := true
sbt option. It is automatically enabled when your app contains Scala sources.
When you pull in external dependencies, ProGuard will likely cause issues, report warnings and abort the build process. This is where you have to step in to add additional configuration rules. When ProGuard raises a warning this usually happens because it detects a reference to a class which does not exist. The Android SDK comes with its own implementation of the Java API which misses out on some classes (e.g. the java.rmi.*
package) so this is not a surprise. A library that supports Android will never call such a missing class when running in this context. It is therefore safe to ignore the warning.
ProGuard may also miss out on classes that are referenced within a library via runtime reflection and remove them too eagerly. If the tool has no chance to detect the usage of a class, it will strip it out and you will encounter ClassDefNotFound
exceptions at runtime. You then have to manually instruct ProGuard to keep this class.
Below is a briefly documented sample ProGuard configuration as it may be added to the sbt build file.
proguardOptions in Android ++=
// Don't remove anything from your app's code. This is especially useful during development!
"-keep class com.example.** { *; }" ::
// Keep certain library packages
"-keep class com.squareup.okhttp.** { *; }" ::
"-keep interface com.squareup.okhttp.** { *; }" ::
// Ignore warnings raised from this class
"-dontwarn okio.Okio" ::
// Keep all test class
"-keep class * extends org.scalatest.FunSuite" ::
// Keep a certain field; being as specific as possible is good practice because it allows
// ProGuard to remove as much unused code as possible which increases your distance to the
// 65K barrier
"-keepclassmembernames class com.some.library.Class { int someField; }" ::
// I've read and understood all notes that are issued by this package
"-dontnote org.scalatest.**" ::
Nil
Make sure to have a look at the ProGuard keeps options for a comprehensive overview.
Example
Okio is an I/O library by square which is also a part of the famous OkHttp library. Depending on Okio causes ProGuard to fail which may look similar to the log output below.
Reading input...
Reading program jar [.../target/android-bin/retrolambda-processed.jar] (filtered)
...
Initializing...
Note: com.fasterxml.jackson.core.type.TypeReference calls 'Class.getGenericSuperclass'
...
Warning: okio.Okio: can't find referenced class java.nio.file.Files
...
Note: ...
Warning: there were 24 unresolved references to classes or interfaces.
You may need to add missing library jars or update their versions.
If your code works fine without the missing classes, you can suppress
the warnings with '-dontwarn' options.
(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedclass)
Warning: there were 5 unresolved references to program class members.
Your input classes appear to be inconsistent.
You may need to recompile the code.
(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)
[trace] Stack trace suppressed: run last android:proguard for the full output.
[error] (android:proguard) java.io.IOException: Please correct the above warnings first.
ProGuard prevents compilation as long as there are warnings. To solve those you need to stick to the information about the affected classes at the top of the log (e.g. Warning: okio.Okio: can't find referenced class java.nio.file.Files
). In your sbt build configuration, you can now use the proguardOptions in Android
key to add ProGuard rules that instruct it to keep certain packages, classes, fields or methods. This is a trial and error process. To exemplarily solve the Okio
warning you would first naively try to keep the java.nio.file.Files
class.
proguardOptions in Android += "-keep class java.nio.file.Files { *; }"
This is usually the first attempt to tackle ProGuard warnings that occur after adding a new dependency. In most cases this is also a sufficient solution. Not in this one, though. Okio is a Java I/O library that also works with Android. The affected class java.nio.file.Files
is part of the Java SDK, but does not exist on Android. Forcing ProGuard to keep the class is therefore not a valid solution because the class does not exist in the first place. Instead we have to rely on Okio to know how to properly handle the Android environment and instead instruct ProGuard to ignore the issue.
proguardOptions in Android += "-dontwarn okio.Okio"
Suppressing warnings is a dangerous undertaking because it may cause runtime errors if your app (or some underlying library) tries to call a class that does not exist. It also hides all further issues related to the okio.Okio
class. Only use it as a last resort to handle ProGuard warnings.
Further reading
- ProGuard
Official project page
- ProGuard Android documentation
Android developer documentation about ProGuard
- ProGuard keeps options
Listing and explanation of all available keep options
- Android documentation: MultiDex
Official documentation
- Android SDK Plugin for SBT sample: MultiDex
GitHub repository