在 AsyncTask doInBackground() 中修改视图不会(总是)抛出异常 [英] Modifying views in AsyncTask doInBackground() does not (always) throw exception

查看:15
本文介绍了在 AsyncTask doInBackground() 中修改视图不会(总是)抛出异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在玩一些示例代码时遇到了一些意想不到的行为.

I just came across some unexpected behaviour when playing around with some sample code.

正如每个人都知道",您不能从另一个线程修改 UI 元素,例如AsyncTaskdoInBackground().

As "everybody knows" you cannot modify UI elements from another thread, e.g. the doInBackground() of an AsyncTask.

例如:

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

如果您运行此程序并单击按钮,您的应用程序将按预期停止,您将在 logcat 中找到以下堆栈跟踪:

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):致命异常:AsyncTask #1
...
11:21:36.630:E/AndroidRuntime(23922):java.lang.RuntimeException:执行doInBackground()时出错
...
11:21:36.630:E/AndroidRuntime(23922):由:android.view.ViewRootImpl$CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触摸其视图.
11:21:36.630: E/AndroidRuntime(23922): 在 android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)

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)

到目前为止一切顺利.

现在我更改了 onCreate() 以立即执行 AsyncTask,而不是等待按钮点击.

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

应用程序没有关闭,日志中没有任何内容,TextView 现在显示Boom!"屏幕上.哇.没想到.

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

也许在 Activity 生命周期中为时过早?让我们将执行移至 onResume().

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

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

与上述行为相同.

好的,让我们把它贴在 Handler 上.

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

同样的行为.我的想法用完了,尝试 postDelayed() 延迟 1 秒:

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

终于!预期的异常:

11:21:36.630:E/AndroidRuntime(23922):由:android.view.ViewRootImpl$CalledFromWrongThreadException:只有创建视图层次结构的原始线程可以触摸其视图.

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.

哇,这和时间有关吗?

我尝试了不同的延迟,似乎对于这个特定的测试运行,在这个特定的设备(Nexus 4,运行 5.1)上,幻数是 60 毫秒,即有时会抛出异常,有时它会更新 TextView 好像什么都没发生过.

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.

我假设当视图层次结构在被 AsyncTask 修改时尚未完全创建时会发生这种情况.这个对吗?有没有更好的解释?Activity 是否有回调可用于确保已完全创建视图层次结构?与时间相关的问题很可怕.

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.

我在这里发现了一个类似的问题 Altering UI thread's Views indoInBackground 中的 AsyncTask,CalledFromWrongThreadException 不总是抛出但没有解释.

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

更新:

由于评论中的请求和建议的答案,我添加了一些调试日志来确定事件链...

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");
    }
}

输出:

10:01:33.126:D/MainActivity(18386):setContentView之前
10:01:33.137:D/MainActivity(18386):在setContentView之后,在执行之前
10:01:33.148:D/MainActivity(18386):执行后
10:01:33.153: D/MyAsyncTask(18386): 在 setText
之前10:01:33.153: D/MyAsyncTask(18386): 在 setText 之后

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

一切都符合预期,这里没有什么异常,setContentView()execute() 被调用之前完成,它又在 setText() 之前完成> 从 doInBackground() 调用.所以不是这样.

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.

更新:

另一个例子:

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

这一次,我在调用 execute() 之前在 ButtononClick() 中添加 TextViewAsyncTask 上.在此阶段,初始 Layout(没有 TextView)已正确显示(即我可以看到按钮并单击它).同样,没有抛出异常.

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.

还有反例,如果我在 setText() 之前将 Thread.sleep(100); 添加到 execute()code>doInBackground() 抛出通常的异常.

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

我现在刚刚注意到的另一件事是,就在抛出异常之前,TextView 的文本实际上已更新并正确显示,仅一瞬间,直到应用程序自动关闭.

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.

我猜我的 TextView 肯定发生了一些事情(异步,即与任何生命周期方法/回调分离),它以某种方式将它附加"到 ViewRootImpl,这使得后者抛出异常.有没有人对有关某物"是什么的进一步文档有解释或指示?

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?

推荐答案

基于 RocketRandom 的回答,我进行了更多的挖掘,并提出了更全面的答案,我觉得这里有必要.

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.

负责最终异常的确实是 ViewRootImpl.checkThread(),它在调用 performLayout() 时被调用.performLayout() 在视图层次结构中向上移动,直到最终到达 ViewRootImpl,但它起源于 TextView.checkForRelayout(),它被调用通过 setText().到现在为止还挺好.那么为什么当我们调用 setText() 时有时不会抛出异常?

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() 仅在 TextView 已经有 Layout (mLayout != null>).(这个检查是在这种情况下禁止抛出异常,而不是 ViewRootImpl 中的 mHandlingLayoutInLayoutRequest.)

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

那么,为什么TextView 有时没有Layout?或者更好,因为很明显它一开始没有,它是从何时何地获得的?

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?

TextView 最初使用 layout.addView(tv); 被添加到 LinearLayout 时,又是一串 requestLayout() 被调用,沿着 View 层次结构向上移动,最后到达 ViewRootImpl,这次没有抛出异常,因为我们还在用户界面线程.在这里,ViewRootImpl 然后调用 scheduleTraversals().

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

这里的重要部分是将回调/Runnable 发布到 Choreographer 消息队列,该队列异步"处理到主要执行流程:

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

Choreographer 最终将使用 Handler 处理它并运行 Runnable ViewRootImpl 在这里发布的任何内容,其中最终将调用 performTraversals()measureHierarchy()performMeasure()(在 ViewRootImpl 上),这将执行进一步的一系列 View.measure()onMeasure() 调用(以及其他一些),沿着 View 层次结构向下移动直到它最后到达我们的TextView.onMeasure(),它调用了makeNewLayout(),它调用了makeSingleLayout(),它最终设置了我们的mLayout 成员变量:

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

发生这种情况后,mLayout 不再为空,任何尝试修改 TextView,即调用 setText() 作为在我们的示例中,将导致众所周知的CalledFromWrongThreadException.

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.

所以我们这里有一个很好的小竞争条件,如果我们的 AsyncTask 可以在 Choreographer 遍历之前拿到 TextView是完整的,它可以修改它而不受惩罚.当然,这仍然是不好的做法,不应该这样做(还有许多其他 SO 帖子处理此问题),但是如果这是意外或不知不觉地完成的,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.

这个人为的例子使用了一个TextView,其他视图的细节可能会有所不同,但一般原则保持不变.其他一些View 实现(可能是自定义的)在每种情况下都不会调用requestLayout() 是否可以在没有惩罚的情况下进行修改,这还有待观察,这可能会导致到更大的(隐藏的)问题.

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.

这篇关于在 AsyncTask doInBackground() 中修改视图不会(总是)抛出异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