在AsyncTask的doInBackground修改意见()不(总是)抛出异常 [英] Modifying views in AsyncTask doInBackground() does not (always) throw exception

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

问题描述

我只是碰到一些意外行为的一些示例code玩耍的时候就来了。

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

由于每个人都知道你不能从另一个线程,例如修改用户界面元素在 doInBackground()的AsyncTask

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 现在显示轰!屏幕上。哇。没想到这一点。

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

活动生命周期也许太早了?让我们将执行到 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);
}

与上述相同的行为。

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

同样的行为了。我跑出来的想法,并尝试 postDelayed()以1秒的延迟:

@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):由: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.

哇,这是时间的选择有关?

Wow, this is timing related?

我尝试不同的延迟,看来,对于这个特殊的测试来看,这种特定的设备(的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 修改的点创建。它是否正确?是否有一个更好的解释吗?是否有关于活动的回调,可以用来确保视图层次结构已经完全产生的?时序相关的问题是可怕的。

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.

我发现了一个类似的问题在这里<一href="http://stackoverflow.com/questions/13882786/altering-ui-threads-views-in-asynctask-in-doinbackground-calledfromwrongthread">Altering UI线程中的AsyncTask在doInBackground意见,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);
    }
}

这时候,我加入了的TextView 的onClick()按钮立即调用之前执行()的AsyncTask 。在这个阶段的初始布局(不包括的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.

而反例,如果我添加视频下载(100); 执行()的setText() 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的答案,我已经做了一些更多的挖掘,并想出了一个更加融为一体prehensive的答案,我觉得这里是必要的。

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 已经有一个布局 mLayout!= NULL )。 (此检查是抑制异常被抛出在这种情况下,不是 mHandlingLayoutInLayoutRequest ViewRootImpl

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 有时的没有的有布局?或者更好的,因为显然它开始没有的,何时何地做它得到它?

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 最初加入的LinearLayout 使用 layout.addView(电视); ,再次链 requestLayout的()被调用,行驶了查看层次结构,在 ViewRootImpl ,在这个时候,没有异常被抛出,因为我们仍然在UI线程上结束了。在这里, 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 编导消息队列,这是处理异步来执行的主要流程:

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

编导将最终处理此使用处理程序并运行任何的Runnable ViewRootImpl 在这里发表,这最终将调用 performTraversals() measureHierarchy() performMeasure()(在 ViewRootImpl ),这将执行另一系列 View.measure() onMeasure()电话(和其他几个人),行驶下来查看分层结构,直到最后达到我们的 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 不为null,更多的,任何试图修改的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 可以得到它的手放在的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 和细节可能会有所不同的其他意见,但总的原则是相同的。这还有待观察,如果其他查看实现(可能是一个自定义的),不叫 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天全站免登陆