The Shortcomings of Android Thread Annotations
When the Android thread annotations such as
@WorkerThread were announced, I was excited – I could rest assured
that the compiler was on my side, ensuring I wasn’t causing performance
hits by writing to disk from the UI thread! However, many months later,
I’ve felt the annotations didn’t work as well as I had hoped but I didn’t
know the exact cause: I decided to investigate why.
The findings: thread annotations are reasonably robust except that they don’t follow the call stack. For example:
In practice, I’ve found this to be a glaring flaw. Annotated methods will often get wrapped by other methods to improve program readability, however, those wrapping methods will not warn the user like the wrapped methods were intended to. We could explicitly add an annotation to the wrapping method, however, it is redundant. Why is that a problem? Redundant code isn’t always changed together. For example:
Also, it’s easy to forget to add a redundancy and takes time to ensure
they’re consistent. How large is your call stack starting from a framework
annotated method like,
onCreate, to one of your utility methods like
writeJSONObjectToFile? It’s difficult to add the annotations to each
of them, nevermind adjust them when the code is refactored.
There is a large maintenance burden for adding redundant annotations and they’re likely to become incorrect over time.
Runnable instances do not infer which thread they’re running on:
From an implementation standpoint, this makes sense –
has to call
yourRunnable.run() and can’t set the thread annotation on
run method, given how they currently work.
We can explicitly annotate, however, note the redundancy:
This redundancy is not as bad as the one mentioned above (as we’ll see later).
The explicit annotation on the overridden method can also be added to a non-anonymous class:
HandlerThread instances do not infer their
and have a similar issue:
It’s the same situation if we create a new
Handler on a worker thread.
When is it useful to add thread annotations?
Thread annotations are most useful when the thread context of the method is unlikely to change. Some examples:
- Methods that directly wrap thread-specific APIs, e.g.
writeJSONObjectToDiskshould only run on a worker thread and
updateViewFromCursorshould only run on the UI thread.
- Methods that may not directly wrap thread-specific APIs but have a
specific thread context in mind, e.g.
HandlerThreadmethods, e.g. a
Runnableposted to the UI thread to update view state isn’t going on a worker thread any time soon.
- Interfaces used for callbacks if the callback will be called on a specific
thread – e.g. if your callback will be called on the UI thread, instead of
Runnable, create a new interface like
UiThreadRunnableand use that.
- Public APIs, e.g. as a library maintainer. Since these methods are
public, they’re hard to change without breaking others’ builds. As such,
their contract, including which thread they run on, is likely to stay
the same for many revisions. For example, the
Activity.onCreatecan be annotated with
These annotations may catch a few bugs but as we saw above, without redundancy, they won’t catch everything. That being said, they’re still useful – just know their shortcomings and don’t rely on them.
Also, when you add a thread annotation, consider adding a comment to explain why it’s there.
Working hard, not hardly working
Besides the concerns above, the thread annotations work just as I would expect! Here are a few of the places where thread annotations delight:
- Methods on the
Viewclasses that change visible View state are annotated
AsyncTaskis appropriately annotated –
- Overriding methods in a subclass inherit the thread annotations from the overridden method in the super class (or interface).
I have examples for these and more on Github.
Edit (4/13/16): Several notes since this was originally posted:
- Igor pointed out that thread annotations are simply Lint checks and do not affect one’s build or compilation.
- rnewman mentioned that an alternative is to use assertions to verify which thread your code is running on.
- I discovered that classes implementing an interface that has a thread annotation may display warnings if its constructor appears on thread that opposes the interface’s thread annotation. A work-around is to annotate the interface methods individually.
- jonfor pointed out some grammar errors.
Thanks everyone! :)