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

Posted

技术标签:

【中文标题】在 AsyncTask doInBackground() 中修改视图不会(总是)抛出异常【英文标题】:Modifying views in AsyncTask doInBackground() does not (always) throw exception 【发布时间】:2015-07-07 06:34:19 【问题描述】:

我只是在玩一些示例代码时遇到了一些意外行为。

“众所周知”,您不能从另一个线程修改 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);
    

如果您运行此程序并单击按钮,您的应用程序将按预期停止,您将在 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)

到目前为止一切顺利。

现在我将onCreate()改为立即执行AsyncTask,而不是等待按钮点击。

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    // same as above...
    new MyAsyncTask().execute(tv);

应用程序没有关闭,日志中没有任何内容,TextView 现在显示“Boom!”屏幕上。哇。没想到。

可能在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()

@Override
protected void onResume() 
    super.onResume();
    Handler handler = new Handler();
    handler.postDelayed(new Runnable() 
        @Override
        public void run() 
            new MyAsyncTask().execute(tv);
        
    , 1000);

终于!预期的异常:

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

哇,这和时间有关吗?

我尝试了不同的延迟,似乎对于这个特定的测试运行,在这个特定的设备(Nexus 4,运行 5.1)上,幻数是 60 毫秒,即有时会引发异常,有时它会更新 TextView,就好像什么都没发生。

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

我在这里Altering UI thread's Views in AsyncTask in doInBackground, CalledFromWrongThreadException not always thrown发现了一个类似的问题,但没有解释。

更新:

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

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

一切正常,这里没有什么异常,setContentView() 在调用 execute() 之前完成,而后者又在从 doInBackground() 调用 setText() 之前完成。所以不是这样的。

更新:

另一个例子:

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

这一次,我在ButtononClick() 中添加TextView,然后立即在AsyncTask 上调用execute()。在这个阶段,初始的Layout(没有TextView)已正确显示(即我可以看到按钮并单击它)。同样,没有抛出异常。

而反例,如果我在doInBackground() 中的setText() 之前将Thread.sleep(100); 添加到execute() 中,则会引发通常的异常。

我现在刚刚注意到的另一件事是,就在引发异常之前,TextView 的文本实际上已更新并正确显示,仅一瞬间,直到应用程序自动关闭。

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

【问题讨论】:

我认为澄清这一点的最佳方法是根据您获得的堆栈跟踪检查 Android 源代码。 Debugging an AsyncTask 应该能帮到你 分享你的异步任务代码 为什么要更改它在doInBackground中的视图? @apk 就在上面。 【参考方案1】:

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

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

不确定这是android中的错误还是故意的:)

【讨论】:

能否多加一些背景/细节/指针,具体解释一下“所有初始绘图遍历完成”是什么意思?我在问题中添加了另一个示例作为更新。 在您更新的示例中 - 在 onClick 中创建并添加了新的文本视图 - 因此更新发生在屏幕上呈现“新”文本视图之前。 我赞成您的回答,因为它让我走上了正确的道路。我做了更多的挖掘,并提出了一个似乎更全面的答案。非常感谢。【参考方案2】:

根据 RocketRandom 的回答,我进行了更多挖掘并提出了一个更全面的答案,我认为这里有必要。

负责最终异常的确实是ViewRootImpl.checkThread(),它在performLayout() 被调用时被调用。 performLayout() 在视图层次结构中向上传播,直到最终到达 ViewRootImpl,但它起源于 TextView.checkForRelayout(),由 setText() 调用。到目前为止,一切都很好。那么为什么调用setText()时有时不会抛出异常呢?

TextView.checkForRelayout() 仅在 TextView 已有 Layout (mLayout != null) 时调用。 (此检查是在这种情况下禁止抛出异常的原因,而不是 ViewRootImpl 中的 mHandlingLayoutInLayoutRequest。)

那么,为什么TextView 有时没有Layout?或者更好,因为显然它一开始就没有,它是从何时何地获得的?

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

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

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

Choreographer 最终将使用Handler 处理此问题并运行Runnable ViewRootImpl 在此处发布的任何内容,最终将调用performTraversals()measureHierarchy()performMeasure()(在ViewRootImpl ),它将执行进一步的一系列View.measure()onMeasure() 调用(以及其他一些调用),沿着View 层次结构向下移动,直到最终到达我们的TextView.onMeasure(),它调用makeNewLayout(),它调用@987654360 @,最终设置了我们的mLayout成员变量:

mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
            effectiveEllipsize, effectiveEllipsize == mEllipsize);

发生这种情况后,mLayout 不再为空,任何修改TextView 的尝试,即在我们的示例中调用setText(),都会导致众所周知的CalledFromWrongThreadException

所以我们这里有一个不错的小竞争条件,如果我们的AsyncTask 可以在Choreographer 遍历完成之前获得TextView,它可以修改它而不会受到惩罚。当然,这仍然是不好的做法,不应该这样做(还有许多其他 SO 帖子处理此问题),但 如果 这是意外或不知情的情况下,CalledFromWrongThreadException 不是完美的保护。

这个人为的示例使用TextView,其他视图的细节可能会有所不同,但一般原则保持不变。在任何情况下都不会调用requestLayout() 的其他一些View 实现(可能是自定义实现)是否可以在没有惩罚的情况下进行修改,这可能会导致更大的(隐藏的)问题,这还有待观察。

【讨论】:

在 Android 8+ performLayout() 中,因此总是调用 ViewRootImpl.checkThread()。例如,您可以看到 TextView.java 第 8513 行:“动态高度,但高度保持不变”,除非高度发生变化,否则不会从 setText() 调用 @JacobNordfalk 这是有道理的。因为我现在测试了一些代码,它在 Android 8+ 中从未抛出异常,而在 android 7 中我突然得到了异常。想知道为什么它之前没有出现。【参考方案3】:

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

它只是声明setContentView(layout);之后的GUI的一部分。

只是我的想法。

【讨论】:

但是我 am 执行 AsyncTask after setContentView(layout). 你用 Log.d() 确定了真正的语句顺序了吗?这不是关于何时执行 asynctask,而是何时执行任务的 doInBackground()。 好吧,doInBackground() 不可能被执行 before execute() 被调用。但是当我再次处理这个问题时,我会取笑你并输入日志语句,而不是现在在 PC 上。 能把你带到那么远的地方来执行任务真的感觉很好。现在我依靠你在后台做这一切;-)。 我添加了调试日志,一切如预期,不是这样。

以上是关于在 AsyncTask doInBackground() 中修改视图不会(总是)抛出异常的主要内容,如果未能解决你的问题,请参考以下文章

AsyncTask

AsyncTask

AsyncTask 替代背景图像下载? (AsyncTask ---------已弃用)

Android AsyncTask get()形成另一个AsyncTask()

Android面试Android异步任务AsyncTask

AsyncTask onCancelled(Object) 从未在 asyncTask.cancel(true) 之后调用;