Android: исключение CalledFromWrongThreadException возникает при обработке широковещательного намерения

Вот основной жизненный цикл моего приложения. На данный момент он нацелен на SDK версии 8, так как я все еще использую Android 2.3.3 на своем устройстве.

  • Приложение запускается, вызывается onResume()
    Вызывается метод show() для отображения кэшированных данных.
  • Запускается фоновая служба, которая загружает и сохраняет данные. Он использует AsyncTask экземпляров для выполнения своей работы.
  • Одна из задач сохраняет загруженные данные в базу данных SQLite.
  • Широковещательное намерение отправляется в onPostExecute(), когда задача сохранения завершена.
  • MapActivity получает намерение и обрабатывает его.
    Метод show() вызывается для отображения кэшированных и новых данных.

В методе show() представление карты становится недействительным после добавления наложения. Это прекрасно работает, когда show() вызывается из самой MapActivity. Однако он возбуждает исключение, когда асинхронная задача является источником вызова метода (косвенно).

Насколько я понимаю, я нахожусь в потоке пользовательского интерфейса, когда запускаю show() в обоих случаях. Это правда?

    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, чтобы получать уведомления о событиях на карте.


ИЗМЕНИТЬ:

Из того, что я сейчас прочитал в документации об AsyncTask ( прокрутите немного вниз), я не уверен, что использую его правильно. Как упоминалось ранее, я запускаю экземпляры AsyncTask из класса Service. Напротив, в документации говорится ...

AsyncTask позволяет выполнять асинхронную работу в пользовательском интерфейсе. Он выполняет блокирующие операции в рабочем потоке, а затем публикует результаты в потоке пользовательского интерфейса, не требуя от вас самостоятельной обработки потоков и/или обработчиков.

... что звучит так, как будто AsyncTask следует использовать только в Activity, а не в Service?!


person JJD    schedule 12.07.2012    source источник
comment
Вы правы в том, что 99% времени onReceive() вызывается в основном потоке, но этот 1% зависит от того, как он был зарегистрирован. Можете ли вы показать раздел registerReceiver() кода?   -  person devunwired    schedule 13.07.2012
comment
@Devunwired Я добавил метод onCreate(), чтобы показать registerReceiver().   -  person JJD    schedule 13.07.2012
comment
Я все еще заинтересован в том, чтобы помочь вам понять, почему это произошло. Можете ли вы опубликовать трассировку стека из неправильного исключения потока? Это поможет выяснить, откуда пришел ошибочный вызов.   -  person devunwired    schedule 13.07.2012
comment
@Devunwired Извините за задержку. Я добавил трассировку стека.   -  person JJD    schedule 14.07.2012
comment
Поэтому внимательно посмотрите на эту трассировку стека и обратите внимание, что этот вызов не имеет ничего общего с вашим BroadcastReceiver. Ваш код выполняется из Timer, который выполняет задачи в различных потоках из этой библиотеки. Эта библиотека вызывает слушателя непосредственно из фонового потока, а не отправляет обратные вызовы в основной поток, как это делает платформа Android.   -  person devunwired    schedule 14.07.2012
comment
@Devunwired Извините! Я признаю, что не дал достаточно подробностей, чтобы не усложнять вопрос. Я отредактировал вопрос и добавил информацию об услуге и задачах, которыми я пользуюсь. Надеюсь, это поможет.   -  person JJD    schedule 15.07.2012


Ответы (2)


Причина вашего сбоя связана с тем, как реализована библиотека MapChange, которую вы используете. Под капотом эта библиотека использует реализации Timer и TimerTask для задержки запуска события изменения и уменьшения количества вызовов, которые ваше приложение получает до onMapChanged(). Однако из документов на Timer видно, что он выполняет свои задачи в созданных потоках:

Каждый таймер имеет один поток, в котором задачи выполняются последовательно. Когда этот поток занят выполнением задачи, выполнение задач может быть задержано.

Поскольку библиотека MapChange ничего не делает для того, чтобы гарантировать, что обратные вызовы отправляются в ваше приложение в основном потоке (серьезная ошибка IMO, особенно на Android), вы должны защитить код, который вы вызываете в результате этого прослушивателя. Вы можете видеть это в примере MyMapActivity, связанном с библиотекой, все из этого обратного вызова направляется через Handler, который отправляет вызовы обратно в основной поток для вас.

В вашем приложении код внутри onMapPan(), а затем showTrees() вызывается в фоновом потоке, поэтому манипулировать пользовательским интерфейсом там небезопасно. Использование Handler или runOnUiThread() из Activity гарантирует, что ваш код вызывается в нужном месте.

