Getting Started with ProGuard
In this Android tutorial, you’ll learn how to strip down your app size by making use of ProGuard – an app shrinking and obfuscation tool. By Kolin Stürt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started with ProGuard
30 mins
- Getting Started
- Using the APK Analyzer
- Adding the BubblePicker Library
- Enabling ProGuard
- Adding “Don’t Warn” Rules
- Adding BubblePicker Code
- Debugging with ProGuard Output Files
- Adding Keep Rules
- Adding Data to the BubblePicker
- Introspection and Reflection
- Enabling Advanced Optimizations
- Understanding Obfuscation
- Adding Annotations
- Where To Go From Here
Adding BubblePicker Code
In the res/layout/activity_main.xml file, replace the second TextView
(descriptionTextView
) with the following:
<com.igalata.bubblepicker.rendering.BubblePicker
android:id="@+id/picker"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgroundColor="@android:color/white" />
This adds the BubblePicker
to the main layout. In the MainActivity.kt file, add the following to the setupBubblePicker()
method:
picker.bubbleSize = 50
picker.centerImmediately = true
picker.adapter = object : BubblePickerAdapter {
val colors = resources.obtainTypedArray(R.array.colors)
val titles = listOf("1", "2", "3", "4", "5", "6")
val multiplier = 2
val modulus = 8
val addition = 1
override val totalCount = titles.size // 1
override fun getItem(position: Int): PickerItem { // 2
return PickerItem().apply {
title = titles[position]
val start = colors.getColor((position * multiplier) % modulus,0)
val end = colors.getColor((position * multiplier) % modulus + addition,0)
gradient = BubbleGradient(start, end, BubbleGradient.VERTICAL)
textColor = ContextCompat.getColor(this@MainActivity, android.R.color.white)
}
}
}
picker.listener = object : BubblePickerListener { // 3
override fun onBubbleSelected(item: PickerItem) {
}
override fun onBubbleDeselected(item: PickerItem) {
}
}
Note: This tutorial assumes you’re familiar with handling imports. If you don’t have on-the-fly imports set up, import by pressing option + return on a Mac or Alt + Enter on a PC while your cursor is over the item that needs to be imported.
Here’s what’s going on in the updated method:
- You are overriding
totalCount
to tell theBubblePicker
how many bubbles there will be. - You’re overriding
getItem()
to return a customizedPickerItem
. - You set up the
BubblePickerListener
to handle selecting a bubble.
Finish off the implementation by adding this to the end of onResume
:
picker.onResume()
Add this to the end of onPause
:
picker.onPause()
Build and run the app. Uh oh — the app crashes with a NullPointerException
! Note that several methods in your stack trace are obfuscated – the names are changed and minified. This is one of the key features of ProGuard.
Check the output log to narrow down what the problem is. You can see in the Run tab that it has something to do with “GL” and “onDrawFrame” in the BubblePicker
library.
Note: It’s always good practice to add sufficient logging in your catch statements, nullability checks and error states. With ProGuard, this is crucial – in the event of a problem, it will help lead you or other developers to the root of the issue, especially when the method names in stack traces are obfuscated.
Debugging with ProGuard Output Files
When ProGuard finishes running, it produces 4 output files. They are:
- usage.txt – Lists code that ProGuard removed.
- dump.txt – Describes the structure of the class files in your APK.
- seeds.txt – Lists the classes and members that were not obfuscated. Good to verify that you have obfuscated your important files.
- mapping.txt – The file that maps the obfuscated names back to the original.
You can use the mapping file to find the culprit of the crash.
Run the APK Analyzer again, then select the classes.dex file. Your crash points to obfuscated code looking something like org.a.d.l.a. The single characters you see may vary from this example but you can follow the directory path of the characters:
At first, it’s not clear what the directories are. Click the Load Proguard mappings… button to map the obfuscated names back to the original.
Select the mapping.txt file in the debug folder and click OK.
Toggle the Deobfuscate names button to the left of the Change ProGuard mappings… button to switch between obfuscated and deobfuscated code. You can trace the problem down to the jbox2d
library.
You need to preserve jbox2d
so that ProGuard doesn’t muck with it.
There are a few more things you should know about the mappings file:
- Every time you make a release build, the mapping.txt file is rewritten. That means you must save each copy with each release of your app. That way, when you receive an obfuscated stack trace for a particular app release, it will be useful.
- You can upload your mapping.txt file to Google Play to deobfuscate your crash stack traces.
- If you’re using Fabric, a deployment and crash reporting tool from Google, instructions for uploading your mapping.txt file are here.
Adding Keep Rules
Keep rules tell ProGuard not to obfuscate certain parts of your code. Some options are:
-
keep
– Preserve entire class and class members. -
keepclassmembers
– Preserve the class members. -
keepclasseswithmembers
– Preserve all classes that have a specified member.
ProGuard rules are written in a specific template format. It’s best practice to use explicit keep rules, rather than keeping the entire class. Instead of preserving the entire BubblePicker
library, you only need to keep org.jbox2d
and its sub-packages. The format is the same as the dontwarn
rules.
Add the following to the proguard-rules.pro file right after the dontwarn
rules you added:
-keep class org.jbox2d.** { *; }
Inside the curly braces, you told ProGuard to match any method name.
Build and run the app. When your app loads up, you should see the bubbles floating around on the screen.
Note: If you’re sharing your code, write keep rules as you write your code and be sure to publish them on your site or GitHub README page so other developers can easily use your code without any problems.
Now you’ll set up some real sloth data.
Adding Data to the BubblePicker
There’s a file included in the res folder of the project called sloths.xml. Since it’s in XML format, you’ll need a way to get that data into Kotlin objects. You’ll use another third party library included with the Retrofit package called SimpleXML.
Add this code to the list of dependencies in the build.gradle app module file. You can exclude groups and modules that you’re sure you won’t use:
implementation ('com.squareup.retrofit2:converter-simplexml:2.0.0-beta3'){
exclude group: 'xpp3', module: 'xpp3'
exclude group: 'stax', module: 'stax-api'
exclude group: 'stax', module: 'stax'
}
Sync Gradle, then build and run the app.
You’ll have some new warnings: a reference to org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement, and many variations of warnings about javax.xml.stream.**.
You won’t be using Java’s XML stream feature. For this tutorial’s purpose, it’s safe to ignore the animal_sniffer warnings. Add the following to the proguard-rules.pro file:
-dontwarn javax.xml.stream.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
Select Build ▸ Make Project. This time the build should succeed.
Open the SlothViewModel.kt file and replace the loadSloths()
function with the following:
private fun loadSloths(resources: Resources) { // 1
val serializer = Persister()
val inputStream = resources.openRawResource(R.raw.sloths) // 2
val sloths = serializer.read(Sloths::class.java, inputStream) // 3
sloths.list?.let { theList ->
val map = theList.associateBy( { it.name }, {it})
this.sloths = map.toSortedMap()
}
}
Here:
- You removed the explicit return type for the function.
- You opened the sloths XML file an an
InputStream
. - You retrieved a list of Sloths by invoking read().
The XML parser knows how to map the XML fields to your Kotlin objects by using annotations. In the Sloths.kt file, add this annotation above the Sloths
constructor:
@Root(name = "sloths", strict = false)
This tells the parser to look for the root node called “sloths”.
Then add annotations above the val list
property in the constructor:
@field:ElementList(entry = "sloth", inline = true)
@param:ElementList(entry = "sloth", inline = true)
@field
and @param
let the parser know to look for “sloth” items.
The same will need to be done for the Sloth class. In the Sloth.kt file, replace everything after the import statement with this:
@Root(name = "sloth", strict = false)
data class Sloth constructor(
@field:Element(name = "name")
@param:Element(name = "name")
var name: String = "",
@field:Element(name = "realName")
@param:Element(name = "realName")
var realName: String = "",
@field:Element(name = "imageResource")
@param:Element(name = "imageResource")
var imageResourceName: String = "",
@field:Element(name = "description")
@param:Element(name = "description")
var description: String = "") : Serializable
In the MainActivity.kt file, add a ViewModel
variable right above onCreate()
:
private val viewModel: SlothViewModel by lazy {
ViewModelProviders.of(this).get(SlothViewModel::class.java)
}
Add a call to get the sloth data as the first line of setupBubblePicker()
:
val map = viewModel.getSloths(resources)
Replace the line that reads val titles = listOf("1", "2", "3", "4", "5", "6")
with the following:
val titles = map.toList()
And finally, replace the title =
line with this:
title = titles[position].first
Build and run the app. Notice the NoSuchMethodException
crash.
You know it must be the code you just added. Doing similar debugging to what you did earlier, you can narrow the problem down to the Parameter
object inside SimpleXML. This time you’ll take a deeper look at the problem. SimpleXML works by loading XML entities presented at runtime, then instantiates Kotlin counterparts. Kotlin can only do this by using introspection and reflection.