ci_*_*ci_ 11 android android-asynctask
我在玩一些示例代码时遇到了一些意想不到的行为.
由于"大家都知道"你不能修改从另一个线程的UI元素,例如在doInBackground()的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);
}
}
Run Code Online (Sandbox Code Playgroud)
如果你运行它,然后单击按钮,你的应用程序将按预期停止,你会在logcat中找到以下堆栈跟踪:
11:21:36.630:E/AndroidRuntime(23922):FATAL EXCEPTION: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)
到现在为止还挺好.
现在我改变了立即onCreate()执行AsyncTask,而不是等待按钮单击.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// same as above...
new MyAsyncTask().execute(tv);
}
Run Code Online (Sandbox Code Playgroud)
应用程序没有关闭,日志中没有任何内容,TextView现在显示"Boom!" 屏幕上.哇.没想到.
可能在Activity生命周期中过早?让我们将执行移动到onResume().
@Override
protected void onResume() {
super.onResume();
new MyAsyncTask().execute(tv);
}
Run Code Online (Sandbox Code Playgroud)
与上述行为相同.
好吧,让我们坚持下去吧Handler.
@Override
protected void onResume() {
super.onResume();
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
new MyAsyncTask().execute(tv);
}
});
}
Run Code Online (Sandbox Code Playgroud)
同样的行为.我的想法已经用完了,尝试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);
}
Run Code Online (Sandbox Code Playgroud)
最后!预期的例外情况:
11:21:36.630:E/AndroidRuntime(23922):引起:android.view.ViewRootImpl $ CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触及其视图.
哇,这与时间有关吗?
我尝试了不同的延迟,看起来对于这个特定的测试运行,在这个特定的设备(Nexus 4,运行5.1)上,幻数是60ms,即有时会引发异常,有时它会更新TextView,好像什么也没发生过.
我假设当视图层次结构尚未完全创建时,会发生这种情况AsyncTask.它是否正确?对它有更好的解释吗?是否有回调Activity可用于确保视图层次已完全创建?时间相关的问题是可怕的.
我在这里找到了一个类似的问题在doInBackground中改变了AsyncTask中的UI线程的视图,并不总是抛出CalledFromWrongThreadException,但是没有解释.
更新:
由于评论中的请求和建议的答案,我添加了一些调试日志以确定事件链...
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");
}
}
Run Code Online (Sandbox Code Playgroud)
输出:
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之后
一切都如预期的那样,在此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);
}
}
Run Code Online (Sandbox Code Playgroud)
这一次,我加入了TextView在onClick()中的Button调用之前立即execute()上AsyncTask.在这个阶段,初始Layout(没有TextView)已正确显示(即我可以看到按钮并单击它).再一次,没有抛出异常.
和计数器的例子,如果我在通常的异常中添加Thread.sleep(100);到execute()之前被抛出.setText()doInBackground()
我刚刚注意到的另一件事是,在抛出异常之前,TextView实际更新了文本,并且只需一瞬间显示,直到应用程序自动关闭.
我想必须发生一些事情(异步,即从任何生命周期方法/回调中分离)到我的TextView某种"附加"它ViewRootImpl,这使得后者抛出异常.有没有人对有关"某事"的内容的进一步文档有解释或指示?
根据 RocketRandom 的回答,我做了一些更多的挖掘,并提出了一个更全面的答案,我认为这是有道理的。
负责最终异常的确实是调用ViewRootImpl.checkThread()时调用的。沿着视图层次结构向上移动,直到最终到达,但它起源于,由 调用。到目前为止,一切都很好。那么为什么我们调用时有时不会抛出异常呢?performLayout()performLayout()ViewRootImplTextView.checkForRelayout()setText()setText()
TextView.checkForRelayout()仅当TextView已经有Layout( mLayout != null) 时才被调用。(此检查是阻止在本例中引发异常的原因,而不是mHandlingLayoutInLayoutRequest在. 中ViewRootImpl。)
那么,为什么TextView有时没有?Layout或者更好,因为显然它一开始就没有,那么它是何时何地从哪里得到的呢?
当TextView最初添加到LinearLayoutusing时layout.addView(tv);,再次调用一系列requestLayout(),沿着层次结构向上移动View,最终到达ViewRootImpl,这一次,没有抛出异常,因为我们仍在 UI 线程上。在这里,ViewRootImpl然后调用scheduleTraversals()。
这里重要的部分是,这会将回调 / 发布到消息队列Runnable上Choreographer,该消息队列与主执行流程“异步”处理:
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
Run Code Online (Sandbox Code Playgroud)
最终将Choreographer使用 a 处理此问题Handler并运行Runnable ViewRootImpl此处发布的任何内容,这最终将调用performTraversals(),measureHierarchy()和performMeasure()(on ViewRootImpl),它将执行进一步的一系列View.measure(),onMeasure()调用(以及其他一些),沿着View层次结构向下移动,直到它最终到达我们的TextView.onMeasure(),它调用makeNewLayout(),它调用makeSingleLayout(),它最终设置我们的mLayout成员变量:
mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
Run Code Online (Sandbox Code Playgroud)
发生这种情况后,mLayout不再为 null,并且任何修改 的尝试TextView,即setText()像我们的示例中那样调用,都将导致众所周知的CalledFromWrongThreadException.
所以我们这里有一个很好的小竞争条件,如果我们可以在遍历完成之前AsyncTask得到它,它可以修改它而不会受到惩罚。当然,这仍然是不好的做法,不应该这样做(还有许多其他 SO 帖子处理此问题),但如果这是意外或不知情地完成的,那么这并不是一个完美的保护。TextViewChoreographerCalledFromWrongThreadException
这个人为的示例使用了 a TextView,其他视图的细节可能有所不同,但总体原理保持不变。是否可以修改View并非在每种情况下都调用的其他实现(可能是自定义实现)而requestLayout()不会受到惩罚,这还有待观察,这可能会导致更大的(隐藏的)问题。
| 归档时间: |
|
| 查看次数: |
816 次 |
| 最近记录: |