RxAndroid's New Async API

“RxAndroid meets VSYNC rubber” — Icon used with permission from Ray Wenderlich

RxAndroid 2.1.0 has a new API

AndroidSchedulers#from(Looper looper, boolean async)

This new async parameter affects Android APIs 16 and newer, and can significantly improve UI performance when set to true if your app makes heavy use of RxJava+RxAndroid.

As RxAndroid’s major versions are tied to RxJava’s and we didn’t want to silently introduce a significant behavior change in a minor release, this API is not enabled by default.

To install it, you can use RxAndroidPlugins to set it as a custom scheduler using this API:

Edit: Due to classloading, the below code is slightly wrong for setInitMainThreadSchedulerHandler. To avoid initialization of the default one, you should call AndroidSchedulers.from(...) from inside the lambda/callable passed in, rather than before.
Kotlin
Java

Context

This is a long time in the making. The main thread scheduler used in RxAndroid has historically always used Handler#post() to schedule new Messages. This normally comes at a cost though: by default, this abides by VSYNC locking and will result in waiting until the next frame to run. That’s a delay of up to 16ms for every emission to go through the post(). To exacerbate things, this happens even if you’re already on the main thread (something Ray Ryan covers in this excellent talk).

So, in 2015 a discussion was started on Jake Wharton’s RxBinding project around “fastpath”-ing the main thread scheduler to immediately run work if it was already on the main thread and avoid the VSYNC cost. This discussion moved to RxAndroid as a proposal PR with a lot of good community feedback, but a consensus was never reached due to concerns around potentially racing the system’s event looper by running events directly and risking deadlocks. So it was punted, but remained a point of friction for consumers with several issues filed around it well into RxAndroid 2.x.

Fast forward to early 2017, at Uber we decided to try to re-hash this internally and found it to be mostly stable. We did see deadlocks from time to time, but they were hard to detect and seemed rare enough to be worth the performance gains we got. After ~1yr in production, we decided to try upstreaming our implementation and rehashing the discussion. This time, Android framework engineers (namely Adam Powell) saw it and chimed in to point us to Message and Handler’s asynchronous APIs. This API allows for Messages to bypass VSYNC locking while still letting the framework handle all the scheduling safely in its looper, which is exactly what we wanted all along!


Wiring in the API

While the asynchronous APIs have existed since API 16, they’d been hidden in the SDK via @hide. In API 22, Message#setAsynchronous() was made public. In the API 28, there’s a new Handler.createAsync() factory API that sets all Messages it handles to be asynchronous by default. Since these APIs power some of the most critical parts of the OS they are unlikely to have been changed and should be safe to access. Here’s where it gets a bit fun.

API 22+

The aforementioned public setAsynchronous() method is used.

API [16–21]

setAsynchronous() is still used but we suppress the lint error that says it’s only 22+. To avoid any (unlikely) OEM situations of deleted/changed internal APIs, we try/catch a quick Message#setAsynchronous() method call in the from() Scheduler factory to ensure it’s there at runtime, catching the NoSuchMethodError if it is missing and falling gracefully back to the standard non-async messaging. It’s not reflection though, because we know this will link at runtime since the method does exist at runtime.

API <16

There is no behavior change and the standard non-async messaging is used since the asynchronous APIs didn’t exist.


That’s it! I hope this gives developers some added peace of mind when using the main thread scheduler. Please give it a try and report any issues :).

Thanks to Florina Muntenescu and Jake Wharton for proofreading.


Note: This was originally posted on my Medium blog, but I am migrating away from Medium.