Foursquare Intersections logo
Explore
Subscribe
Engineering

Gson Gotchas on Android

Written by 4SQ ENG on Mar 17, 2015 - Read time: 4 min - Read Later

This is Part 2 in our 2 part series on latency. In Part 1, we discuss some techniques we use for measuring latency at Foursquare. Here we'll discuss some specific Gson related changes we made to improve performance in our Android apps.

Shortly after the launch of Foursquare for Android 8.0, we found that our app was not as performant as we wanted it to be. Scrolling through our homepage was not buttery smooth, but quite janky with frequent GC_FOR_ALLOC calls in logcat. Switching between activities and fragments was not as quick as it should be. We investigated and profiled, and one of the largest items that jumped out was the amount of time spent parsing JSON. In many situations, this turned out to be multiple seconds even on relatively modern hardware such as the Nexus 4, which is crazy. We decided to dig in and do an audit of our JSON parsing code to find out why.

In Foursquare and Swarm for Android, practically all interaction with the server is done through a JSON API. We use Google's Gson library extensively to deserialize JSON strings into Java objects that we as Android developers like working with. Here's a simple example that converts a string representation of a venue into a Java object:

String venueJson = "{\"name\": \"Starbucks\" }";
Gson gson = new Gson();
Venue venue = gson.fromJson(venueJson, Venue.class);
// Do something with object.
String name = venue.getName();

This works, but we don't actually need the whole JSON string to begin parsing. Fortunately, Gson has a streaming API that let's us parse a JSON stream one token at a time. Here's what a simple example of that would look like:

InputStream in = ...; // Obtained from HTTP client.
JsonReader reader = new JsonReader(in);
Gson gson = new Gson();
Venue venue = gson.fromJson(reader, Venue.class);
// Do something with object.
String name = venue.getName();

So we did this, but still didn't see any significant speed up or smoother app performance. What was going on? It turns out that we were shooting ourselves in the foot with our usage of custom Gson deserializers. We use custom deserializers because there are times when we don't want a strict 1:1 mapping between JSON and Java objects they deserialize to. Gson allows for this, and provides the JsonDeserializer interface to facilitate this:

public interface JsonDeserializer {
  T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException;
}

The way you use this is you implement this interface and tell it what type you want it to watch out for. You then register this with the Gson instance you are using to deserialize, and from then on whenever you try to deserialize some JSON to a certain type Type typeOfT, Gson will check to see if a custom deserializer is set up to handle that type and if so, will call that custom deserializer's deserialize method. We use this for a few types, one of which happens to be our outermost Response type that encapsulates all Foursquare API responses:

class ResponseDeserializer implements JsonDeserializer {
  @Override
  public Response deserialize(JsonElement json) {
    JsonObject object = json.getAsJsonObject();
    String meta = object.get("meta");
    String result = object.get(“response");
    // Do custom parsing.
    return response;
  }
}

The problem here is that despite us thinking we were using Gson's streaming API, our usage of custom deserializers would cause whatever JSON stream we were trying to deserialize to be completely read up into a JsonElement object tree by Gson to be passed to that deserialize method (the very thing we were trying to avoid!). To make matters worse, doing this on our outermost response type that wraps every single response we receive from the server prevents any kind of streaming deserialization from ever happening. It turns out that TypeAdapters and TypeAdapterFactorys are what are now preferred and recommended over JsonDeserializer. Their class definitions look roughly like this:

abstract class TypeAdapter {
  ResponseV2 read(JsonReader in);
}

Note the JsonReader stream being passed to the read() method as opposed to the JsonElement tree. After being enlightened with this information, we updated our custom deserializers to extend TypeAdapters and TypeAdapterFactorys and noticed significant parse time decreases of up to 50% for large responses. More importantly, the app felt significantly faster. Scroll performance that was previously janky from constant GCs due to memory pressure was noticeably smoother.

Takeaways

  • Use GSON's streaming APIs, especially in memory-constrained environments like Android. The memory savings for non-trivial JSON strings are significant.
  • Deserializers written using TypeAdapters are generally uglier than those written with JsonDeserializers due to the lower level nature of working with stream tokens.
  • Deserializers written using TypeAdapters may be less flexible than those written with JsonDeserializers. Imagine you want a type field to determine what an object field deserializes to. With the streaming API, you need to guarantee that type comes down in the response before object.
  • Despite its drawbacks, use TypeAdapters over JsonDeserializers as the Gson docs instruct. The memory savings are usually worth it.
  • But in general, avoid custom deserialization if at all possible, as it adds complexity

Interested in these kinds of problems? Come join us!

Matthew Michihara

Subscribe

Follow 4SQ ENG

Gson Gotchas on Android

Read Later

Pardot response heading