Strip Android application with ProGuard
ProGuard - Magic dieting pill for your app
As stated in previous blog post Android Multidex tool has some limitations. I found another solution for the same problem:
ProGuard can detect unused classes, methods, fields and attributes as end result the bytecode is optimized. Then it renames remaining elements what makes the reverse ingeneering a bit tougher. This is far better solution for overcoming reference count. Configure the Gradle settings for your app to run ProGuard with minifyEnabled set to true. This applies ProGuard Gradle plugin to build process.
defaultConfig {
minSdkVersion 10
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
/* Do not have to use the multidex anymore because the ProGuard minimizes the compiled
package */
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt',
'proguard-androidannotations.pro', 'proguard-scala.pro'
}
debug {
minifyEnabled false
}
}
Now the packed .apk file size is 3.9MB, almost half of the size it used to be. App installs without problems. Even on devices with API 10 and 15. But hold right there.
Proguard rule file
Every step can be configurable in external simple text file. These are then referenced in proguardFiles gradle property. Here is the link to all the available options and explanations for rules.
Preverification is irrelevant for the dex compiler and the Dalvik VM, so we can switch it off with the -dontpreverify option.
As an contra argument for not using Proguard in projects I heard that the obfuscation of their code isn’t needed or wanted. The short answer for this, my dear friend, is:
-dontobfuscate
With this simple line in rules file you can skip obfuscation in your project and your good to go.
Reflection problem
Now when any response is received from the server, app goes south. It quickly turns out that the JSON parsing is not functioning correct anymore.
09-23 15:28:23.260 584-584/com.rivancic.android.client E/BaseActivity﹕ Message: java.lang.NoSuchMethodException:
prepareToDeserialize
java.lang.NoSuchMethodException: prepareToDeserialize
at java.lang.ClassCache.findMethodByName(ClassCache.java:247)
at java.lang.Class.getMethod(Class.java:962)
at com.rivancic.android.client.core.repositories.Store$1ResponseHandler.handle(Store.java:128)
at com.rivancic.android.client.core.repositories.Store$1ResponseHandler.handle(Store.java:118)
I am using reflection when parsing JSON response from server and deserializing models. ProGuard didn’t catch any reference to this prepareToDeserialize method in the Response class. That is the reason it is removed.
Method method = responseType.getMethod("prepareToDeserialize", new Class[]{JsonObject.class});
Response response = (Response) method.invoke(null, event.body());
The results of the ProGuard can be found in outputs/debug/mapping directory. In the seeds.txt you can see which elements left untouched. No method prepareToDeserialize can be found in this file so this means the ProGuard change id and this is the reason the reflection doesn’t find it. Search in usage.txt immediately pops the method prepareToDeserialize which means that the ProGuard excluded it from final package. There are even line numbers prepend usage.txt file with pinpointed problematic line:
com.rivancic.android.client.core.messages.training.ListTrainingsResponse:
45:62:public static com.rivancic.android.client.core.messages.training.ListTrainingsResponse prepareToDeserialize(com
.goodow.realtime.json.JsonObject)
67:76:private void addTraining(com.goodow.realtime.json.JsonObject)
85:86:public void setTrainingsList(java.util.List)
So this was causing errors. There is a straight solution for that. with properly set ProGuard rule that tells which instructions are necessary. Because I am using reflection for serializing JSON I need to say to ProGuard leave the member methods with this Keep option in proguard-rules.txt file:
-keepclassmembers class * extends com.rivancic.android.client.core.messages.Response {
public static ** prepareToDeserialize(com.goodow.realtime.json.JsonObject);
}
Now, tadaa! In seeds.txt there is line
com.rivancic.android.client.core.messages.training.ListTrainingsResponse: com.rivancic.android.client.core.messages.training.ListTrainingsResponse prepareToDeserialize(com.goodow.realtime.json.JsonObject)
which means the method exists in the final package and will be found trough reflection. I do not need Multidex. My application is now properly packaged with ProGuard. This is like latest trend in every web application which should use minimized and bundled css and javascript files. More on Keep, Shrinking, Optimization and other options you can find on ProGuard web page.
Reflection No. 2
After a while another similar trouble with ProGuard. The ErrorModel constructor was used in prepareToDeserialize method.
09-24 17:43:55.487 24418-24885/com.rivancic.android.client.customer.android D/dalvikvm﹕ newInstance failed: no <init>()
09-24 17:43:55.487 24418-24885/com.rivancic.android.client.customer.android E/AssignModel﹕ Deserialize Model error: java.lang.InstantiationException: can't instantiate class com.rivancic.android.client.core.models.impls.ErrorModel; no empty constructor
09-24 17:43:55.497 24418-24885/com.rivancic.android.client.customer.android E/JsonSerializable﹕ Can't instantiate
java.lang.InstantiationException: can't instantiate class com.rivancic.android.client.core.models.impls.ErrorModel; no empty constructor
at java.lang.Class.newInstanceImpl(Native Method)
Again because using reflection, default empty constructor was removed. I took the same steps, looking at the build/outputs/mapping/debug/usage.txt file where lines:
com.rivancic.android.client.core.models.impls.ErrorModel:
public static final java.lang.String TYPE
private int errorSource
private java.lang.String request
34:35:public ErrorModel()
94:94:public int getErrorSource()
show that the default public constructor was removed. The following line has to be added:
-keepclassmembers public class com.rivancic.android.client.core.models.impls.ErrorModel {
public <init>(...);
}
So the constructor stays in class. And the final usage.txt file is 25000 lines long! That means there were quite some LOC in my app that were not used or could be optimized.
Lesson learned
Android applications suffer from the limitation of references in the compiled dex file. This can happen if application uses big external libraries. You have the option to use Multidex or solve the problem with the ProGuard.
The main problems using Multidex feature is the application file size, because lot of unnecessary code is shipped with it and even bigger issue is the inability to run the apps on the Android OS pre 4.0 version.
ProGuard way: enable minification in gradle build file, test the application. If something is not working, pinpoint the problem then look in the output files of the ProGuard to find what part was removed or obfuscated and is causing trouble. Accordingly set additional rules in the ProGuard file. Mainly this happens with the use of reflection.
Enjoy puzzling your proguard rules together.
Sources
Building Apps with Over 65K Methods
[ProGuard] (http://proguard.sourceforge.net/)