Menu Close Back

DexGuard optimization for the Google Gson library

DexGuard optimization for the Google Gson library

DexGuard comes with a new optimization specifically designed for the Google Gson library. The optimization was introduced in DexGuard 8.3. It has multiple benefits: (1) it enables app developers to obfuscate the encoded data in a more thorough manner, (2) it improves the performance of Android applications using the Gson library and (3) makes it easier to configure projects that make use of the library.

In this blog, we describe the technique used by DexGuard to optimize the usage of Google’s GSON library and discuss the gains you can expect from using it.

Google Gson library

The Gson library is a commonly used open-source library from Google, used for serializing Java objects to and from JSON. It enables developers to write code as follows:

Address address = new Address(362, "Tervuursestraat", "3000", "Leuven");
Company company = new Company("Guardsquare", address);

Gson    gson    = new GsonBuilder().create();
System.out.println(gson.toJson(company));

This code outputs the following JSON.

{
       "name": "Guardsquare",
       "address":
       {
              "number":362,
              "street":"Tervuursestraat",
              "city":"Leuven",
              "zip":"3000"
       }
}

The Gson library relies heavily on reflection to perform this feat, that is to traverse the fields of Java objects that need to be serialized and derive their types. This makes Gson very powerful and dynamic. The downside is that reflection has a significant impact on the performance of mobile applications.

How DexGuard optimizes Gson usage

In the past, developers who wanted to process an Android app containing the Gson library with DexGuard had to keep class and field names occurring in the library. Obfuscating these identifiers had the unwanted side-effect of rendering illegible (and unusable) the property names in the resulting JSON.

The newly added Gson optimization uses DexGuard’s capability to inspect the fields of Java classes, detect method invocations in bytecode and inject new methods into Java classes. DexGuard now searches for calls to the Gson library in the application code and detects which domain classes are involved in Gson serialization calls. It then generates and injects methods into the domain classes that directly access the fields of the domain object. With this technique, each of these domain classes ends up with two additional methods:

  • a toJson() method that reads the values of the fields of the domain object, turns them into a Json formatted String and returns the String

  • a fromJson() method that accepts a Json formatted String, parses it and sets the values of the fields of the domain object accordingly.

In our initial example, DexGuard would detect the call to Gson.toJson() and conclude that the Company instances need to be serialized. The Company class has a String field (i.e. the name) that can be serialized directly. Besides the String field, there is an Address field that contains a number of String and primitive fields, each of which also can be serialized directly. DexGuard will generate optimized toJson()for the two domain classes that need to be optimized (Company and Address).

public class Company

{
       private String  name;
       
       private Address address;

       public Company(String name, Address address)
       {
              this.name    = name;
              this.address = address;
       }

       ...

       // Generated by DexGuard.
       public String toJson()
       {
              return ”{\”name\”:” + this.name +
                     // etc.
                     “}”;
       }

       ...
}

public class Address

{
       private int    number;
       private String street;
       private String city;
       private String postalCode;

       public Address(int number, String street, String city, String postalCode)
       {
              this.number     = number;
              This.street     = street;
              this.city       = city;
              this.postalCode = postalCode;
       }

       ...
       // Generated by DexGuard.
       public String toJson()
       {
              return ”{\”number\”:” + this.number +
                     // etc.
                     “}”;
       }

       ...
}

To enable the use of the optimized methods, DexGuard patches the Gson class: it injects a TypeAdapter that invokes the generated toJson() and fromJson() methods. DexGuard does this in such a way that custom TypeAdapters, registered by the developer, get precedence over the optimized code to guarantee compatibility.

Thanks to the technique above, DexGuard significantly reduces the runtime overhead that is inherent to reflection as most of the heavy lifting is done during build time. We have benchmarked the performance using a Gson test app, that shows the time it takes to serialize and deserialize JSON of increasing sizes. We see a 20-30% speed increase on various mobile devices. The following table shows the results on a Samsung Galaxy S5.
 

Dexguard runtime reduction test
 

Another important advantage of DexGuard’s Gson optimization is that the domain classes involved in Gson serialization calls can now safely be obfuscated, since the generated code directly access the fields. It also enables developers to take an additional measure to secure the (often sensitive) data stored and transmitted in JSON format. The described technique allows them to obfuscate the Strings in the domain classes with String encryption to prevent third parties from disassembling the code and extracting the JSON field names.

Improved usability

Before version 8.3, DexGuard came with a default set of rules that kept all the fields with a Gson annotation. For non-annotated fields (see example above) additional rules had to be specified in the project-specific DexGuard configuration, like so:

-keepclassmembers class com.example.Company,
                        com.example.Employee {
    <fields>;
}

As of DexGuard 8.3 it is no longer necessary to specify such keep rules, as the injected code will be obfuscated with the fields.

Compatibility

DexGuard’s Gson optimization is compatible with all Gson versions from 2.1 onwards.

To make sure the application code behaves as before, DexGuard does not apply the optimization in cases where Gson features are used for which DexGuard is unable to generate code at build time. Examples of  unsupported Gson features are the use of version tags @Since and @Until on fields or using a custom FieldNamingStrategy. In such cases, the code injected by DexGuard will automatically fall back on the default Gson implementation for all affected domain classes. It is worth noting, however, that customizing field names using the @SerializedName annotation is fully supported by the Gson optimization.