Increasing performance in an Android application
Performance is the most important parameter in a mobile application—if it’s slow and/or buggy, then on the whole, it’s likely to be rejected by users. Then there’s a competitive factor. The better your app performs, the better the chances are for your app in the market. Personally, I’d spend a couple of extra dollars for significantly better performance. Someone wise once said time is money, so let’s cut to the chase and look at how developers can optimize performance on Android.
In this post, we’ll work through things that will help you increase the performance for your Android apps. These improvements primarily cover micro-optimizations that can improve overall app performance when combined. This post covers best practices and common mistakes I’ve seen people make—or mistakes I’ve made myself and have learned how to correct. Let’s get right into it.
Table of Contents
- Working with threads
- Managing memory leaks
- Removing deprecated APIs
- Avoid abuse
- Prefer static methods over virtual methods
- Use static final for constants
- Use for-each loop instead of for loop
- Avoid using float
- Layout performance improvements
- Know and use libraries
- Use native methods carefully
Working with threads:
One of the secrets for a stable and consistently high-performing application is how you manage threads within it. I’d say 90% of performance issues can be solved if you know how to work with threads.
First of all, there are two main types of threads that you should be aware of: the main thread, also called UI Thread, and the background thread. Any changes that you make to the UI thread block the main thread, and this won’t be assigned to another method or call until the current task is finished and it follows the FIFO method.
Briefly, never do the following things on the UI thread:
- Load Images or Streams
- Parse a JSON
- Access a local or remote server
- Make database calls, even SQLite (if possible)
- Make API calls
There is an extensive document by the Android team on working with threads, which can be found below:
Managing memory leaks:
The first and the most important problems that you need to take care of are memory leaks. What are memory leaks, and how do they happen? A memory leak is when the garbage collector is not able to collect the allocated memory.
For example: If we hold the reference in our code to an unused Activity, we hold all of the Activity’s layout and, in turn, all of its views and everything else that the Activity holds.
- Avoid Static References: Another problem is when you keep static references. Don’t forget, both Activities and fragments have a lifecycle. Once we have a static reference, this Activity, or fragment, will not be garbage collected.
- Unregister your events and handlers: I notice that a lot of times developers don’t unregister events and handlers. This is one of the easiest ways your app could leak. Assume you have an Activity that has a listener assigned to a singleton:
You’ll need to make sure you remove this listener once you’re done:
In the onDestroy, for our Activity, we simply unregister ourselves from all events and handlers. Of course, this raises another important question: Should we use singletons in the first place (as shown in the example above)? Usually, we shouldn’t (depends on your architecture, of course), but sometimes it’s the only solution.
Steps to remove leaks:
- Avoid using static variables.
- Always unregister your events and listeners.
- Use tools such as Eclipse Analyzer or LeakCanary to find these leaks.
- Perform code reviews and code review implementations. Sometimes your peers can point out things you might not have noticed at all. Usually, this happens when you’ve been looking at the same code over and over again.
- Understand your architecture properly before writing any piece of code, so you don’t do anything unnecessary.
Removing deprecated APIs:
Deprecation essentially references when an API is removed and, it could be that one or two days after a major release, your app might not work on certain devices. To make things even worse, sometimes if you’re dependent on an older version of a library, there’s no way to update both APIs and tools.
The major reason you shouldn’t use deprecated APIs is that after the API is removed from the Android system, your application won’t be able to find it and will crash with a RuntimeException saying that the method was not found.
Similarly, always make sure that if you’re using libraries, that they’re maintained by the providers—if they’re not maintained, you might again face the above issues and get stuck in a pothole without a way out.
Note: Not all APIs that are replacements for a deprecated API have backwards compatibility. In that case, you might want to either use the AppCompat versions of similar APIs or write conditions that only execute the deprecated APIs in older versions of Android.
To avoid issues with deprecations:
- Know and use proper APIs.
- Refactor your dependencies.
- Don’t abuse the system by accessing private methods using reflection.
- Update your dependencies and tools periodically.
- Newer is better.
Prefer Toolbar over ActionBar, and prefer RecyclerView over ListView, especially for animations, and if you have a bunch of huge images, because it’s optimized for that.
An example of deprecation prevention is the Apache HTTP connection that was removed in Android M. By the way, it’s still available as a Gradle dependency. So you’d need to use HttpURLConnection instead. It has simpler APIs, light-weight, transparent compression, better response caching, and other amazing features.
Mobile development and machine learning are becoming fast friends. Don’t miss out on the latest news, tools, and learning from the mobile ML world. Sign up for our weekly newsletter.
You might read this section title and ask, what does avoiding abuse actually mean? What I mean is to avoid the following:
- Don’t call private APIs by reflection.
- Don’t call private native methods from the NDK or C level.
- Using adb shell am to communicate with other processes should be avoided.
- Don’t use Runtime.exec to communicate with processes.
Prefer static methods over virtual methods
If you don’t need to access an object’s fields, make your method static. Invocations will be about 15%-20% faster. It’s also a good practice because you can tell from the method signature that calling the method can’t alter the object’s state.
Use static final for constants
I’ve seen many people make this mistake, wherein they declare static constants without the final key. You should always define your constants with the final keyword. When you have a static field without the final keyword, that variable is considered as a field and not as a constant. For example, imagine you have two fields defined, as shown below:
static int integerVal = 1;
static String stringVal= "Hello, Android!";
The compiler generates a class initializer method called <clinit> that’s executed when the class is used for the first time. The method stores the value 42 into integerVal and extracts a reference from the classfile string constant table for stringVal. When these values are referenced later on, they are accessed with field lookups. But when you add the final keyword to it…
static final int intVal = 42;
static final String strVal = "Hello, Android!";
the class no longer requires a <clinit> method, because the constants go into static field initializers in the dex file. Code that refers to integerVal will use the integer value 42 directly and accesses to stringVal will use a relatively inexpensive "string constant" instruction instead of a field lookup.
Note: This optimization applies only to primitive types and String constants, not arbitrary reference types. Still, it's good practice to declare constants static final whenever possible.
Use for-each loop instead of for loop
The for-each loop, also known as the enhanced for loop, is always a better option when trying to iterate through collections that implement the Iterable interface, or even for arrays.
The Android docs say:
With collections, an iterator is allocated to make interface calls to hasNext() and next(). With an ArrayList, a hand-written counted loop is about 3x faster (with or without JIT), but for other collections the enhanced for loop syntax will be exactly equivalent to explicit iterator usage.
So you should use the enhanced for loop by default, but consider a hand-written counted loop for performance-critical ArrayList iteration.
Avoid using float
As a rule of thumb, floating-point is about 2x slower than the integer on Android devices.
In terms of speed, there’s no difference between float and double on more modern hardware. Space-wise, double is 2x larger. As with desktop machines, assuming space isn't an issue, you should prefer double to float.
Also, even for integers, some processors have hardware multiply but lack hardware divide. In such cases, integer division and modulus operations are performed in software — something to think about if you’re designing a hash table or doing loads of math.
Layout performance improvements
Layouts are a fundamental part of any Android application, and they directly affect the UX. If implemented inadequately, your layout will lead to a memory-hungry application with slow UIs. The good part is that the Android SDK includes certain tools to help you identify problems in your layout performance, which when combined with the below tips, will help you implement smooth scrolling UI’s with a minimal memory trace.
Re-using layouts with <include/> and <merge/>
Although Android has a variety of controls to provide efficient and re-usable interactive elements, you might also need to re-use larger components like custom controls that require a special layout.
To efficiently re-use complete layouts, you can use the <include/> and <merge/> tags to embed another layout inside the current one. Reusable layouts are particularly powerful, as they allow you to create a reusable complicated layout. For example, a yes/no button panel, or custom progress bar with description text.
It also means that any parts of your application that are common across multiple layouts can be extracted, managed separately, and then included in each layout using the above methodology. So while you can create individual UI components by writing a custom View, you can do it even more easily by re-using a layout file. You can check the Android documentation for a detailed guide on working with reusable layouts.
Delayed View Loading
Sometimes your layouts might require complex views that are rarely used, i.e. under a certain condition. Whether they’re item details, progress indicators, or undo messages, you can reduce memory usage and speed up layout rendering by loading the views only when they’re needed. Delayed loading of resources is an important technique to use when you have complex views that your app might need in the future. You can implement this technique by defining a ViewStub for those complex and rarely-used views.
You can check out this amazing blog by Google on optimizing layouts with ViewStub
It’s a common misconception amongst the community that using basic layout structures is the most efficient way of coming up with consistent and high-performance UI controls and layouts. However, each widget and layout you add to your application requires initialization, layout, and drawing. For example, using a nested LinearLayout can lead to an excessively deep view hierarchy.
Moreover, nesting several instances of LinearLayout that use the layout_weight parameter can be especially time-consuming, as each child needs to be measured twice. This is particularly important when the layout is inflated in the View repeatedly, such as when used in a ListView or GridView.
Inspecting your layout with Hierarchy Viewer: The Android SDK has a tool called the Hierarchy Viewer that allows you to examine your layout while your application is running. Using this tool helps you discover bottlenecks in layout performance.
Hierarchy Viewer works by allowing you to select running processes on a connected device or emulator and then displaying the layout tree. The traffic lights on each block represent its Measure, Layout and Draw performance, helping you identify potential issues.
For more on how to use this, you can check the Android docs.
Working with Lint: When you check the Android docs in regards to working with lint it says:
It is always good practice to run the lint tool on your layout files to search for possible view hierarchy optimizations. Lint has replaced the Layoutopt tool and has much greater functionality. Some examples of lint rules are:
Use compound drawables — A LinearLayout which contains an ImageView and a TextView can be more efficiently handled as a compound drawable.
Merge root frame — If a FrameLayout is the root of a layout and does not provide background or padding etc, it can be replaced with a merge tag which is slightly more efficient.
Useless leaf — A layout that has no children or no background can often be removed (since it is invisible) for a flatter and more efficient layout hierarchy.
Useless parent — A layout with children that has no siblings, is not a ScrollView or a root layout, and does not have a background, can be removed and have its children moved directly into the parent for a flatter and more efficient layout hierarchy.
Deep layouts — Layouts with too much nesting are bad for performance. Consider using flatter layouts such as RelativeLayout or GridLayout to improve performance. The default maximum depth is 10.
Other benefits of using Lint include:
- Lint is integrated into Android Studio.
- Lint automatically runs whenever you compile your program.
- With Android Studio, you can also run lint inspections for a specific build variant, or for all build variants.
- Lint has the ability to automatically fix some issues, provide suggestions for others, and jump directly to the offending code for review.
Know and use libraries
Always know the API(s) and the libraries that you use. Always prefer to use a library’s code over writing your own. Remember that the system has the privilege to replace your calls to the library method(s) using a hand-coded assembler, which could be even better than the best code the JIT can come up with for the equivalent Java.
A classic example here would be the String’s IndexOf method and associated APIs, which Dalvik will replace with an inlined intrinsic. Similarly, the System’s ArrayCopy method is about 9x faster than a hand-coded loop on a Nexus One with the JIT.
Use native methods carefully
Another very famous myth is that developing your app with native code using the Android NDK is more efficient than programming with Java/Kotlin. Not necessarily. Moreover, there’s a loss associated with the Java-native transition, and the JIT can’t optimize across these boundaries. If you’re allotting native resources (memory on the native heap, file descriptors, or whatever), it can be significantly more complex to arrange timely collection of these resources.
You also need to compile your code for each architecture(ABI) you wish to run on (rather than rely on it having a JIT).
You may even have to compile multiple versions for what you consider the same architecture: native code compiled for the ARM processor in the G1 can’t take full advantage of the ARM in the Nexus One, and code compiled for the ARM in the Nexus One won’t run on the ARM in the G1.
Native code is essentially useful when you have an existing native codebase that you want to port to Android, not for “speeding up” parts of your Android app written in Java.
If you do need to use native code, you should read Android’s JNI Tips.
This blog described and discussed techniques for increasing the performance of an Android application. Collectively, these techniques can greatly reduce the amount of work being performed by a CPU and the amount of memory consumed by an application.
If I’ve missed something, go ahead and add it in the comments. I’ll make sure to add any needed changes to the post. Also, if you find something incorrect in the blog, please go ahead and correct me in the comments.
Smash that clap button if you liked this post.
The next revolution in mobile development? Machine learning. Learn more about how Fritz can help you build a new kind of mobile experience.