Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating Views from non-UI threads

I am confused about how the Android system works, specially when it updates the view hierarchy. We all know that we should not update any view from any thread other than UI (Main) thread. And even Android system throws exception when we try to do so. The other day I was trying to implement a custom progress showing view in my application. So I started with using standard Java threads and handler combo.

What I found surprised me as I was able to update a TextView from background thread.

new Thread(new Runnable() {

        @Override
        public void run() {
            mTextView.setText("I am " + Thread.currentThread().getName());
        }
    }).start();

After which I tried updating other views also, which worked pretty well. So I tried putting a sleep call in background Thread.

new Thread(new Runnable() {

        @Override
        public void run() {
            mTextView.setText("Thread : before sleep");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mTextView.setText("Thread : after sleep");
        }
    }).start();

And it crashes as expected saying

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Then I tried putting the setText() calls in loop for 100, 1000 times before and after the sleep() call. Of course the app crashes every single time but I was able to see the "Before Sleep" text on my textview.

So my question is when does system detect that some non-UI thread is trying to update the view. And why does not it work when there is no sleep() call in non-UI thread ?

like image 369
apersiankite Avatar asked Dec 07 '15 11:12

apersiankite


1 Answers

I run your code snippet with sleep in Lollipop and it crashes. The stack trace is:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
        at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:909)
        at android.view.ViewGroup.invalidateChild(ViewGroup.java:4690)
        at android.view.View.invalidateInternal(View.java:11801)
        at android.view.View.invalidate(View.java:11765)
        at android.view.View.invalidate(View.java:11749)
        at android.widget.TextView.checkForRelayout(TextView.java:6850)
        at android.widget.TextView.setText(TextView.java:4057)
        at android.widget.TextView.setText(TextView.java:3915)
        at android.widget.TextView.setText(TextView.java:3890)
        at com.test.MainActivity$16.run(MainActivity.java:1126)
        at java.lang.Thread.run(Thread.java:818)

So the key hides around line 4057 of TextView.setText which is:

if (mLayout != null) {
    checkForRelayout();
}

We can see if the mLayout of the TextView is null, checkForRelayout() won't be called and thus the app will not crash. And the mLayout will be initialized in onDraw of TextView. So the app doesn't crash the first time setText is called because mLayout is null. After drawing, mLayout is initialized and cause the app to crash the second time setText is called.

I guess you start the Thread before the TextView is drawn (e.g. in onCreate or onResume). Right?

Whether the app crashes or not depends on the TIME you call setText. If you call setText before the TextView is first drawn, everything is ok. Otherwise the app crashes.

like image 195
shhp Avatar answered Nov 14 '22 23:11

shhp