marko devcic

  • github:
    deva666
  • email:
    madevcic {at} gmail.com

Keeping your Android app running smooth

Posted on 3 July 2016

Creating objects is never free on managed runtimes. Allocating objects on the heap will force the garbage collector to run. When it runs it suspends all the threads in your app, including the thread responsible for rendering the UI. This can lead to dropped frames and glitchy UX.

With the introduction of Android Runtime (ART) in Android 5.0 which replaced the Dalvik VM, garbage collection has been hugely improved. Most notable changes include only one pause for marking and enumerating live objects, instead of Dalvik’s two pauses. Another huge improvement is that unlike desktop GCs, ART GC is non compacting, meaning surviving objects are not moved after collection (actually, they are, but only when the app is in the background). Now these changes make a huge difference and Google states that the average GC pause in ART is only 5ms. But if you look at your logcat you’ll see often enough that number is a lot higher.

Logcat output from one open source app, in which you can see that actual pause time is 100ms:

Background sticky concurrent mark sweep GC freed 367225(12MB) AllocSpace objects, 0(0B) LOS objects, 17% free, 55MB/67MB, paused 1.367ms total 103.208ms

Here are some tips how to avoid unneeded object allocations and relieve the GC of unneeded work.


1. Construct collections with predetermined size.
Almost all Collection and Map classes (ArrayLists, HashSets, HashMaps, …) in Java are internally backed by an Array. For example, lets take a look at ArrayList class. Basically, it is a implementation of List interface which encapsulates an Array for storing objects. We can add objects to it and internally the class will manage the size of the Array. Internally resizing works by creating a new Array, when old one reaches its size and copying items from old one to new, larger one.
And this is the problem. Let's say you create an ArrayList and want to add 100 items to it. The internal Array will start with size for 1 item and when enlarging will grow 1.5 times in size. That means that until we hit the needed 100 slots we'll allocate about 11 useless arrays that are going to be discarded and will have to be GC'ed. The solution is to construct the collection with predetermined size when we can. Almost all collection classes have an overloaded constructor that accepts an integer as a size of the internal Array.

This is a common scenario, transforming a collection of objects to something else.

private final void badExample(Collection<SomeObject> incomingObjects){
    List<TransformedObject> result = new ArrayList<TransformedObject>();
    for (int i = 0; i < incomingObjects.size(); i++) {
        SomeObject someObject = incomingObjects.get(i);
        TransformedObject transformedObject = transform(someObject);
        result.add(transformedObject);
    }
}

But since we now exactly how much space we need in the new collection, we construct it with predetermined size.

private final void betterExample(Collection<SomeObject> incomingObjects){
    List<TransformedObject> result = new ArrayList<TransformedObject>(incomingObjects.size());
    for (int i = 0; i < incomingObjects.size(); i++) {
        SomeObject someObject = incomingObjects.get(i);
        TransformedObject transformedObject = transform(someObject);
        result.add(transformedObject);
    }
}


2. Don’t use enhanced foreach loop on Collections
Java implementation of enhanced for loop needs an allocation of Iterator object. But, also it about 3x times slower than a manual counted for loop. Exceptions to this are Arrays, they don’t need an iterator object and the speed is the same as in manually counted for loop..
Example of enhanced for loop.

for(SomeObject item: someObjectsList){
   //do something with the item
}


3. Don’t box primitive types
Java has primitive types (int, boolean, float, ...) and reference types (Integer, Boolean, Float, ...). The difference is that variables of primitive types contain actual values and variables of reference types contain a reference to an object allocated on the heap. Autoboxing is automatic conversion of primitive type to a reference type. And Java allows interchangeable usage of primitive types and their reference counterparts.
Here's an example I saw in one Android app. getHeight method of the View class returns a primitive int type and when assigning it to a reference type we autobox it and allocate it on the heap.

Integer height = view.getHeight();

Even worse example is this.

 Integer total = 0;
 for(int i=0; i<someSize; i++){
   total += i;
 }

Since + operator is not applicable to an Integer object, compiler will generate something like this. And we end up with useless allocation on the heap for each for loop step.

 Integer total = 0;
 for(int i=0; i<someSize; i++){
   int temp = total.intValue() + i;
   total = new Integer(temp);
 }


4. Use Sparse arrays
Sparse arrays are Android only data structures for mapping integer to objects and in some cases you can use them instead of HashMaps. They are intended to be more memory efficient. Compared to HashMaps they don't need additional Entry object allocated for each item in the Map. Also, they don't auto-box primitive int types. There is a speed trade off though, because internally it uses binary search for key lookup. So, on average lookup will take O(log N) instead of O(1). But for smaller containers this is not really a big difference.


5. Use ArrayMap
ArrayMap is another Android only data structure, but this one implements generic Map <K, V> interface. The benefits and drawbacks are the same as in Sparse Arrays, doesn’t need additional object for each entry at the cost of doing binary search for lookup. Again, smaller containers shouldn’t notice the difference.


Happy coding!