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

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)

这一次,我加入了TextViewonClick()中的Button调用之前立即execute()AsyncTask.在这个阶段,初始Layout(没有TextView)已正确显示(即我可以看到按钮并单击它).再一次,没有抛出异常.

和计数器的例子,如果我在通常的异常中添加Thread.sleep(100);execute()之前被抛出.setText()doInBackground()

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

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

ci_*_*ci_ 4

根据 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()

这里重要的部分是,这会将回调 / 发布到消息队列RunnableChoreographer,该消息队列与主执行流程“异步”处理:

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()不会受到惩罚,这还有待观察,这可能会导致更大的(隐藏的)问题。