onPostExecute не вызывается в AsyncTask (исключение среды выполнения обработчика)


У меня есть AsyncTask, который извлекает некоторые данные, а затем обновляет пользовательский интерфейс с этими новыми данными. Он работал нормально в течение нескольких месяцев, но недавно я добавил функцию, которая отображает уведомление, когда появляются новые данные. Теперь, когда мое приложение запускается через уведомление, иногда я получаю это исключение и onPostExecute не вызывается.

Вот что происходит при запуске приложения:

1) разверните пользовательский интерфейс и найдите представления

2) отменить сигнал тревоги (через AlarmManager), который проверяет наличие новых данные и сброс сигнализации. (Это так, что если пользователь отключает сигнал тревоги, он отменяется до следующей перезагрузки.)

3) Начните AsyncTask. Если приложение было запущено из уведомления, передайте немного данных, а затем отмените уведомление.

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

Спасибо!

Вот исключение:

I/My App(  501): doInBackground exiting
W/MessageQueue(  501): Handler{442ba140} sending message to a Handler on a dead thread
W/MessageQueue(  501): java.lang.RuntimeException: Handler{442ba140} sending message to a Handler on a dead thread
W/MessageQueue(  501):  at android.os.MessageQueue.enqueueMessage(MessageQueue.java:179)
W/MessageQueue(  501):  at android.os.Handler.sendMessageAtTime(Handler.java:457)
W/MessageQueue(  501):  at android.os.Handler.sendMessageDelayed(Handler.java:430)
W/MessageQueue(  501):  at android.os.Handler.sendMessage(Handler.java:367)
W/MessageQueue(  501):  at android.os.Message.sendToTarget(Message.java:348)
W/MessageQueue(  501):  at android.os.AsyncTask$3.done(AsyncTask.java:214)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask$Sync.innerSet(FutureTask.java:252)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask.set(FutureTask.java:112)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:310)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask.run(FutureTask.java:137)
W/MessageQueue(  501):  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
W/MessageQueue(  501):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
W/MessageQueue(  501):  at java.lang.Thread.run(Thread.java:1096)

EDIT: вот мой метод onCreate в моей основной деятельности (тот, который открывается уведомлением). Есть некоторые onClickListeners, которые я опустил, чтобы сэкономить место. Я не думаю, что они должны иметь какой-либо эффект, так как кнопки, к которым они прикреплены, не нажимаются.

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // Call the parent

    setContentView(R.layout.main); // Create the UI from the XML file

    // Find the UI elements
    controls = (SlidingDrawer) findViewById(R.id.drawer); // Contains the
    // buttons
    // comic = (ImageView) findViewById(R.id.comic); // Displays the comic
    subtitle = (TextView) findViewById(R.id.subtitleTxt); // Textbox for the
    // subtitle
    prevBtn = (Button) findViewById(R.id.prevBtn); // The previous button
    nextBtn = (Button) findViewById(R.id.nextBtn); // The next button
    randomBtn = (Button) findViewById(R.id.randomBtn); // The random button
    fetchBtn = (Button) findViewById(R.id.comicFetchBtn); // The go to specific id button
    mostRecentBtn = (Button) findViewById(R.id.mostRecentBtn); // The button to go to the most recent comic
    comicNumberEdtTxt = (EditText) findViewById(R.id.comicNumberEdtTxt); // The text box to Zooming image view setup
    zoomControl = new DynamicZoomControl();

    zoomListener = new LongPressZoomListener(this);
    zoomListener.setZoomControl(zoomControl);

    zoomComic = (ImageZoomView) findViewById(R.id.zoomComic);
    zoomComic.setZoomState(zoomControl.getZoomState());
    zoomComic.setImage(BitmapFactory.decodeResource(getResources(), R.drawable.defaultlogo));
    zoomComic.setOnTouchListener(zoomListener);

    zoomControl.setAspectQuotient(zoomComic.getAspectQuotient());

    resetZoomState();

    // enter the new id
    imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // Used to hide the soft keyboard

    Log.i(LOG_TAG, "beginning loading of first comic");
    int notificationComicNumber = getIntent().getIntExtra("comic", -1);
    Log.i(LOG_TAG, "comic number from intent: " + notificationComicNumber);
    if (notificationComicNumber == -1) {
        fetch = new MyFetcher(this, zoomComic, subtitle, controls, comicNumberEdtTxt, imm, zoomControl);
        fetch.execute(MyFetcher.LAST_DISPLAYED_COMIC);
    } else {
        fetch = new MyFetcher(this, zoomComic, subtitle, controls, comicNumberEdtTxt, imm, zoomControl);
        fetch.execute(notificationComicNumber);
        ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
    }
    Log.i(LOG_TAG, "ending loading of new comic");

    Log.i(LOG_TAG, "first run checks beginning");
    // Get SharedPreferences
    prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE);

    // Check if this is the first run of the app for this version
    if (prefs.getBoolean("firstRun-" + MAJOR_VERSION_NUMBER, true)) {
        prefs.edit().putBoolean("firstRun-" + MAJOR_VERSION_NUMBER, false).commit();
        firstRunVersionDialog();
    }

    // Check if this is the first run of the app
    if (prefs.getBoolean("firstRun", true)) {
        prefs.edit().putBoolean("firstRun", false).commit();
        firstRunDialog();
    }
    Log.i(LOG_TAG, "First run checks done");

            // OnClickListener s for the buttons omitted to save space

