Troubleshooting problems with requestLayout on Android

Update: I’ve posted a StackOverflow question on this topic, which has a shorter write-up with diagrams. I have also added those diagrams to this post and condensed the explanation a bit.

One thing I hope to accomplish with this blog is to document some weird findings that I haven’t found on StackOverflow or elsewhere, in hopes of sparing others the pain I had to endure to find the answers I needed.

With that said, nothing is worse than hunting for answers online, finding a promising blog post that may contain the answers you need, but being forced to slog through paragraphs of tedious minutia because the author simply won’t cut to the chase. Huge pet peeve of mine. So, other than this one-time preamble, my plan is to always post the good stuff first, and then include the exhaustive detail afterwards for people who want to know more.

The problem

On more than one occasion, I’ve run into a problem where I’m trying to change the LayoutParams of a view, and the view just refuses to cooperate. For instance, I may have some code like the following:

  ViewGroup.LayoutParams params = myView.getLayoutParams();
  params.height = someHeight;
  myView.requestLayout();

…and the height of myView just doesn’t change at all, ever.

The cause

There are a few different potential causes of this problem, but the focus of this post is on one particularly subtle cause: another view in the view hierarchy that has made a layout request on a background thread.

The solution

Often times, knowing the cause is the same as knowing the solution. However, the tricky part of this solution is finding the offending view among the hundreds of views in your hierarchy. Which view is making layout requests on a background thread? And on which line of code is the evil deed being done?

Luckily, there is an easy way to answer the first question:

  1. Step into the myView.requestLayout() method call.
  2. Look for a View reference called mAttachInfo.mViewRequestingLayout
  3. Whichever view that reference points to is your culprit.

Now, all that remains is to find all of the places in your code where the offending view is requesting a layout (look for calls requestLayout, forceLayout, setLayoutParams), and fix any cases where the request is being made from a background thread. Sometimes these can be tricky to find, e.g. if the view in question is being passed as an argument to another method, which performs the illegal layout request. You’ll have to follow all usages of the offending view to their logical conclusion.

I can’t prove that this solution works 100% of the time, but it hasn’t failed me yet. If this solution doesn’t work for you, you may be dealing with another cause entirely.

Why does this happen?

It’s common knowledge that modifying the view hierarchy on a background thread is a no-no, but it’s still useful to understand why it can cause this particular behavior.

First, it’s important to understand a few things about what happens when you call requestLayout:

  1. Layout is not performed on the view right away. Instead, the view sets a flag on itself, named PFLAG_FORCE_LAYOUT, which indicates that the view has requested layout.
  2. The view also calls requestLayout on its parent, which kicks off a recursive traversal up the view hierarchy, all the way to the view root. As a result, each ancestor of the view who originally requested layout (including the view root) has their layout flag set.
  3. The view root performs a layout pass once per frame. This is actually a double pass:
    1. During the first phase, all views who have requested layout are measured. None of those views should be calling requestLayout during this phase. However, if even if a view disobeys this rule, the view root will try to accommodate them by adding them to a special list.
    2. During the second phase, any views that are in that list are laid-out again. The view root ignores further layout requests during this phase.

That last point is important. Take a look at the implementation of requestLayout for the ViewRootImpl class:

@Override 
public void requestLayout() { 
    if (!mHandlingLayoutInLayoutRequest) { 
        checkThread(); 
        mLayoutRequested = true; 
        scheduleTraversals(); 
    } 
}

Note: ViewRootImpl is not a subclass of View and it does not use the PFLAG_FORCE_LAYOUT flag. Instead, it has its own mLayoutRequested flag, which serves the same purpose.

The mHandlingLayoutInLayoutRequest variable is true when the view root is performing the second phase of the layout pass. So, any calls to requestLayout during this phase will be effectively ignored.

This is done for a good reason, and shouldn’t cause any problems so long as all calls to requestlayout occur on the ui thread. The gist is that mLayoutRequested isn’t needed at this stage to determine whether the view root should perform the layout pass — it’s already performing one. In fact, the view root prefers to clear this flag at the beginning of the layout pass and then keep it cleared until the next frame. Further, if this method is called during the second phase of the layout pass, that means that some view requested layout during the first phase of the layout pass and the second phase of the layout pass, and the view root is not going to try to accommodate that.

However, if requestLayout is called from any thread other than the ui thread, this logic breaks. To understand why, we need one more piece of information. Take a look at the implementation of View.requestLayout, particularly the part in bold:

public void requestLayout() {
  if (mMeasureCache != null) mMeasureCache.clear();

  if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
    // Only trigger request-during-layout logic if this is the view requesting it,
    // not the views in its parent hierarchy
    ViewRootImpl viewRoot = getViewRootImpl();
    if (viewRoot != null && viewRoot.isInLayout()) {
      if (!viewRoot.requestLayoutDuringLayout(this)) {
        return;
      }
    }
    mAttachInfo.mViewRequestingLayout = this;
  }

  mPrivateFlags |= PFLAG_FORCE_LAYOUT;
  mPrivateFlags |= PFLAG_INVALIDATED;

  if (mParent != null && !mParent.isLayoutRequested()) {
    mParent.requestLayout();
  }
  if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
    mAttachInfo.mViewRequestingLayout = null;
  }
}

What you may now notice is that requestLayout stops recursing upward if a view’s parent indicates that its PFLAG_FORCE_LAYOUT flag has already been set.

Putting it all together, we can now see how things could break. Here are the steps:

Step 1: The initial state of the view hierarchy

Imagine we have the following view hierarchy.

Step 2: A naught view calls requestLayout from a background thread

In this case, the call happens to occur during the second phase of the layout pass. So, the request propagates all the way up to the view root, but the view root ignores it.

Since the request is ignored, the request for layout goes unfulfilled. The PFLAG_FORCE_LAYOUT flag is never cleared for these views.

Step 3: A nice view calls requestLayout from the ui thread

However, this request is blocked by the common ancestor of NiceView and NaughtyView. Since this common ancestor already has its PFLAG_FORCE_LAYOUT flag set, traversal stops, and the view root is still unaware that the requests have been made.

Calling requestLayout on a background thread invalidates certain assumptions that ViewRootImpl is (rightfully) making about the sequence of things. With the view hierarchy in the above state, any of the views with their PFLAG_FORCE_LAYOUT flag set would block layout requests from their descendants.

This problem may spontaneously fix itself if another view, who doesn’t share a common ancestor with NaughtyView, were to make a layout request. In that case, the ViewRootImpl would have its mLayoutRequested flag set, and on the next frame, it would measure all of its children who have requested layout, including NaughtyView and NiceView. However, you cannot count on this to fix the problem for you, so it’s much better not to call requestLayout from a background thread in the first place.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s