在AsyncTask中修改视图doInBackground()不会(总是)抛出异常

时间:2015-04-28 12:00:58

标签: android android-asynctask

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

由于“每个人都知道”,您无法修改其他线程的UI元素,例如doInBackground()的{​​{1}}。

例如:

AsyncTask

如果你运行它,然后单击按钮,你的应用程序将按预期停止,你会在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)

到目前为止一切顺利。

现在我立即更改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); } } 以执行onCreate(),而不是等待按钮点击。

AsyncTask

应用程序未关闭,日志中没有任何内容,@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // same as above... new MyAsyncTask().execute(tv); } 现在显示“Boom!”屏幕上。哇。没想到。

TextView生命周期中可能还为时过早?让我们将执行移动到Activity

onResume()

与上述行为相同。

好的,让我们把它贴在@Override protected void onResume() { super.onResume(); new MyAsyncTask().execute(tv); } 上。

Handler

再次出现相同的行为。我的想法用完了,试了@Override protected void onResume() { super.onResume(); Handler handler = new Handler(); handler.post(new Runnable() { @Override public void run() { new MyAsyncTask().execute(tv); } }); } ,延迟时间为1秒:

postDelayed()

最后!预期的例外:

  

11:21:36.630:E / AndroidRuntime(23922):引起:android.view.ViewRootImpl $ CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触及其视图。

哇,这与时间有关吗?

我尝试了不同的延迟,看起来对于这个特定的测试运行,在这个特定的设备(Nexus 4,运行5.1)上,幻数是60ms,即有时会抛出异常,有时会更新@Override protected void onResume() { super.onResume(); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { new MyAsyncTask().execute(tv); } }, 1000); } 好像什么也没发生过。

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

我在Altering UI thread's Views in AsyncTask in doInBackground, CalledFromWrongThreadException not always thrown找到了类似的问题,但没有解释。

更新

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

Activity

输出:

  

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之后

所有内容都符合预期,此处没有任何异常,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"); } } 在调用setContentView()之前完成,而execute()setText()调用之前完成。所以那不是它。

更新

另一个例子:

doInBackground()

这一次,我在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() Button添加了execute()。在此阶段,初始AsyncTask(没有Layout)已正确显示(即我可以看到按钮并单击它)。再一次,没有抛出异常。

作为反例,如果我在TextView Thread.sleep(100);之前execute()添加setText(),则会抛出通常的异常。

我刚才注意到的另一件事是,在抛出异常之前,doInBackground()的文本实际上已更新,并且只需一瞬间显示,直到应用程序自动关闭。

我想必须发生一些事情(异步,即从任何生命周期方法/回调中分离)到我的TextView,它以某种方式将它“附加”到TextView,这使得后者抛出异常。有没有人对有关“某事”的进一步文档有解释或指示?

3 个答案:

答案 0 :(得分:3)

ViewRootImpl.java的checkThread()方法负责抛出此异常。 使用成员mHandlingLayoutInLayoutRequest抑制此检查,直到performLayout(),即所有初始绘图遍历完成。

因此只有在我们使用延迟时它才会抛出异常。

不确定这是否是android或故意的错误:)

答案 1 :(得分:3)

根据RocketRandom的回答,我已经做了一些挖掘,并提出了一个更全面的答案,我认为这是有道理的。

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

仅当TextView.checkForRelayout()已有TextViewLayout)时才会调用{p> mLayout != null。 (此检查禁止在这种情况下抛出异常,而不是mHandlingLayoutInLayoutRequest中的ViewRootImpl

那么,为什么TextView有时Layout?或者更好,因为很明显它开始没有一个,它何时何地从何而来?

TextView最初使用LinearLayout添加到layout.addView(tv);时,再次调用requestLayout()链,沿着View层次结构向上移动,结束于ViewRootImpl,这次,没有异常被抛出,因为我们仍然在UI线程上。在此,ViewRootImpl会调用scheduleTraversals()

这里的重要部分是,它会将回调/ Runnable发布到Choreographer消息队列中,这些消息队列会被处理并且异步地#34;到主要的执行流程:

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

Choreographer最终将使用Handler处理此问题并运行此处发布的Runnable ViewRootImpl,最终会调用performTraversals(),{{1和measureHierarchy()(在performMeasure()上),它将执行另一系列ViewRootImplView.measure()调用(以及其他一些调用),沿{{1}行进层次结构,直到它最终到达onMeasure(),调用View,调用TextView.onMeasure(),最终设置我们的makeNewLayout()成员变量:

makeSingleLayout()

发生这种情况后,mLayout不再为空,任何修改mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize, effectiveEllipsize, effectiveEllipsize == mEllipsize); 的尝试,即在我们的示例中调用mLayout,都会导致已知TextView

所以我们这里有一个很好的小竞争条件,如果我们的setText()可以在CalledFromWrongThreadException遍历完成之前获得AsyncTask,它可以修改它而不会受到处罚。当然这仍然是不好的做法,并且不应该做(有很多其他SO帖子处理这个),但如果这是偶然或不知情地完成的,TextView不是一个完美的保护。

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

答案 2 :(得分:0)

如果它还不是GUI的一部分,你可以在doInBackground中写入TextView。

它只是语句{{1}}之后的GUI的一部分。

只是我的想法。