我在玩一些示例代码时遇到了一些意想不到的行为。
由于“每个人都知道”,您无法修改其他线程的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
,这使得后者抛出异常。有没有人对有关“某事”的进一步文档有解释或指示?
答案 0 :(得分:3)
ViewRootImpl.java的checkThread()方法负责抛出此异常。 使用成员mHandlingLayoutInLayoutRequest抑制此检查,直到performLayout(),即所有初始绘图遍历完成。
因此只有在我们使用延迟时它才会抛出异常。
不确定这是否是android或故意的错误:)
答案 1 :(得分:3)
根据RocketRandom的回答,我已经做了一些挖掘,并提出了一个更全面的答案,我认为这是有道理的。
负责最终异常的确是ViewRootImpl.checkThread()
,在调用performLayout()
时调用。 performLayout()
向上移动视图层次结构,直到它最终到达ViewRootImpl
,但它始于TextView.checkForRelayout()
,由setText()
调用。到现在为止还挺好。那么为什么当我们调用setText()
时,有时候不会抛出异常?
TextView.checkForRelayout()
已有TextView
(Layout
)时才会调用{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()
上),它将执行另一系列ViewRootImpl
,View.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的一部分。
只是我的想法。