Implementing a cool idea is a great start toward an app that delights users, but it’s just the beginning. The next step is maximizing your app’s performance. For example, users want apps that:
- Use power sparingly.
- Start up quickly.
- Respond quickly to user interaction.
This section provides you with the know-how you need in order to make your apps not only cool, but also performant. Read on to discover how to develop apps that are power-thrifty, responsive, efficient, and well-behaved.
Know your Android app performance and spot and resolve bottlenecks quickly using New Relic. Optimize your app’s performance by gaining a clear view into every action, session, and swipe from your users, all in real-time.
- Track response times and optimize to improve performance
- Get insight into app errors and easily isolate the root cause
- Understand how good or bad performance of an app affects the bottom-line of your business
- Use device-centric data to see how your app is behaving at the device-level with usage metrics including database, CPU, and memory
- End-to-end User Interaction Traces give you visibility into individual events, and show you exactly where you need to optimize your app
- Compare your app’s performance across various Android phones and tablets, as well as operating systems
- Quickly deploy New Relic’s Android SDK* into your app and start seeing data in minutes.
Table of Contents
- String vs String Builder
- Data Type
- Location Updates
- Network request
- Reflection
- AutoBoxing
- OnDraw
- ViewHolder
- Resize Image
- Strick Mode
1. String vs. StringBuilder
Let’s say that you have a String, and for some reason you want to append more Strings to it 10 thousand times. The code could look something like this.
String string = "hello";
for (int i = 0; i < 10000; i++) {
string += " world";
}
You can see on the Android Studio Monitors how inefficient some String concatenation can be. There’s tons of Garbage Collections (GC) going on.
This operation takes around 8 seconds on my fairly good device, which has Android 5.1.1. The more efficient way of achieving the same goal is using a StringBuilder, like this.
StringBuilder sb = new StringBuilder("hello");
for (int i = 0; i < 10000; i++) {
sb.append(" world");
}
String string = sb.toString();
On the same device this happens almost instantly, in less than 5ms. The CPU and Memory visualizations are almost totally flat, so you can imagine how big this improvement is. Notice though, that for achieving this difference, we had to append 10 thousand Strings, which you probably don’t do often. So in case you are adding just a couple Strings once, you will not see any improvement. By the way, if you do:
String string = "hello" + " world";
It gets internally converted to a StringBuilder, so it will work just fine.
You might be wondering, why is concatenating Strings the first way so slow? It is caused by the fact that Strings are immutable, so once they are created, they cannot be changed. Even if you think you are changing the value of a String, you are actually creating a new String with the new value. In an example like:
String myString = "hello";
myString += " world";
What you will get in memory is not 1 String “hello world”, but actually 2 Strings. The String myString will contain “hello world”, as you would expect. However, the original String with the value “hello” is still alive, without any reference to it, waiting to be garbage collected. This is also the reason why you should store passwords in a char array instead of a String. If you store a password as a String, it will stay in the memory in human-readable format until the next GC for an unpredictable length of time. Back to the immutability described above, the String will stay in the memory even if you assign it another value after using it. If you, however, empty the char array after using the password, it will disappear from everywhere.
2. Picking the Correct Data Type
Before you start writing code, you should decide what data types you will use for your collection. For example, should you use a Vector
or an ArrayList
? Well, it depends on your usecase. If you need a thread-safe collection, which will allow only one thread at once to work with it, you should pick a Vector
, as it is synchronized. In other cases you should probably stick to an ArrayList
, unless you really have a specific reason to use vectors.
How about the case when you want a collection with unique objects? Well, you should probably pick a Set
. They cannot contain duplicates by design, so you will not have to take care of it yourself. There are multiple types of sets, so pick one that fits your use case. For a simple group of unique items, you can use a HashSet
. If you want to preserve the order of items in which they were inserted, pick a LinkedHashSet
. A TreeSet
sorts items automatically, so you will not have to call any sorting methods on it. It should also sort the items efficiently, without you having to think of sorting algorithms.
Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.
— Rob Pike’s 5 Rules of Programming
Sorting integers or strings is pretty straightforward. However, what if you want to sort a class by some property? Let’s say you are writing a list of meals you eat, and store their names and timestamps. How would you sort the meals by timestamp from the lowest to highest? Luckily, it’s pretty simple. It’s enough to implement the Comparable
interface in the Meal
class and override the compareTo()
function. To sort the meals by lowest timestamp to highest, we could write something like this.
@Override
public int compareTo(Object object) {
Meal meal = (Meal) object;
if (this.timestamp < meal.getTimestamp()) {
return -1;
} else if (this.timestamp > meal.getTimestamp()) {
return 1;
}
return 0;
}
3. Location Updates
There are a lot of apps out there which collect the user’s location. You should use the Google Location Services API for that purpose, which contains a lot of useful functions. There is a separate article about using it, so I will not repeat it.
I’d just like to stress some important points from a performance perspective.
First of all, use only the most precise location as you need. For example, if you are doing some weather forecasting, you don’t need the most accurate location. Getting just a very rough area based on the network is faster, and more battery efficient. You can achieve it by setting the priority to LocationRequest.PRIORITY_LOW_POWER
.
You can also use a function of LocationRequest
called setSmallestDisplacement()
. Setting this in meters will cause your app to not be notified about location change if it was smaller than the given value. For example, if you have a map with nearby restaurants around you, and you set the smallest displacement to 20 meters, the app will not be making requests for checking restaurants if the user is just walking around in a room. The requests would be useless, as there wouldn’t be any new nearby restaurant anyway.
The second rule is requesting location updates only as often as you need them. This is quite self explanatory. If you are really building that weather forecast app, you do not need to request the location every few seconds, as you probably don’t have such precise forecasts (contact me if you do). You can use the setInterval()
function for setting the required interval in which the device will be updating your app about the location. If multiple apps keep requesting the user’s location, every app will be notified at every new location update, even if you have a higher setInterval()
set. To prevent your app from being notified too often, make sure to always set a fastest update interval with setFastestInterval()
.
And finally, the third rule is requesting location updates only if you need them. If you are displaying some nearby objects on the map every x seconds and the app goes in background, you do not need to know the new location. There is no reason to update the map if the user cannot see it anyway. Make sure to stop listening for location updates when appropriate, preferably in onPause()
. You can then resume the updates in onResume()
.
4. Network Requests
First, the problem with Java networking, which made its way into Android, is the tricky APIs. In case you don’t know, many java.net API calls are blocking. This is another excellent reason to not do networking on the UI thread. Remember that stream calls are blocking, and to make things even more interesting, equals of the URL class result in DNS call — this is time that you are paying. Use libraries, or any other threading solution that fits to your program.
The hard part about the networking is they’re doing Async Http. If I remember correctly from Android 4.4, OkHttp is the part of a Android sources, however, if you feel that you need the latest version of OkHttp with some stuff that hasn’t made its way into the official Android, consider bundling your own. There is another public library which is made by Google, Volley. There is a excellent library made by Square called Retrofit for your REST calls, all the solutions you have to make your app network-friendly successful.
There is a high chance that your app is using the internet for downloading or uploading data. If it is, you have several reasons to pay attention to handling network requests. One of them is mobile data, which is very limited to a lot of people and you shouldn’t waste it.
The second one is battery. Both WiFi and mobile networks can consume quite a lot of it if they are used too much. Let’s say that you want to download 1 kb. To make a network request, you have to wake up the cellular or WiFi radio, then you can download your data. However, the radio will not fall asleep immediately after the operation. It will stay in a fairly active state for about 20–40 more seconds, depending on your device and carrier.
So, what can you do about it? Batch. To avoid waking up the radio every couple seconds, prefetch things that the user might need in the upcoming minutes. The proper way of batching is highly dynamic depending on your app, but if it is possible, you should download the data the user might need in the next 3–4 minutes. One could also edit the batch parameters based on the user’s internet type, or charging state. For example, if the user is on WiFi while charging, you can prefetch a lot more data than if the user is on mobile internet with low battery. Taking all these variables into account can be a tough thing, which only few people would do. Luckily though, there is GCM Network Manager to the rescue!
GCM Network Manager is a really helpful class with a lot of customizable attributes. You can easily schedule both repeating and one-off tasks. At repeating tasks you can set the lowest, as well as the highest repeat interval. This will allow batching not only your requests, but also requests from other apps. The radio has to be woken up only once per some period, and while it’s up, all apps in the queue download and upload what they are supposed to. This Manager is also aware of the device’s network type and charging state, so you can adjust accordingly. You can find more details and samples in this article, I urge you to check it out. An example task looks like this:
Task task = new OneoffTask.Builder()
.setService(CustomService.class)
.setExecutionWindow(0, 30)
.setTag(LogService.TAG_TASK_ONEOFF_LOG)
.setUpdateCurrent(false)
.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
.setRequiresCharging(false)
.build();
By the way, since Android 3.0, if you do a network request on the main thread, you will get a NetworkOnMainThreadException
. That will definitely warn you not to do that again.
5. Reflection
Reflection is the ability of classes and objects to examine their own constructors, fields, methods, and so on. It is used usually for backward compatibility, to check if a given method is available for a particular OS version. If you have to use reflection for that purpose, make sure to cache the response, as using reflection is pretty slow. Some widely used libraries use Reflection too, like Roboguice for dependency injection. That’s the reason why you should prefer Dagger 2. For more details about reflection, you can check a separate post.
6. Autoboxing
Autoboxing and unboxing are processes of converting a primitive type to an Object type, or vice versa. In practice it means converting an int to an Integer. For achieving that, the compiler uses the Integer.valueOf()
function internally. Converting is not just slow, Objects also take a lot more memory than their primitive equivalents. Let’s look at some code.
Integer total = 0;
for (int i = 0; i < 1000000; i++) {
total += i;
}
While this takes 500ms on average, rewriting it to avoid autoboxing will speed it up drastically.
int total = 0;
for (int i = 0; i < 1000000; i++) {
total += i;
}
This solution runs at around 2ms, which is 25 times faster. If you don’t believe me, test it out. The numbers will be obviously different per device, but it should still be a lot faster. And it’s also a really simple step to optimize.
Okay, you probably don’t create a variable of type Integer like this often. But what about the cases when it is more difficult to avoid? Like in a map, where you have to use Objects, like Map<Integer, Integer>
? Look at the solution many people use.
Map<Integer, Integer> myMap = new HashMap<>();
for (int i = 0; i < 100000; i++) {
myMap.put(i, random.nextInt());
}
Inserting 100k random ints in the map takes around 250ms to run. Now look at the solution with SparseIntArray.
SparseIntArray myArray = new SparseIntArray();
for (int i = 0; i < 100000; i++) {
myArray.put(i, random.nextInt());
}
This takes a lot less, roughly 50ms. It’s also one of the easier methods for improving performance, as nothing complicated has to be done, and the code also stays readable. While running a clear app with the first solution took 13MB of my memory, using primitive ints took something under 7MB, so just the half of it.
SparseIntArray is just one of the cool collections that can help you avoid autoboxing. A map like Map<Integer, Long>
could be replaced by SparseLongArray
, as the value of the map is of type Long
. If you look at the source code of SparseLongArray
, you will see something pretty interesting. Under the hood, it is basically just a pair of arrays. You can also use a SparseBooleanArray
similarly.
If you read the source code, you might have noticed a note saying that SparseIntArray
can be slower than HashMap
. I’ve been experimenting a lot, but for me SparseIntArray
was always better both memory and performance wise. I guess it’s still up to you which you choose, experiment with your use cases and see which fits you the most. Definitely have the SparseArrays
in your head when using maps.
7. OnDraw
As I’ve said above, when you are optimizing performance, you will probably see the most benefit in optimizing code which runs often. One of the functions running a lot is onDraw()
. It may not surprise you that it’s responsible for drawing views on the screen. As the devices usually run at 60 fps, the function is run 60 times per second. Every frame has 16 ms to be fully handled, including its preparation and drawing, so you should really avoid slow functions. Only the main thread can draw on the screen, so you should avoid doing expensive operations on it. If you freeze the main thread for several seconds, you might get the infamous Application Not Responding (ANR) dialog. For resizing images, database work, etc., use a background thread.
I’ve seen some people trying to shorten their code, thinking that it will be more efficient that way. That definitely isn’t the way to go, as shorter code totally doesn’t mean faster code. Under no circumstances should you measure the quality of code by the number of lines.
One of the things you should avoid in onDraw()
is allocating objects like Paint. Prepare everything in the constructor, so it’s ready when drawing. Even if you have onDraw()
optimized, you should call it only as often as you have to. What is better than calling an optimized function? Well, not calling any function at all. In case you want to draw text, there is a pretty neat helper function called drawText()
, where you can specify things like the text, coordinates, and the text color.
Slow Rendering
Slow Rendering is the most common performance problem. Because what the the designers want from us and what we do, may not be the same and trying to do the best visuality, we can sometime fail in development.
Rendering is defined in terms of times which ensures that app is running Butterly smooth at a constant 60 FPS without any dropped or delayed frames.
What Causes Slow Rendering?
System tries to attempt redrawn your activities after every 16ms. This means that our app has to do all the logic to update the screen in that 16 ms.
It’s called dropped frame. For example, if your calculation takes 24 ms, this case happens. System tried to draw a new picture to the screen, but it wasn’t ready. So it did not refresh anything. And this caused, user’s seeing the refreshed picture after 32 ms instead of 16ms. If even there is one dropped frame, the animation will start not to be seen smooth.
8. ViewHolders
You probably know this one, but I cannot skip it. The Viewholder design pattern is a way of making scrolling lists smoother. It is a kind of view caching, which can seriously reduce the calls to findViewById()
and inflating views by storing them. It can look something like this.
static class ViewHolder {
TextView title;
TextView text; public ViewHolder(View view) {
title = (TextView) view.findViewById(R.id.title);
text = (TextView) view.findViewById(R.id.text);
}
}
Then, inside the getView()
function of your adapter, you can check if you have a useable view. If not, you create one.
ViewHolder viewHolder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.list_item, viewGroup, false);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}viewHolder.title.setText("Hello World");
You can find a lot of useable info about this pattern around the internet. It can also be used in cases when your list view has multiple different types of elements in it, like some section headers.
9. Resizing Images
Chances are, your app will contain some images. In case you are downloading some JPGs from the web, they can have really huge resolutions. However, the devices they will be displayed on will be a lot smaller. Even if you take a photo with the camera of your device, it needs to be downsized before displaying as the photo resolution is a lot bigger than the resolution of the display. Resizing images before displaying them is a crucial thing. If you’d try displaying them in full resolutions, you’d run out of memory pretty quickly. There is a whole lot written about displaying bitmaps efficiently in the Android docs, I will try summing it up.
So you have a bitmap, but you don’t know anything about it. There’s a useful flag of Bitmaps called inJustDecodeBounds at your service, which allows you to find out the bitmap’s resolution. Let’s assume that your bitmap is 1024x768, and the ImageView used for displaying it is just 400x300. You should keep dividing the bitmap’s resolution by 2 until it’s still bigger than the given ImageView. If you do, it will downsample the bitmap by a factor of 2, giving you a bitmap of 512x384. The downsampled bitmap uses 4x less memory, which will help you a lot with avoiding the famous OutOfMemory error.
Now that you know how to do it, you should not do it. … At least, not if your app relies on images heavily, and it’s not just 1–2 images. Definitely avoid stuff like resizing and recycling images manually, use some third party libraries for that. The most popular ones are Picasso by Square, Universal Image Loader, Fresco by Facebook, or my favourite, Glide. There is a huge active community of developers around it, so you can find a lot of helpful people at the issues section on GitHub as well.
As far as image libraries go, the following typically provide the required functionality, based on our experience working with leading companies: Glide, Picasso, Fresco. Just know that they have all different design trade-offs, especially around the large images count.
10. Strict Mode
Strict Mode is a quite useful developer tool that many people don’t know about. It’s usually used for detecting network requests or disk accesses from the main thread. You can set what issues Strict Mode should look for and what penalty it should trigger. A google sample looks like this:
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
If you want to detect every issue Strict Mode can find, you can also use detectAll()
. As with many performance tips, you should not blindly try fixing everything Strict Mode reports. Just investigate it, and if you are sure it’s not an issue, leave it alone. Also make sure to use Strict Mode only for debugging, and always have it disabled on production builds.
Wrapping Up
So, this was the good news. The bad new is, optimizing Android apps is a lot more complicated. There are a lot of ways of doing everything, so you should be familiar with the pros and cons of them. There usually isn’t any silver bullet solution which has only benefits. Only by understanding what’s happening behind the scenes will you be able to pick the solution which is best for you. Just because your favorite developer says that something is good, it doesn’t necessarily mean that it’s the best solution for you. There are a lot more areas to discuss and more profiling tools which are more advanced, so we might get to them next time