android-galaxyzoo: Network I/O and UI responsiveness

While developing the Galaxy Zoo app for Android, I experienced problems with UI responsiveness while the app was downloading from, or uploading to, the remote server. I don’t mean just the usual problems caused by doing network I/O or file system access in the UI thread – mistakes that can often be caught by using Strict Mode. I mean that buttons didn’t respond to presses if the app happened to be doing I/O in an AsyncTask.doInBackground().

I solved this problem, but it was an incredibly frustrating journey. These notes might help the next person. So many Android apps are just a fancy way to interact with a website’s data, caching it locally, but the regular Android API doesn’t make that easy enough.

My app uses a ContentProvider, because the Android API seems to point us towards doing so, as I found while creating my experimental Android Glom app. The ContentProvider usually runs in the same process (and thread) as the main UI, so I don’t think this architecture was a cause of the UI responsiveness problem.

Thread+ResponseHandler versus AsyncTask

My ContentProvider needed to communicate with the remote server via REST messages, downloading images from the URLs that it then discovered and caching those images locally as well as storing data in its SQLite database. At first, I used the Thread class directly, with a UriRequestTask class derived from Runnable, that used HttpClient and accepted a (HttpClient) ResponseHandler. This was based on the example code in O’Reilly’s Programming Android (2nd Edition) book, in its “A Content Provider as a Facade for a RESTful Web Service” chapter (which you can see some of here).

Later, before I found the UI responsiveness problem, I replaced this Thread and RequestTask idea with AsyncTask because that seemed more robust and made it easier to get a result back to the main thread. I didn’t understand why the “Programming Android” book didn’t use AsyncTask in its ContentProvider. Yet later, I found that Strict Mode didn’t allow that use of AsyncTask in a SyncAdapter’s non-UI thread on some older devices (API version <15), causing an ExceptionInInitializerError with “Can’t create handler inside thread that has not called Looper.prepare()”. Maybe the authors found the same problem.

HttpURLConnection instead of HttpClient

Incidentally, I later realized that the Android developers now discourage use of HttpClient, because their version of HttpClient is buggy and they can’t update it without breaking compatibility. So I changed from HttpClient to HttpURLConnection (and here and here). Luckily, this fixed the endless redirect problem I (like others) was having with my uploads.

Again, I felt led astray by the “Programming Android” book, but books get out of date very quickly and the Android documentation apparently gives us new clues only reluctantly. Hopefully the 3rd edition will improve this.

Avoiding the user

This is when I noticed the UI responsiveness problem. I tried using Thread directly in various ways instead of AsyncTasks, fearing my own changes had caused it, but it made no difference.

I tried all kinds of tricks to avoid doing any I/O, even in the AsyncTasks, when the user was likely to be using the UI, trying to wait until it seemed that the user was idle and then doing some quick I/O before getting out of the way again. But the user is unpredictable and would still try to press a button now and then exactly when my app was doing some sneaky I/O.

After lots of imprecise testing, I was fairly sure that the problem was worse when the app happened to be doing some filesystem I/O, even just writing to the SQLite database, but it was definitely caused too by doing network I/O of even small JSON files. We should expect disk I/O to be worse than network I/O, of course.

SyncAdapter

I wanted to learn about Android’s SyncAdapter framework anyway, so I spent a week figuring that out despite its awful hand-wavy documentation. Luckily, it fixed the UI responsiveness problems. I think it helped partly because the I/O was now in a separate process, though that doesn’t feel like it should be necessary. Also, the SyncAdapter framework can decide when best to let the SyncAdapter do its work, based on the whole system. Presumably it can then prevent unrelated processes from making the UI responsiveness even worse.

Unfortunately, using SyncAdapter does mean that I lose control over when I may download items in advance. For instance, I often find that a new install of the app doesn’t fill up its initial list of ten items until five or ten minutes after the first start. In the meantime, the app requests the items directly when necessary. I call ContentProvider.requestSync(), with SYNC_EXTRAS_MANUAL, when I’m sure there’s work for the SyncAdapter to do, but it just seems to take this as a suggestion that it’s free to ignore.

Authenticator and Account

To use the SyncAdapter, I had to create an Authenticator, for which I needed an Account type, even if you are accessing your server anonymously. This account type will show up in the Settings->Accounts UI. Presumably this is why some other apps show up in the Accounts Settings, even though they don’t need any login. Maybe they are doing it just so they can use SyncAdapter.

SyncAdapter versus SharedPreferences/PreferenceFragment

So I started by storing the login details via the AccountManager instead of via SharedPreferences. I later discovered that the app’s SharedPreferences (such as how many items to download) were unavailable to the SyncAdapter’s separate process. That is documented as “currently this class does not support use across multiple processes“. So I later put other preferences in the account too. Unfortunately, the standard settings/preferences UI only uses SharedPreferences, so I have hacky code to keep the SharedPreferences and the Account in sync – see my onSharedPreferencesChange() override in my PreferenceFragment, so the account’s UserData is changed when the standard settings UI is used, via my hacky copyPrefToAccount method.

AccountManager versus main/UI thread

I also later found that you can’t use the AccountManager API from the main UI thread even though the documentation says things like “It is safe to call this method from the main thread”. The Firefox on Android developers discovered this too (1, 2, 3). So I moved that code to an AsyncTask (for example). If you don’t then you’ll get StrictMode exceptions at least on API level 15 and 16 – maybe others. StrictMode seems to have been on for Android Honeycomb, and some users seem to enable StrictMode, so you will see this as crash reports on some devices.

Volley

I also wanted to try out the Volley library. This isn’t an official part of the Android API. For instance, there’s not even an official way to add it as a dependency – I used the Volley package that mcxiaoke helpfully published to maven central. But Google use Volley in some of their own apps and they recommend Volley in the Android documentation. The video linked from there is well worth watching.

When I rewrote my HttpURL Connection-using code with Volley it seemed to make my downloads noticeably faster, probably because it avoids doing I/O in too many threads simultaneously. This was an easy win.

I couldn’t use Volley for my uploads, unfortunately, because Volley uses HttpClient on older Android versions whose HttpUrlConnection wasn’t good enough yet, causing the old endless-redirect problem again. So I use HttpURLConnection directly for that.

Picasso

I also switched to using Picasso to fill ImageViews with bitmaps from our ContentProvider or directly from the internet, instead of my own AsyncTask. This simplified the code and gave me some extra caching, particularly simplifying my ListView/RecyclerView adaptor that previously had its own custom LruCache. For instance 1, 2, 3. Image loading just generally felt much faster everywhere that I used Picasso, though I don’t know exactly why.

Just filling an ImageView without blocking the UI seems like something unexciting that the Android API should already make easy.

2 thoughts on “android-galaxyzoo: Network I/O and UI responsiveness

    1. Yes, thanks, that might work. But I only discovered the (redirect when POSTing) problem after I’d one lots of testing already and I didn’t want to test everything all over again on the various Android versions. I also tend to trust the defaults more. I wonder why okhttp isn’t the default for Volley.

Comments are closed.