EDIT 2: я копался в исходном коде Android, отслеживая, откуда берется исключение. Это строки 456 и 457 из sendMessageAtTime в Handler:

msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);

А это enqueueMessage из MessageQueue:

    final boolean enqueueMessage(Message msg, long when) {
        if (msg.when != 0) {
            throw new AndroidRuntimeException(msg
                    + " This message is already in use.");
        }
        if (msg.target == null && !mQuitAllowed) {
            throw new RuntimeException("Main thread not allowed to quit");
        }
        synchronized (this) {
            if (mQuiting) {
                RuntimeException e = new RuntimeException(
                    msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                return false;
            } else if (msg.target == null) {
                mQuiting = true;
            }

            msg.when = when;
            //Log.d("MessageQueue", "Enqueing: " + msg);
            Message p = mMessages;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                this.notify();
            } else {
                Message prev = null;
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
                msg.next = prev.next;
                prev.next = msg;
                this.notify();
            }
        }
        return true;
    }
Я немного запутался в том, что такое mQuiting, но похоже, что предыдущий раз, когда enqueueMessage назывался msg.target, был нулевым.
6 17

6 ответов:

Чтобы обобщить решение Джонатана Перлоу для ошибки, которую он определил специально, я использую следующее В любом классе, который использует AsyncTask. Looper / handler / post-это способ запуска чего-либо в потоке пользовательского интерфейса в любом месте приложения Android без передачи дескриптора в действие или другой контекст. Добавьте этот статический блок инициализации внутри класса:

