12

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 doInBackground() of an AsyncTask.

For example:

public class MainActivity extends Activity {
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<TextView, Void, Void> {
        @Override
        protected Void doInBackground(TextView... params) {
            params[0].setText("Boom!");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        tv = new TextView(this);
        tv.setText("Hello world!");
        Button button = new Button(this);
        button.setText("Click!");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MyAsyncTask().execute(tv);
            }
        });
        layout.addView(tv);
        layout.addView(button);
        setContentView(layout);
    }
}

If you run this, and click the button, you're app will stop as expected and you'll find the following stack trace in logcat:

11:21:36.630: E/AndroidRuntime(23922): FATAL EXCEPTION: AsyncTask #1
...
11:21:36.630: E/AndroidRuntime(23922): java.lang.RuntimeException: An error occured while executing doInBackground()
...
11:21:36.630: E/AndroidRuntime(23922): Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
11:21:36.630: E/AndroidRuntime(23922): at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)

So far so good.

Now I changed the onCreate() to execute the AsyncTask immediately, and not wait for the button click.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // same as above...
    new MyAsyncTask().execute(tv);
}

The app doesn't close, nothing in the logs, TextView now displays "Boom!" on the screen. Wow. Wasn't expecting that.

Maybe too early in the Activity lifecycle? Let's move the execute to onResume().

@Override
protected void onResume() {
    super.onResume();
    new MyAsyncTask().execute(tv);
}

Same behaviour as above.

Ok, let's stick it on a Handler.

@Override
protected void onResume() {
    super.onResume();
    Handler handler = new Handler();
    handler.post(new Runnable() {
        @Override
        public void run() {
            new MyAsyncTask().execute(tv);
        }
    });
}

Same behaviour again. I'm running out of ideas and try postDelayed() with a 1 second delay:

@Override
protected void onResume() {
    super.onResume();
    Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            new MyAsyncTask().execute(tv);
        }
    }, 1000);
}

Finally! The expected exception:

11:21:36.630: E/AndroidRuntime(23922): Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Wow, this is timing related?

I try different delays and it appears that for this particular test run, on this particular device (Nexus 4, running 5.1) the magic number is 60ms, i.e. sometimes is throws the exception, sometimes it updates the TextView as if nothing had happened.

I'm assuming this happens when the view hierarchy has not been fully created at the point where it is modified by the AsyncTask. Is this correct? Is there a better explanation for it? Is there a callback on Activity that can be used to make sure the view hierachy has been fully created? Timing related issues are scary.

I found a similar question here Altering UI thread's Views in AsyncTask in doInBackground, CalledFromWrongThreadException not always thrown but there is no explanation.

Update:

Due to a request in comments and a proposed answer, I have added some debug logging to ascertain the chain of events...

public class MainActivity extends Activity {
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<TextView, Void, Void> {
        @Override
        protected Void doInBackground(TextView... params) {
            Log.d("MyAsyncTask", "before setText");
            params[0].setText("Boom!");
            Log.d("MyAsyncTask", "after setText");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        tv = new TextView(this);
        tv.setText("Hello world!");
        layout.addView(tv);
        Log.d("MainActivity", "before setContentView");
        setContentView(layout);
        Log.d("MainActivity", "after setContentView, before execute");
        new MyAsyncTask().execute(tv);
        Log.d("MainActivity", "after execute");
    }
}

Output:

10:01:33.126: D/MainActivity(18386): before setContentView
10:01:33.137: D/MainActivity(18386): after setContentView, before execute
10:01:33.148: D/MainActivity(18386): after execute
10:01:33.153: D/MyAsyncTask(18386): before setText
10:01:33.153: D/MyAsyncTask(18386): after setText

Everything as expected, nothing unusual here, setContentView() completed before execute() is called, which in turn completes before setText() is called from doInBackground(). So that's not it.

Update:

Another example:

public class MainActivity extends Activity {
    private LinearLayout layout;
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... params) {
            tv.setText("Boom!");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        layout = new LinearLayout(this);
        Button button = new Button(this);
        button.setText("Click!");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv = new TextView(MainActivity5.this);
                tv.setText("Hello world!");
                layout.addView(tv);
                new MyAsyncTask().execute();
            }
        });
        layout.addView(button);
        setContentView(layout);
    }
}

This time, I'm adding the TextView in the onClick() of the Button immediately before calling execute() on the AsyncTask. At this stage the initial Layout (without the TextView) has been displayed properly (i.e. I can see the button and click it). Again, no exception thrown.

And the counter example, if I add Thread.sleep(100); into the execute() before setText() in doInBackground() the usual exception is thrown.

One other thing I have just noticed now is, that just before the exception is thrown, the text of the TextView is actually updated and it displays properly, for just a split second, until the app closes automatically.

