Renato Ivancic - Software Engineer

Background image

Strip Android application with ProGuard

Android Development

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 Android

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/)

Android about ProGuard

#ProGuard #Android #Java #Gradle