Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.2k views
in Technique[技术] by (71.8m points)

android - 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 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...