Using a cache to speed up data retrieval from Cursors

It’s easier than you think.


Most Android developers work with Cursors from time to time. It’s most likely not the highlight of the week, but there’s usually no getting around it.

Working with cursors can be a bit tedious and like many other aspects of Android development, users will immediately know if you haven’t done a good job. The most obvious reason for this is slow loading of data — and that affects the user experience. Users want to use apps, not wait for them.

So, what can we do to avoid this? Let’s take a look at an example of how you would usually load some data from a Cursor.

The problem

while (cursor.moveToNext()) {
fooList.add(
new Foo(
cursor.getString(cursor.getColumnIndex("dataColumnName1"),
cursor.getInt(cursor.getColumnIndex("dataColumnName2"),
cursor.getDouble(cursor.getColumnIndex("dataColumnName3")
)
);
}

The code snippet above is a pretty normal example of reading data from a Cursor. You usually need to read data from a number of columns, and this happens over and over again.

So, what can we do about this simple piece of code? Let’s break it down.

The Cursor related code includes four method calls: getString(), getInt(), getDouble() and getColumnIndex(). We’re interested in the last one.

Let’s check out the documentation for that method:

Returns the zero-based index for the given column name, or -1 if the column doesn’t exist.

So, all this method does is look up the index of a given column based on its name. That seems fairly simple, right? Sadly not.

Here’s the source code of the method in question:

public int getColumnIndex(String columnName) {
// Hack according to bug 903852
final int periodIndex = columnName.lastIndexOf(‘.’);
if (periodIndex != -1) {
Exception e = new Exception();
Log.e(TAG, “requesting column name with table name — “ + columnName, e);
columnName = columnName.substring(periodIndex + 1);
}
String columnNames[] = getColumnNames();
int length = columnNames.length;
for (int i = 0; i < length; i++) {
if (columnNames[i].equalsIgnoreCase(columnName)) {
return i;
}
}
if (false) {
if (getCount() > 0) {
Log.w(“AbstractCursor”, “Unknown column “ + columnName);
}
}
return -1;
}

The method performs a String index check (and potentially some light String modification) and then proceeds to loop through all column names, comparing each one to the one you supplied. If it finds a match, it returns the index.

It’s not uncommon for database tables to have 10-20 columns, so you’re in for some bad luck if you’re trying to look up the last one in the array of column names.

This tells us that quite a lot happens when you call getColumnIndex() — and keep in mind that this gets called a lot when retrieving data using a Cursor. It’s not uncommon to call the method several thousand times.

In other words, you’re asking the Android device to perform several thousand String comparisons — just to get the index of a database table column.

There’s no reason to do all that work over and over again, so let’s use a simple cache to prevent doing just that!

The solution

public class ColumnIndexCache {
private ArrayMap<String, Integer> mMap = new ArrayMap<>();
   public int getColumnIndex(Cursor cursor, String columnName) {
if (!mMap.containsKey(columnName))
mMap.put(columnName, cursor.getColumnIndex(columnName));
return mMap.get(columnName);
}
   public void clear() {
mMap.clear();
}
}

This simple cache does everything we need.

All column indexes are stored and it takes up very little memory. The benefit, however, is huge and it gets even greater if you had many getColumnIndex() calls in your original code.

Most applications will easily achieve performance improvements of at least 20 percent.

Let’s see it in action, applied to our initial example:

ColumnIndexCache cache = new ColumnIndexCache();
while (cursor.moveToNext()) {
fooList.add(
new Foo(
cursor.getString(cache.getColumnIndex(cursor,
"dataColumnName1"),
cursor.getInt(cache.getColumnIndex(cursor,
"dataColumnName2"),
cursor.getDouble(cache.getColumnIndex(cursor,
"dataColumnName3")
)
);
}
// Clear the cache after you're done
cache.clear();

It’s a simple change and easy to implement. Not only is it faster, it also makes your device use less memory and fewer CPU cycles.

Give it a try and measure the performance improvement you get!

PS. It’s not mentioned in the code snippets, but don’t forget to close the Cursor when you’re done reading from it!