【问题标题】:Android: CalledFromWrongThreadException thrown when broadcast intent is handledAndroid:处理广播意图时抛出 CalledFromWrongThreadException
【发布时间】:2012-07-12 21:52:46
【问题描述】:

这是我的应用程序的基本生命周期。它现在针对的是 SDK 版本 8,因为我的设备上仍在运行 Android 2.3.3。

  • 应用程序启动,onResume() 被调用
    调用show()方法显示缓存数据。
  • 后台服务启动,下载和存储数据。它使用AsyncTask 实例来完成其工作。
  • 其中一项任务将下载的数据存储在 SQLite 数据库中。
  • 当存储任务完成时,会在onPostExecute() 中发送广播意图。
  • MapActivity 接收意图并处理它。
    调用 show() 方法来显示缓存和新数据。

show() 方法中,地图视图在添加叠加层后无效。当从 MapActivity 本身调用 show() 时,这可以正常工作。但是,当异步任务是方法调用的来源(间接)时,它引发异常

据我了解,当我在这两种情况下触发 show() 时,我都处于 UI 线程。这是真的吗?

    public class CustomMapActivity extends MapChangeActivity {

        private boolean showIsActive = false;

        private BroadcastReceiver mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals(IntentActions.FINISHED_STORING)) {
                    onFinishedStoring(intent);
                }
            }
        };

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            registerReceiver(mReceiver, new IntentFilter(IntentActions.FINISHED_STORING));
        }

        @Override
        protected void onResume() {
            super.onResume();
            show();
        }

        @Override
        protected void onMapZoomPan() {
            loadData();
            show();
        }

        @Override
        protected void onMapPan() {
            loadData();
            show();
        }

        @Override
        protected void onMapZoom() {
            loadData();
            show();
        }

        private void onFinishedStoring(Intent intent) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                boolean success = extras.getBoolean(BundleKeys.STORING_STATE);
                if (success) {
                    show();
                }
        }

        private void loadData() {
            // Downloads data in a AsyncTask
            // Stores data in AsyncTask
        }

        private void show() {
            if (showIsActive) {
                return;
            }
            showIsActive = true;
            Uri uri = UriHelper.getUri();
            if (uri == null) {
                showIsActive = false;
                return;
            }
            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                List<Overlay> mapOverlays = mapView.getOverlays();
                CustomItemizedOverlay overlay = ItemizedOverlayFactory.getCustomizedOverlay(this, cursor);
                if (overlay != null) {
                    mapOverlays.clear();
                    mapOverlays.add(overlay);
                }
            }
            cursor.close();
            mapView.invalidate(); // throws CalledFromWrongThreadException
            showIsActive = false;
        }

    }

这是堆栈跟踪...

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRoot.checkThread(ViewRoot.java:3020)
    at android.view.ViewRoot.invalidateChild(ViewRoot.java:647)
    at android.view.ViewRoot.invalidateChildInParent(ViewRoot.java:673)
    at android.view.ViewGroup.invalidateChild(ViewGroup.java:2511)
    at android.view.View.invalidate(View.java:5332)
    at info.metadude.trees.activities.CustomMapActivity.showTrees(CustomMapActivity.java:278)
    at info.metadude.trees.activities.CustomMapActivity.onMapPan(CustomMapActivity.java:126)
    at info.metadude.trees.activities.MapChangeActivity$MapViewChangeListener.onChange(MapChangeActivity.java:50)
    at com.bricolsoftconsulting.mapchange.MyMapView$1.run(MyMapView.java:131)
    at java.util.Timer$TimerImpl.run(Timer.java:284)

注意:我使用MapChange 项目来接收有关地图事件的通知。


编辑:

从我现在在documentation about AsyncTask 中读到的内容(向下滚动一点),我不确定我是否以正确的方式使用它。如前所述,我从Service 类中启动AsyncTask 实例。相反,文档状态...

AsyncTask 允许您在用户界面上执行异步工作。它在工作线程中执行阻塞操作,然后在 UI 线程上发布结果,无需您自己处理线程和/或处理程序。

...听起来好像AsyncTask 只能在Activity 中使用,而不是在Service@ 中使用?!

【问题讨论】:

  • 您是正确的,99% 的时间 onReceive() 在主线程上被调用,但 1% 取决于它是如何注册的。你能显示代码的registerReceiver() 部分吗?
  • @Devunwired 我添加了onCreate() 方法来显示registerReceiver()
  • 我仍然有兴趣帮助您找出发生这种情况的原因。您可以从错误的线程异常中发布堆栈跟踪吗?这将有助于了解错误呼叫的来源。
  • @Devunwired 抱歉耽搁了。我添加了堆栈跟踪。
  • 因此请仔细查看该堆栈跟踪并注意此调用与您的BroadcastReceiver 无关。您的代码是从 Timer 执行的,它从该库内部执行各种线程上的任务。该库直接从后台线程调用侦听器,而不是像 Android 框架那样在主线程上发布回调。

