Should you inflate Android Views on a background thread?

Should you inflate Android Views on a background thread?
Inflation can be slow, but inflating off-UI-thread is playing with fire. Photo by Will O / Unsplash

TL;DR: No.

Android code can be slow. Profiling will often reveal the cause as"View inflation", which just means "constructing a View", e.g. calling new TextView(Context), or new ImageView(Context), or new LinearLayout(Context). These constructors often do a lot of work, slowly (for reasons out of scope to this post), and they block the UI thread.

When people find UI-thread-blocking jank, they naturally want to speed it up. In Android, the usual advice is to move janky slow stuff to a background thread. But it's unsafe to move View inflation to a background thread. You may make your code faster, but you risk undefined behaviour: very annoying, impossible-to-debug failures that will waste your team's time.

Android is pretty clear on this:

the Android UI toolkit is not thread-safe. So, you must not manipulate your UI from a worker thread—you must do all manipulation to your user interface from the UI thread.

The Problems

View constructors access their Context, their parent View, and static variables. All of this data is mutable, and written and read without synchronization. And if you do these reads and writes from different threads, you have data races, you may see both old and new data. You'll get undefined behaviour.

Here is a (non-exhaustive) list of examples:

  1. Mutable Context. Context (really, ContextImpl behind the scenes) is very mutable. Some data is protected by mutexes, but not all. For example, ContextImpl.mDisplay is non-final and mutated. Without synchronization, there's no safe publication across threads. Views may access the Display to decide what UI to show.
  2. Parent LayoutParams. Views often access their parent's LayoutParams to decide how to render. The parent could be mutated from the UI thread (perhaps in the middle of a layout pass) while you're reading their params.
  3. Static Variables. Android often puts data in mutable static datastructures, to save allocations. If you're read these concurrently with a write, you'll see corrupted data.

For example, every View subclass must eventually call the super-constructor android.view.View(Context), which calls ViewConfiguration.get(Context), which reads and writes a static SparseMap cache variable:

static final SparseArray<ViewConfiguration> sConfigurations =
    new SparseArray<ViewConfiguration>(2);

public static ViewConfiguration get(@NonNull @UiContext Context context) {
  ...
  ViewConfiguration configuration = sConfigurations.get(density);
  if (configuration == null) {
    configuration = new ViewConfiguration(context);
    sConfigurations.put(density, configuration);
  }
  ...
}
Reading and writing a static (global) SparseArray sConfigurations. No synchronization!

SparseArray is a compound data structure, backed by an Object[] and an int length. Interleaved calls to this method from different threads are a data race, and could (for example) result in an int length longer than the Object[]'s size.

This is just one data race, which affects all Views. There are probably many more races. Custom Views can have arbitrary logic.

What about AsyncLayoutInflater?

What about AsyncLayoutInflater, the official Android support library which "Triggers view inflation on background thread"?

AsyncLayoutInflater notes some requirements about where it should be used:

For a layout to be inflated asynchronously it needs to have a parent whose generateLayoutParams is thread-safe and all the Views being constructed as part of inflation must not create any Handlers or otherwise call myLooper.

This addresses problem #2 above... but not #1 and #3.

So is AsyncLayoutInflater safe? I don't think so.

Mark Hansen

Mark Hansen

I'm a Software Engineering Manager working on Google Maps in Sydney, Australia. I write about software {engineering, management, profiling}, data visualisation, and transport.
Sydney, Australia