Skip to content Skip to sidebar Skip to footer

Modifying Views In Asynctask Doinbackground() Does Not (always) Throw Exception

I just came across some unexpected behaviour when playing around with some sample code. As 'everybody knows' you cannot modify UI elements from another thread, e.g. the doInBackgro

Solution 1:

The checkThread() method of ViewRootImpl.java is responsible for throwing this exception. This check is suppressed using member mHandlingLayoutInLayoutRequest until performLayout() i.e all the initial drawing traversals are complete.

hence it throws exception only if we use delay.

Not sure if this is a bug in android or intentional :)

Solution 2:

Based on RocketRandom's answer I've done some more digging and came up with a more comprehensive answer, which I feel is warranted here.

Responsible for the eventual exception is indeed ViewRootImpl.checkThread() which is called when performLayout() is called. performLayout() travels up the view hierarchy until it eventually ends up in ViewRootImpl, but it originates in TextView.checkForRelayout(), which is called by setText(). So far so good. So why does the exception sometimes not get thrown when we call setText()?

TextView.checkForRelayout() is only called if the TextView already has a Layout (mLayout != null). (This check is what inhibits the exception from being thrown in this case, not mHandlingLayoutInLayoutRequest in ViewRootImpl.)

So, again, why does the TextView sometimes not have a Layout? Or better, since obviously it starts out not having one, when and where does it get it from?

When the TextView is initially added to the LinearLayout using layout.addView(tv);, again, a chain of requestLayout() is called, travelling up the View hierarchy, ending up in ViewRootImpl, where this time, no exception is thrown, because we're still on the UI thread. Here, ViewRootImpl then calls scheduleTraversals().

The important part here is that this posts a callback/Runnable onto the Choreographer message queues, which is processed "asynchronously" to the main flow of execution:

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

The Choreographer will eventually process this using a Handler and run whatever RunnableViewRootImpl has posted here, which will eventually call performTraversals(), measureHierarchy(), and performMeasure() (on ViewRootImpl), which will perform a further series of View.measure(), onMeasure() calls (and a few others), travelling down the View hierarchy until it finally reaches our TextView.onMeasure(), which calls makeNewLayout(), which calls makeSingleLayout(), which finally sets our mLayout member variable:

mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
            effectiveEllipsize, effectiveEllipsize == mEllipsize);

After this happens, mLayout isn't null any more, and any attempt to modify the TextView, i.e. calling setText() as in our example, will lead to the well known CalledFromWrongThreadException.

So what we have here is a nice little race condition, if our AsyncTask can get its hands on the TextView before the Choreographer traversals are complete, it can modify it without penalties. Of course this is still bad practice, and shouldn't be done (there are many other SO posts dealing with this), but if this is done accidentally or unknowingly, the CalledFromWrongThreadException is not a perfect protection.

This contrived example uses a TextView and the details may vary for other views, but the general principle remains the same. It remains to be seen if some other View implementation (perhaps a custom one) that doesn't call requestLayout() in every case may be modified without penalties, which might lead to bigger (hidden) issues.

Solution 3:

You can write in doInBackground to a TextView if it is not part of the GUI yet.

It is only part of the GUI after statement setContentView(layout);.

Just my thought.

Post a Comment for "Modifying Views In Asynctask Doinbackground() Does Not (always) Throw Exception"