标签: android broadcastreceiver invalidation mapactivity


【解决方案1】:

崩溃的原因是您使用的 MapChange 库的实现方式。在底层,这个库使用TimerTimerTask 实现来延迟触发更改事件并减少应用程序对onMapChanged() 的调用次数。但是,您可以从 Timer 上的文档中看到它在创建的线程中运行其任务:

每个计时器都有一个线程,任务在其上按顺序执行。当该线程忙于运行任务时,可运行的任务可能会出现延迟。

由于 MapChange 库没有确保回调在主线程上发布到您的应用程序(IMO 的一个严重错误,尤其是在 Android 上),因此您必须保护您调用的代码作为此侦听器的结果。您可以在与库捆绑的示例 MyMapActivity 中看到这一点,来自该回调的所有内容都通过 Handler 汇集,该 Handler 将为您将调用发回主线程。

在您的应用程序中,onMapPan() 和随后的showTrees() 中的代码在后台线程上被调用,因此在那里操作 UI 是不安全的。使用Activity 中的HandlerrunOnUiThread() 将保证您的代码在正确的位置被调用。

关于你关于AsyncTask 的第二个问题,没有什么能阻止你在任何应用程序组件中使用它,而不仅仅是Activity。尽管它是一个“后台”组件,但默认情况下 Service 仍在主线程上运行,因此 AsyncTask 仍然需要临时将长期处理卸载到另一个线程。

【讨论】:

  • 该死的。一开始我实际上使用了MyMapActivity 中给出的实现,但使用Handler 对我来说没有意义。您认为在 lib 摆脱计时器方面还有改进的余地吗?您知道接收此类地图更改事件的更好方法吗?谢谢你的精彩研究!!!总结一下:我的广播是完全安全的,并不是异常的原因,而是来自onMapPan()onMapZoom()、...的loadData()调用?放置runOnUiThread() 不会影响来自广播接收器的show() 调用,对吧?
  • 我打开了question 来寻找MapChange library 的替代解决方案。
  • 我会看看修改后的库。开发人员采纳了我的建议,结果看起来更清晰。
  • 再次感谢您!在我意识到您的改进建议后,我检查了它。效果很好!感谢您抽出宝贵时间,非常感谢!
【解决方案2】:

如果它在错误的线程上被调用,那么它很可能不在 UI 线程上。你有没有试过这个:

runOnUiThread(new Runnable() {
    public void run() {
        mapView.invalidate();
    }});

【讨论】:

  • 它可以工作,但是,我不明白它在哪个线程上运行。你能解释为什么这是必要的吗?我有设计错误吗?
  • 可能与您的异步任务有关。看看developer.android.com/reference/android/os/AsyncTask.html 看到后台线程计算的部分了吗?由于mapView是UI线程创建的视图,所以只能在UI线程上修改。如果修改它的调用是在后台线程上进行的,则会引发错误。这可能是不好的做法,但如果有可能在后台线程上调用编辑视图的方法,请调用 runOnUiThread 函数以确保它正在 UI 线程上运行。
  • 任务应该没有直接连接到活动。后台进程完成后,我发送广播意图。然后活动处理意图。
  • 我在这里发现了这个小花絮:developer.android.com/reference/android/content/… "使用 Intent 启动 Activity 是一个前台操作,它会修改用户当前正在与之交互的内容;广播 Intent 是用户正在进行的后台操作通常不知道。”如果我正确解释它,它会在后台运行。也许根据定义它是一个后台线程?我不太熟悉广播意图和异步任务,因此您可能需要进行更多研究以找到正确答案。
  • 无意冒犯!我撤销了“答复授权”,因为在我阅读了documentation on Threads 并觉得这里的runOnUiThread 更像是一种解决方法之后。这是我的错,因为我没有使用 ServiceAsyncTask 添加足够的信息。但是,规范明确指出 AsyncTask “在 UI 线程上发布结果,而不需要您自己处理线程和/或处理程序。” 如果我理解错误,请纠正我!我真的很想解决我的实现的基本问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-01
  • 1970-01-01
  • 2012-10-26
  • 1970-01-01
  • 1970-01-01
  • 2022-11-28
相关资源
最近更新 更多