Что касается вашего второго вопроса о AsyncTask, ничто не мешает вам использовать его внутри любого компонента приложения, а не только Activity. Несмотря на то, что это «фоновый» компонент, по умолчанию Service все еще работает в основном потоке, поэтому AsyncTask по-прежнему необходим для временной передачи долговременной обработки другому потоку.

person devunwired    schedule 16.07.2012
comment
Черт. Вначале я действительно использовал реализацию, указанную в MyMapActivity, но для меня не имело смысла использовать Handler. Видите ли вы возможности для улучшения библиотеки, избавившись от таймеров? Знаете ли вы лучший способ получать события смены карты такого рода? Спасибо за прекрасное исследование!!! Подводя итог: мои трансляции полностью безопасны и не были причиной исключения, но loadData() вызов изнутри onMapPan(), onMapZoom(), ...? Установка runOnUiThread() не влияет на вызов show(), исходящий от широковещательного приемника, верно? - person JJD; 17.07.2012
comment
Я открыл вопрос, чтобы найти альтернативные решения для библиотека MapChange. - person JJD; 17.07.2012
comment
Я бы взглянул на исправленную библиотеку. Разработчик учел мое предложение, и результат выглядит намного чище. - person devunwired; 17.07.2012
comment
Еще раз спасибо! Я проверил это после того, как понял ваше предложение по улучшению. Это прекрасно работает! Спасибо за потраченное время, мне очень приятно! - person JJD; 18.07.2012

Если он вызывается не в том потоке, то, скорее всего, он не в потоке пользовательского интерфейса. Вы пробовали это:

runOnUiThread(new Runnable() {
    public void run() {
        mapView.invalidate();
    }});
person skUDA    schedule 12.07.2012
comment
Работает, однако я не понимаю, на каком потоке он работает тогда. Не могли бы вы объяснить, зачем это нужно? У меня ошибка проектирования? - person JJD; 13.07.2012
comment
Вероятно, что-то связанное с вашими асинхронными задачами. Взгляните на developer.android.com/reference/android/os/AsyncTask. html См. часть о вычислении фонового потока? Поскольку mapView — это представление, созданное потоком пользовательского интерфейса, его можно изменить только в потоке пользовательского интерфейса. Если вызов для его изменения сделан в фоновом потоке, он выдает ошибку. Вероятно, это плохая практика, но если есть шанс, что метод, редактирующий представление, может быть вызван в фоновом потоке, вызовите функцию runOnUiThread, чтобы убедиться, что он выполняется в потоке пользовательского интерфейса. - person skUDA; 13.07.2012
comment
Задача не должна иметь прямого отношения к деятельности. Я отправляю широковещательное намерение, когда фоновый процесс завершен. Затем активность обрабатывает намерение. - person JJD; 13.07.2012
comment
Я нашел этот небольшой кусочек здесь: developer.android.com/reference/android/content / запуск действия с намерением — это операция переднего плана, которая изменяет то, с чем в данный момент взаимодействует пользователь; широковещательная передача Intent — это фоновая операция, о которой пользователь обычно не знает. Если я правильно понимаю, он работает в фоновом режиме. Возможно, это по определению фоновый поток? Я не очень хорошо разбираюсь в широковещательных намерениях и асинхронных задачах, поэтому вы, вероятно, захотите провести больше исследований, чтобы найти правильный ответ. - person skUDA; 13.07.2012
comment
Не обижайся! Я отозвал разрешение на ответ, поскольку после прочтения документации по потокам и чувствую, что runOnUiThread здесь скорее обходной путь. Это моя вина, так как я не добавил достаточно информации, используя Service и AsyncTask. Однако в спецификации четко указано, что AsyncTask публикует результаты в потоке пользовательского интерфейса, не требуя от вас самостоятельной обработки потоков и/или обработчиков. Пожалуйста, поправьте меня, если я ошибаюсь! Я действительно хотел бы разобраться с базовой проблемой моей реализации. - person JJD; 15.07.2012
comment
Ни один не взят. Это обходной путь, а не полное решение вашей проблемы. Я не могу помочь вам с фактическим корнем вашей проблемы, так как я действительно мало знаю об асинхронных задачах и не использую их. Похоже, вы получаете хорошую помощь от Devunwired, поэтому я желаю вам удачи и надеюсь, что вы во всем разберетесь. - person skUDA; 16.07.2012