I guess something must be happening (asynchronously, i.e. detached from any lifecycle methods/callbacks) to my TextView that somehow "attaches" it to ViewRootImpl, which makes the latter throw the exception. Does anybody have an explanation or pointers to further documentation about what that "something" is?

Community
  • 1
  • 1
ci_
  • 8,594
  • 10
  • 39
  • 63
  • 1
    I suppose that the best way to clarify this would be checking the Android source code based on the stack trace you got. – Jorge Morales Apr 28 '15 at 12:07
  • [Debugging an AsyncTask](http://stackoverflow.com/questions/4770587/how-do-i-use-the-eclipse-debugger-in-an-asynctask-when-developing-for-android) should help you – VulfCompressor Apr 28 '15 at 12:09
  • 1
    share your async task code – amodkanthe Apr 28 '15 at 12:11
  • Why do you want to change the view of it in `doInBackground`? – SorryForMyEnglish Apr 28 '15 at 12:22
  • @apk it's there, right at the top. – ci_ Apr 28 '15 at 13:12
  • @SorryForMyEnglish I don't want to do it. I wanted to create an example that shows what happens when you do it. Then I wanted to simplify it and came across a scenario where what I wanted to show happens, didn't happen. – ci_ Apr 28 '15 at 13:13
  • exception will occur only when new task begins before ending previous one – amodkanthe Apr 28 '15 at 13:25
  • @apk Explain? There is only one AsyncTask, it's only executed once. – ci_ Apr 28 '15 at 13:28
  • @RodolfoPerottoni debugging the AsyncTask most likely won't help. Because of the timing involved, debugging it will always trigger the exception. However I want to know why I sometimes not get the exception. I suppose if it was a bug you could call it a [Heisenbug](http://en.wikipedia.org/wiki/Heisenbug). – ci_ Apr 28 '15 at 14:14
  • @JorgeMorales I hoped somebody would just know this offhand. Of course studying the source code can answer every question in the end. Studying the source based on the stack trace can give you insight into why you get an exception easily enough, but finding out why you didn't get it _sometimes_ means backtracking every possible code path to it and finding out where you branched differently. That's quite a task. – ci_ Apr 28 '15 at 14:22
  • @ci_ of course debuging will help you. Debug almost always help you. Have you even tried ? – VulfCompressor Apr 28 '15 at 14:43
  • @RodolfoPerottoni yes I have. Every time I debug, the exception gets triggered. Obviously, because it takes me longer than 60ms to react after the breakpoint is reached. Can you please explain how I am supposed to debug the case where the exception is _not_ thrown if debugging always triggers the exception case? It is notoriously difficult to debug timing related cases interactively. – ci_ Apr 28 '15 at 14:51
  • It's much better to use Log.d() statements to see the sequence of executed statements. – greenapps Apr 28 '15 at 17:47

3 Answers3

3

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 :)

RocketRandom
  • 1,102
  • 7
  • 20
  • Can you add some more background/details/pointers and specifically explain what "all the initial drawing traversals are complete" means? I have added another example to the questions as an update. – ci_ May 08 '15 at 14:27
  • In your updated example - the new text view is created and added in onClick - Hence the update happens before the "new" text view is rendered on the screen. – RocketRandom May 11 '15 at 07:51
  • I've upvoted your answer because it put me on the right track. I've done a bit more digging and came up with an answer that seems more comprehensive. Thanks a lot. – ci_ May 21 '15 at 11:44
3

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 Runnable ViewRootImpl 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.

ci_
  • 8,594
  • 10
  • 39
  • 63
  • 1
    In Android 8+ performLayout() and thus ViewRootImpl.checkThread() is *not* called always. For example you can see TextView.java line 8513: "Dynamic height, but height has stayed the same," that is isnt called from setText() unless the height changes – Jacob Nordfalk Nov 05 '18 at 13:26
  • @JacobNordfalk this makes sense. As I now tested some code, and it never threw exception in Android 8+, whereas in android 7 I suddenly got the exception. Was wondering why it didn't show up before. – Maverick Meerkat Feb 24 '19 at 14:41
0

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.

greenapps
  • 11,154
  • 2
  • 16
  • 19
  • But I _am_ executing the AsyncTask _after_ setContentView(layout). – ci_ Apr 28 '15 at 18:26
  • Have you determined the real sequence of statements with Log.d() ? It is not about when you execute the asynctask but when doInBackground() of the task is executed. – greenapps Apr 28 '15 at 18:35
  • Well, doInBackground() cannot possibly be executed _before_ execute() is called. But I will humour you and put in log statements when I get around to this again, not at the PC right now. – ci_ Apr 28 '15 at 18:50
  • It really feels good that i could bring you that far to execute a task. Now i rely on you doing it all in back ground ;-). – greenapps Apr 28 '15 at 18:57
  • I have added debug logging, everything as expected, that's not it. – ci_ Apr 30 '15 at 09:12
  • Well interesting indeed. I have no comments anymore. – greenapps Apr 30 '15 at 10:40