Proguard 101
I wanted to write about every android developer's favorite code shrinking and obsfucation tool: Proguard. I know that when I was first learning to develop on android that information on what Proguard was, how it worked, and how you can configure it were sparse. There are lots of stackoverflow posts telling you to just add some magic words like -keep and -dontwarn to random files and then all will be well.
The goal of this post is to give a quick overview of what proguard does for your app and provide some tips on configuring it for most common use cases and working with it in your app so that hopefully you are doing more than blind copy pasta when you add things to your proguard configuration.
I've created a small android project located here to go along with the post.
It can be used as a little playground to get some first hand experience with the things we'll discuss in this post.
What does it do?
Proguard is a tool that we can enable as part of the build process to do three things for us.
- Code Obfuscation - Makes your code harder to understand/read to help protect against reverse engineering.
- Shrinkage - Removes any unused code so that you don't ship a bloated apk.
- Optimization - Rewrite parts of your code to be more performant
How do I turn proguard on?
You can enable proguard in your build.gradle file with one simple line. In fact the default build.gradle file that android studio generates for you will already have the line in it. You just need to change the value from false to true.
buildTypes {
release {
minifyEnabled true//Defaults to false
}
}
Once you've set that property to true all of your release builds will kick off proguard and allow it to work its magic on your APK.
As an example of how proguard can affect your final APK, here's two screen shots taken from the analyze apk tool in android studio for this post's simple companion project. You can view the state of the project at the point these screenshots were taken here. The app is just launching its main activity and displaying a textview but already you can see a huge change in the size of the APK before and after proguard has been applied.
Before we enabled proguard
After we enabled proguard
You can see in the first picture that the apk includes the Message and MessageSender classes even though there's no code making use of these classes at this point in the project's commit history.
In the second picture you'll notice that those classes are no longer in the APK. Proguard was able to see that those classes were not in use and it made sure they were removed from the final APK.
You'll also notice that the apk size has been reduced from
1.9 MB to just shy of 1 MB. That's not because Message and MessageSender are giant classes full of code. It's because proguard is able to remove unused classes from the libraries our app depends upon just as easily as it can for code we bundle directly in our app.
The other visible side effect we can see now that we've enabled proguard is that in our build/outputs folder we have a new subfolder called mappings. In the mappings folder you'll find the mapping.txt file. The mapping.txt file contains thousands of lines showing what proguard renamed each symbol in your app. Here's a few lines from the mapping.txt file of our sample app.
androidx.appcompat.R$attr -> a.a.a:
androidx.appcompat.R$bool -> a.a.b:
androidx.appcompat.R$color -> a.a.c:
androidx.appcompat.R$dimen -> a.a.d:
androidx.appcompat.R$drawable -> a.a.e:
androidx.appcompat.R$id -> a.a.f:
androidx.appcompat.R$layout -> a.a.g:
androidx.appcompat.R$string -> a.a.h:
androidx.appcompat.R$style -> a.a.i:
androidx.appcompat.R$styleable -> a.a.j:
androidx.appcompat.app.ActionBar -> a.a.a.a:
The mapping file is useful when you use tools like Crashlytics. If you provide your mapping file to Crashlytics then when a crash happens it can use the mapping file to de-obfuscate your symbols. If you don't provide a mapping file to Crashlytics then you're gonna have a hard time making sense of the stack traces that your crash reports will include.
Now that we've seen how to turn proguard on for our project and what it does for us, let's look at how it can blow up in our face. Let's take our Message class and make use of it via reflection in our MainActivity's onCreate method.
val clazz = Class.forName("com.abnormallydriven.proguardbasics.Message")
val constructor = clazz.getConstructor(String::class.java)
val myReflectedMessage = constructor.newInstance("My Message String")
Log.d("ProguardBasics", myReflectedMessage.toString())
If we build and run the above code with our debug app variant then everything will workout just fine. You'll see our message printed to logcat and you might think all is well and ready for release to production. When you build your release variant though and attempt to launch your app you'll crash immediately.
That's because when we build our release variant proguard will come through and do its job of removing unused code and it can't see that we are actually making use of our Message class via reflection. Since it doesn't see our clever reflection trick, it will remove the Message class from our release APK and then our reflection will fail at run time.
What can we do to keep this from happening? Well besides not using reflection in such contrived ways we can actually teach proguard about classes or even just methods and fields within a class that we would like it to leave alone.
Proguard Rule Files
We can inform proguard about how it should handle certain classes using special syntax in proguard rule files. The build.gradle file of your app module will list the name of your proguard rule file. The default should look something like this inside your release build build type
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
We know we want let proguard know that it should keep our Message class even though it thinks it should be removed because we know that we create and use an instance of the class via reflection. We can do this by adding this line to our proguard rule file:
-keep class com.abnormallydriven.proguardbasics.messaging.Message { *; }
Now when we rebuild the release variant of our app, the Message class will be included in the final APK and everything we will work as we intended.
We can go with something even simpler for situtations like this where we want to keep an entire class. We can use the @Keep annotation. This special annotation will ensure the proguard rule we wrote above manually is automatically generated for us.
If you can get away with just using the @Keep annotation I strongly recommend it. It's simpler than writing the rule out in your proguard file and it lives right alongside the code that it applies to.
Here's what our Message class would look like with the @Keep annotation applied.
@Keep
data class Message(val contents: String) {
override fun toString(): String {
return contents
}
}
Now you may be wondering "what if I only want to keep part of a class?". This can come up when you've included a very large library but really only need one small feature or part of it in your app. Let's imagine we have a MessageSender class that has two methods on it, one which we'll make a call to using reflection again and the other we have no real need for in our final APK.
class MessageSender {
fun sendMessage(message: Message){
Log.d("SuperImportant", message.toString())
}
fun methodThatWeNeverUse(){
//and yet it remains in our code
}
}
We'll make use of this class the hard away again in our MainActivity in a method that we call at the end of onCreate()
private fun usePartOfMessageSender() {
val clazz = Class.forName("com.abnormallydriven.proguardbasics.MessageSender")
val constructor = clazz.getConstructor()
val message = Message("Super Important Message")
val myReflectedSender = constructor.newInstance()
val sendMessageMethod = clazz.getDeclaredMethod("sendMessage", Message::class.java)
sendMessageMethod.invoke(myReflectedSender, message)
}
Now if we dont use the @Keep annotation or create a keep rule we'll be in for a crash when we go to run our release apk on a device, but we know we're only making use of the sendMessage() method and we'd really like proguard to get rid of that crusty method we never call named methodThatWeNeverUse(). We can do that again with a simple line in our proguard rule file.
-keep class com.abnormallydriven.proguardbasics.MessageSender {
void sendMessage(com.abnormallydriven.proguardbasics.Message);
}
And just like that we have a functioning APK again with just the code we need and no extra code baggage along for the ride.
You should now be able to create proguard rules for your projects that cover 99% of the use cases you will run into in your day to day android development experience.
Proguard & Multi-Module Apps
Now let's bring our app into the brave new mordern world of multi module android projects by creating a Messaging module and moving MessageSender and Message to that module. When you make the new module you might be tempted to move your proguard rules to the proguard file that gets auto generated in your new module directory. Don't give into that temptation though or you'll find yourself crashing at runtime again.
Proguard runs on the APK that gets produced by our app module and its going to use the rule file in the app module when its making decisions about what to keep and what to drop. You can leave your proguard rules right where they are and everything will continue to work just fine.
Proguard & Library Modules
Now if we decided that our messaging module was not simply a feature module to be used when constructing our app's APK, but was instead a library that we wished to publish to the world and it made internal use of some reflection we can take advantage of the consumerProguardFiles
property of the android gradle plugin. This will make sure that apps which choose to use our library will have our rules appended to the app's rules and make our library's users lives a little easier.
android{
defaultConfig {
consumerProguardFiles 'my-library-proguard-rules.txt"
}
}
R8
If you've been keeping up on all the exciting android news around build improvements then you may have heard that proguard is going away and being replaced by R8 which will allow shrinking, desugaring, and dexing to all be done in one step without the need to delegate out to an external tool like Proguard.
Does this mean everything we just learned in this post is useless?!
Nope! R8 is backwards compatible with your current proguard rules. This means that if you've got your proguard config perfected today then the switch to R8 should be seamless for you.
I've included some links that I've found helpful when learning about and dealing with proguard below.