ANR internals: touch dispatching through the view hierarchy

ANR internals: touch dispatching through the view hierarchy

I'm writing a blog series on ANR internals, where I'll use ANRs as an excuse to learn more about how various parts of Android work. This first article is focused on touch dispatching through the view hierarchy.

ANR triggers

How is an "Application Not Responding" (ANR) error triggered?

According to the Android documentation on ANRs:

When the UI thread of an Android app is blocked for too long, an "Application Not Responding" (ANR) error is triggered.

While blocking the UI thread is the cause of most ANRs, the Android OS doesn't care what your app's main thread is doing. Instead, it has expectations for how long apps should take to handle a few specific events. ANR means the application is not responding to the system (rather than to the user):

Input dispatching timed out

Input dispatching timed-out is the ANR trigger that Android developers are most familiar with:

An ANR is triggered for your app when your app has not responded to an input event (such as key press or screen touch) within 5 seconds.

To understand how these ANRs get triggered, it's helpful to understand how input dispatching works. In this article, we'll start by looking at touch dispatching through the view hierarchy.

View touch event dispatching

To start, let's add a breakpoint in a View.OnClickListener to see what happens when we tap a button:

View.PerformClick is a Runnable that invokes View.OnClickListener#onClick :

public class View {

  private final class PerformClick implements Runnable {
    @Override
    public void run() {
      performClick();
    }
  }

  public boolean performClick() {
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
      li.mOnClickListener.onClick(this);
      return true;
    } else {
      return false;
    }
  }
}

(source)

The View.PerformClick runnable is posted to the main thread on MotionEvent.ACTION_UP in View#onTouchEvent, and runs later on as the main thread looper dequeues its messages.

public class View {

  public boolean onTouchEvent(MotionEvent event) {
    switch (action) {
      case MotionEvent.ACTION_UP:
        // Use a Runnable and post this rather than calling
        // performClick directly. This lets other visual state
        // of the view update before click actions start.
        if (mPerformClick == null) {
          mPerformClick = new PerformClick();
        }
        mHandler.post(mPerformClick)
    }
  }
}

(source)

Now let's add a breakpoint to View#onTouchEvent to understand where touch events come from:

MessageQueue#next invokes InputEventReceiver#dispatchInputEvent from native code. The event goes through a chain of ViewRootImpl.InputStage delegates before getting dispatched through the view hierarchy via ViewGroup#dispatchTouchEvent.

We merge those two sequence diagrams:

Compose touch event dispatching

Let's add a breakpoint in a Compose click lambda to understand how Compose handles taps:

Button(
    onClick = { /* breakpoint here */ },
}

MessageQueue#next invokes InputEventReceiver#dispatchInputEvent from native code. The event goes through a chain of ViewRootImpl.InputStage delegates before getting dispatched to through the view hierarchy and then getting dispatched through Compose nodes which eventually invoke the click lambda.

Aside: smoke & mirrors

Notice how the view framework posts the invocation of the view listener, whereas Compose invokes the lambda immediately on MotionEvent.ACTION_UP. This is presumably more efficient (no delay in handling of taps). However, if your tap handling happens to be slow and blocks the main thread for a bit (e.g. updating the entire UI in response to a tap), the view posting allows the render thread to start a ripple animation on the button on MotionEvent.ACTION_UP and the ripple will animate while the main thread is blocked. I noticed this when, after migrating a button from views to compose, the interaction felt worse and slower even though the performance was similarly bad.

Conclusion

Today we saw that the Android framework has native code that invokes InputEventReceiver#dispatchInputEvent which then dispatches touch events to the view hierarchy of the target window. With Compose, clicks listeners are invoked immediately on MotionEvent.ACTION_UP whereas with views click listeners are invoked from a main thread post enqueued on MotionEvent.ACTION_UP.

This article was a warm-up, next time we'll dig into something a little more interesting: Looper internals.