Author is a professional software developer providing android and iOS development services. More info here.
All my android developing life I’ve used java serialization as a data transfer mechanism between activities. I was vaguely aware of
Parcelable and it proclaimed benefits but never really saw any performance issues related to this and thus never had a need to start writing all the parcelable boilerplate in my classes instead of just marking them as
Serializable. When I recently started working on an ongoing project I’ve quickly jumped ship since: a) it’s supposed to be an order of magnitude faster and b) there is an awesome code generation library, Parceler, relieving developers of writing the boilerplate while keeping all the performance goodness.
I’ve just finished implementing my first big feature in the app and all was well until our amazing QA guy came to me with an issue – app was crashing on transition from one activity to another – throwing a
TransactionTooLargeException (or just logging
E: !!! FAILED BINDER TRANSACTION !!! on pre 15 API levels). And he wasn’t even trying to do anything crazy. Quick google search led me to a world of remote procedure calls and binder transaction buffers, a world I was blissfully unaware previously. So there is this binder transaction buffer, and it appears to be limited to 1MB, and it is used by all ongoing transactions simultaneously. I am moving a fairly large object graph through an activity starting intent, and maybe I’m not supposed to be doing that, but surely I can’t be hitting a 1MB limit? Or can I?
Let’s get to the bottom of this
First thing I tried was reverting to java serialization and sure enough everything worked. Then I tried to hit the limit of this approach and failed, even when creating a 100x larger graph than the one causing the original crash and many times larger than worst case scenario I could conceive. I’ve already discovered, before this problem surfaced, that Parcelable/Parceler combination will not preserve references to the same object in a graph on demarshalling but create copies of the object as many times as it is referenced in the original graph. Although this behavior might not be very surprising, or not surprising at all, it is different from how Serializable magic works.
To test this in more controlled environment I created a little experiment app. You can find an apk here and the source here. In it you’ll find an
EmptyObject class, and it’s just that – a class with no instance variables of any kind, actually with no code whatsoever. I wanted to see how many of these useless bastards I could cram into an ArrayList and transfer from one activity to another using java serialization or Parcelable/Parceler. I’ve also created a class called
Package holding a reference to the ArrayList<EmptyObject> and nothing else. An instance of Package class is the only thing I’ll put in an Intent and start another activity with it.
The results are in
Parcelable/Parceler mechanism can transfer a package of 4 045 EmptyObject instances. When the Package instance is marshalled the resulting byte is 517 764 bytes long. Adding one more EO instance to the mix crashes the app with TransactionTooLargeException, so I guess 517 892 bytes is a bit too much. At the same time a package of 4 045 EO instances is only 24 482 bytes long when it’s serialized to byte so it can be transferred easily. The limit when using java serialization is at 86 276 EO instances in a package, creating a serialized byte of 517 928 bytes. My assumption was that if I create a list with all the references pointing to just one EO instance this number will be much larger but actually it’s not. A package containing a list of 86 276 EO references to a single instance when serialized to byte has 431 593 bytes so we can cram another 20k or so in there It’s clear that regardless of the used technology the limit is the amount of bytes you can transfer. On my Moto X (2nd gen) running Android 5.0 that seems to be a number a little lower than 512KB. The results are device specific: On other devices I’ve seen behavior ranging from slightly better on Galaxy S5 and S4, they can transfer couple of KB more, to almost a full MB on HTC One M8. It doesn’t look to be connected to the runtime environment the phone is using (dalvik/ART) .The one thing that is clear is that java serialization creates much smaller byte than using a Parcelable/Parceler combination on the same object graph.
Shit gets weird
Maybe you’re wondering why I needed a Package class, why didn’t I send ArrayList<EmptyObject> directly. In order to simplify I’ve decided to do just that. And everything blew up. Suddenly I couldn’t send 86k of objects with java serialization, I couldn’t even send a measly 4k I could with Parcelable/Parceler. And what’s even weirder if I sent a list with 10 references to the same object I’d get 10 different objects on the other side. I had no idea what was going on.
ArrayList implements Serializable interface, and it’s full of also serializable EmptyObject instances I thought I was calling
Intent.putExtra(String name, Serializable value) flavor of putExtra methods. And I was, but it wasn’t doing what I expected. If you look at the code of this method you’ll see that it’s actually calling
putSerializable(String key, Serializable value) method of a
Bundle class (
BaseBundle from API level 21) and all that method does is dumps the Serializable object in a map under the given String name key. This map will actually get marshalled by a
writeArrayMapInternal(ArrayMap<String, Object> val) method from a
Parcel class. That’s where things get interesting. This method will iterate on a said map and call
writeValue(Object v) method on every object in it, a method that is only a glorified switch statement and it will do a bunch of instanceof checks and call writeThis and writeThat methods. Our list will not in fact be written with a
writeSerializable(Serializable s) method but with a
writeList(List val) method iterating and doing separate writeSerializable on list elements. And there you have it: we gave a serializable object to Intent’s putExtra method that also happens to be a list and expected it to serialize the whole thing in one go but instead it will serialize every instance in the list by itself.
Ok, but Parcelable is still 10x faster right?
I actually don’t know since I’m not going to start implementing pure Parcelable classes. However the experiment app measures the time from creating an Intent in Activity A to unpacking it in Activity B, albeit perhaps naively. I didn’t do the test 1000 times to get the average but it looks to me that 10x or 17x improvement over java serialization is not going to happen in this scenario. Yes, it looks like Parcelable/Parceler combination is faster but more like up to 2x faster. But hey, I’m sending a bunch of empty objects here. Remember the object graph from the real app, the one that started all this? It’s actually something like 3x faster to use java serialization on it, and I’ve confirmed this on a plethora of devices. I don’t know why and at this point I’m too afraid to ask.
I’m not going to tell you to go back to java serialization. Maybe I’m doing something wrong here or maybe it has something to do with the fact that android devices are now very different beasts than the ones from just couple of years ago. My only advice is do not trust blindly everything you read on the internet and make some trials on your own real life scenario. That should enable you to draw some meaningful conclusions.