{ // https://stackoverflow.com/questions/4280330/onpostexecute-not-being-called-in-asynctask-handler-runtime-exception
    Looper looper = Looper.getMainLooper();
    Handler handler = new Handler(looper);
    handler.post(new Runnable() {
      public void run() {
        try {
          Class.forName("android.os.AsyncTask");
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
      }
    });
}

Мы столкнулись с этой проблемой, когда пытались запустить модульные тесты. Я нашел обходной путь для этого, но не специально определить проблему. Мы только знали, что попытка использовать AsyncTask в тесте Android JUnit привела к тому, что onPostExecute() не вызывается. Теперь мы знаем почему.

Этот пост показывает, как запустить многопоточный асинхронный код в тесте Android JUnit:

Использование CountDownLatch в тестах JUnit на базе Android AsyncTask

Для использования с unit-тестами без пользовательского интерфейса я создал простой подкласс android.тест.InstrumentationTestCase. У него есть флаг " ОК " и обратный отсчет. сбросить или reset (count) создает новый CountDownLatch ({1,count}). good() устанавливает ok=true, count--, и вызывает.обратный отсчет () на защелке. bad() устанавливает ok=false и ведет обратный отсчет до конца. waitForIt (секунды) ожидает таймаута или фиксации обратного отсчета до нуля. Затем он вызывает assertTrue (ok).

Тогда тесты похожи:

someTest() {
  reset();
  asyncCall(args, new someListener() {
    public void success(args) { good(); }
    public void fail(args) { bad(); }
  });
  waitForIt();
}

Из-за ошибки статической инициализации AsyncTask нам пришлось запускать наши фактические тесты внутри Runnable, передаваемого в runTestOnUiThread(). С правильной статической инициализацией, как указано выше, это не должно быть необходимо, если только тестируемый вызов не должен выполняться в потоке пользовательского интерфейса.

Другая идиома, которую я теперь использую, заключается в том, чтобы проверить, является ли текущий поток потоком пользовательского интерфейса, а затем выполнить требуемое действие в соответствующем потоке независимо. Иногда имеет смысл разрешить вызывающему абоненту запрашивать синхронизацию против асинхронности, переопределяя при необходимости. Например, сетевые запросы всегда должны выполняться в фоновом потоке. В большинстве случаев пул потоков AsyncTask идеально подходит для этого. Просто поймите, что только определенное число будет работать сразу, блокируя дополнительные запросы. Чтобы проверить, является ли текущий поток потоком пользовательского интерфейса:

boolean onUiThread = Looper.getMainLooper().getThread() == Thread.currentThread();

Затем используйте простой подкласс (просто doInBackground () и onPostExecute () необходимы) AsyncTask для запуска в потоке или обработчике, отличном от UI.post () или postDelayed () для запуска в потоке пользовательского интерфейса.

Предоставление вызывающему параметру возможности запуска sync или async выглядит следующим образом (получение локально допустимого значения onUiThread не показано здесь; добавьте локальные логические значения как выше):

void method(final args, sync, listener, callbakOnUi) {
  Runnable run = new Runnable() { public void run() {
    // method's code... using args or class members.
    if (listener != null) listener(results);
    // Or, if the calling code expects listener to run on the UI thread:
    if (callbackOnUi && !onUiThread)
      handler.post(new Runnable() { public void run() {listener()}});
    else listener();
  };
  if (sync) run.run(); else new MyAsync().execute(run);
  // Or for networking code:
  if (sync && !onUiThread) run.run(); else new MyAsync().execute(run);
  // Or, for something that has to be run on the UI thread:
  if (sync && onUiThread) run.run() else handler.post(run);
}

Кроме того, использование AsyncTask может быть сделано очень простым и кратким. Используйте определение RunAsyncTask.java ниже, затем напишите код следующим образом:

    RunAsyncTask rat = new RunAsyncTask("");
    rat.execute(new Runnable() { public void run() {
        doSomethingInBackground();
        post(new Runnable() { public void run() { somethingOnUIThread(); }});
        postDelayed(new Runnable() { public void run() { somethingOnUIThreadInABit(); }}, 100);
    }});

Или просто: new RunAsyncTask ("").выполнить(новый Runnable(){общественная пустота запустить(){ doSomethingInBackground(); }});

RunAsyncTask.java:

package st.sdw;
import android.os.AsyncTask;
import android.util.Log;
import android.os.Debug;

public class RunAsyncTask extends AsyncTask<Runnable, String, Long> {
    String TAG = "RunAsyncTask";
    Object context = null;
    boolean isDebug = false;
    public RunAsyncTask(Object context, String tag, boolean debug) {
      this.context = context;
      TAG = tag;
      isDebug = debug;
    }
    protected Long doInBackground(Runnable... runs) {
      Long result = 0L;
      long start = System.currentTimeMillis();
      for (Runnable run : runs) {
        run.run();
      }
      return System.currentTimeMillis() - start;
    }
    protected void onProgressUpdate(String... values) {        }
    protected void onPostExecute(Long time) {
      if (isDebug && time > 1) Log.d(TAG, "RunAsyncTask ran in:" + time + " ms");
      v = null;
    }
    protected void onPreExecute() {        }
    /** Walk heap, reliably triggering crash on native heap corruption.  Call as needed. */  
    public static void memoryProbe() {
      System.gc();
      Runtime runtime = Runtime.getRuntime();
      Double allocated = new Double(Debug.getNativeHeapAllocatedSize()) / 1048576.0;
      Double available = new Double(Debug.getNativeHeapSize()) / 1048576.0;
      Double free = new Double(Debug.getNativeHeapFreeSize()) / 1048576.0;
      long maxMemory = runtime.maxMemory();
      long totalMemory = runtime.totalMemory();
      long freeMemory = runtime.freeMemory();
     }
 }

Это связано с ошибкой в AsyncTask в рамках Android. AsyncTask.java имеет следующий код:

private static final InternalHandler sHandler = new InternalHandler();

Он ожидает, что это будет инициализировано в главном потоке, но это не гарантируется, так как он будет инициализирован в любом потоке, который вызовет запуск статических инициализаторов класса. Я воспроизвел эту проблему, где обработчик ссылается на рабочий поток.

Общим шаблоном, который приводит к этому, является использование класса IntentService. Пример кода C2DM делает этот.

Простой обходной путь заключается в добавлении следующего кода к методу onCreate приложения:

Class.forName("android.os.AsyncTask");

Это заставит AsyncTask инициализироваться в главном потоке. Я зарегистрировал ошибку по этому поводу в базе данных ошибок android. См. http://code.google.com/p/android/issues/detail?id=20915 .

У меня была такая же проблема на устройстве с Android 4.0.4 с IntentService и решил ее, как sdw сказал С классом.forName ("андроид.ос.AsyncTask"). То же самое не произошло на Android 4.1.2, 4.4.4 или 5.0. Интересно, разрешил ли этот Google проблему Мартина Уэста с 2011 года?

Я добавил этот код в свое приложение onCreate, и он сработал:

    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) {
        try {
            Class.forName("android.os.AsyncTask");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
Было бы неплохо узнать, нужно ли менять версию Android на что-то другое.

AsyncTask.execute() должен выполняться в потоке пользовательского интерфейса, то есть внутри действия.

У меня та же проблема, кажется, это происходит, когда AsyncTask работает во время приостановки / возобновления.

Править: Да, не думал, что у меня есть, но я использовал это http://developer.android.com/guide/appendix/faq/commontasks.html#threading чтобы всегда запускать AsyncTask в потоке пользовательского интерфейса, и проблема исчезла. Проблема возникла после того, как я добавил функцию лицензирования, siggghhhhh

Спасибо

Хотя это не дает прямого ответа на вопрос ОП, я думаю, что это будет полезно для людей, ищущих решение той же проблемы при выполнении тестов.

В целом, ответ Питера Кнего хорошо подводит итог.

Моя проблема была конкретно с запуском теста на классе вне действия, которое использовало AsyncTask Android для вызова API. Класс работает в приложении, так как он используется активностью, но я хотел запустить тест, делая фактический Вызов API из теста.

Хотя ответ Джонатана Перлоусработал, мне не нравилось вносить изменения в мое приложение исключительно из-за теста.

Таким образом, в случае теста runTestOnUiThread можно использовать (@UiThreadTest нельзя использовать, так как вы не можете ждать результата в тесте, который использует эту аннотацию).

public void testAPICall() throws Throwable {
    this.runTestOnUiThread(new Runnable() {
        public void run() {
            underTest.thisMethodWillMakeUseOfAnAsyncTaskSomehow();
        }           
    }); 

    // Wait for result here *
    // Asserts here
}
Хотя иногда, особенно в функциональных тестах, ответ Джонатана Перлоу кажется единственным, который работает.

* посмотрите здесь , чтобы увидеть, как приостановить тест в ожидании результата.