在非UI线程更新视图

8
我对Android系统如何工作感到困惑,特别是当它更新视图层次结构时。我们都知道除了UI(主)线程之外,不应该从任何线程更新任何视图。甚至当我们尝试这样做时,Android系统会抛出异常。有一天,我试图在我的应用程序中实现自定义进度显示视图。所以我开始使用标准的Java线程和处理程序组合。
令我惊讶的是,我能够从后台线程更新TextView。
new Thread(new Runnable() {

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

随后我尝试更新其他视图,效果很不错。于是我尝试在后台线程中添加一个睡眠调用。

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();

我预期它会崩溃,并显示以下信息:

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

然后,我尝试在循环100次、1000次之前和之后调用setText()方法。当然,应用程序每次都会崩溃,但我能够看到我的textview上的"Before Sleep"文本。

所以我的问题是系统何时检测到某个非UI线程正在尝试更新视图,以及为什么在非UI线程中没有sleep()调用时不起作用


@Dogcat:我知道Handler。但我的问题是,为什么第一种情况下系统不会崩溃? - apersiankite
1
我的问题是为什么系统会这样表现,有时只会抛出异常,而不是每次都会抛出异常。Thread.sleep()如何改变系统的行为? - apersiankite
1
@pskink:实际上,我从不在非UI线程中触碰UI元素,事实上,我很少使用线程。我的一个同事(大学新生)做了这样的事情,所以我开始怀疑。顺便说一句,我的疑问出现在“你不能这样做”的那个点上,因为我能够做到。系统的行为并不一致。 - apersiankite
系统表现不一致,所以我只想知道原因。就这样,没什么大不了的。我不会从其他非UI线程更新视图。顺便说一句,我认为那个比喻不太合适。 - apersiankite
那么你想知道ViewRootImpl的细节吗?如果他们说:“不要从非UI线程触摸UI,否则会得到未知结果”,这难道不足够了吗?也许完整的检查会减慢系统速度,也许会使它变得更大?也许他们只是错过了一些需要检查的点?但这真的重要吗?如果您不打算从其他非UI线程更新视图,那就不重要了。 - pskink
显示剩余13条评论
2个回答

4

我在Lollipop上运行了你的代码片段,其中包含sleep,但它崩溃了。以下是堆栈跟踪:

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)

因此,关键点位于TextView.setText的第4057行附近。
if (mLayout != null) {
    checkForRelayout();
}

我们可以看到,如果TextView的mLayout是null,checkForRelayout()将不会被调用,因此应用程序不会崩溃。而mLayout将在TextView的onDraw中初始化。因此,当第一次调用setText时,应用程序不会崩溃,因为mLayout是null。绘图后,mLayout被初始化并导致第二次调用setText时应用程序崩溃。
我猜您在TextView绘制之前(例如在onCreate或onResume中)开始了线程,对吗?
应用程序是否崩溃取决于您何时调用setText。如果您在TextView首次绘制之前调用setText,那么一切都很顺利。否则,应用程序会崩溃。

你可以尝试在第一次调用 setText 之前添加 sleep。应用程序也会崩溃。 - shhp
Thread.sleep() 过程中,TextView 已经被绘制了,这也意味着 mLayout 已经初始化。 - shhp
@Dogcat,我尝试像你的代码一样将线程睡眠100或200毫秒,但我的应用程序仍然崩溃。 - apersiankite
@apersiankite 我可以在 Thread 和 TextView 类中添加一些调试代码,并在明天制作一个自定义的 Android 构建版本。我的意思是,如果你真的很想得到答案的话。 - Dogcat
@Dogcat: 哇,真想看到那个。提前谢谢啦。 - apersiankite
显示剩余2条评论

-1

线程是与UI线程并行的进程。当您尝试在线程中放置睡眠函数时,线程的执行会停止。您问题的答案就在问题本身中。它说-只有创建视图层次结构的原始线程才能触摸其视图。因此,还有两个线程运行一个UI线程和另一个您创建的线程。当您调用睡眠方法时,您的线程停止执行,没有与UI线程同步。当您的线程尝试更改TextView的文本时,两个线程不同步。在睡眠之前,线程是同步的。在睡眠后,它们不再同步。


3
首先,线程不是进程。暂时忘记sleep()调用,尝试在没有sleep的情况下运行代码,代码可以正常工作。这就是我怀疑的地方,为什么Android系统允许任何非UI线程更新视图。它不会抛出异常。顺便说一句,UI线程与任何其他线程之间都没有同步,无论你是否将其置于睡眠状态。 - apersiankite
3
你在开玩笑吗?我已经告诉过你(在你上一个回答中)我熟知编写Android代码时所需的所有多线程注意事项和相关模式。请仔细再读一遍我的问题。 - apersiankite

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接