onPostExecute 未在 AsyncTask 中调用(处理程序运行时异常)
我有一个 AsyncTask 来获取一些数据,然后用这些新数据更新 UI。它已经工作了几个月,但我最近添加了一项功能,可以在有新数据时显示通知。现在,当我的应用程序通过通知启动时,有时我会收到此异常,并且不会调用 onPostExecute
。
这是应用程序启动时发生的情况:
1) 展开 UI 并查找视图
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)
编辑:这是我的主活动中的 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
编辑2:我一直在挖掘Android源代码,追踪异常的来源。这是 Handler
中 sendMessageAtTime
的第 456 行和第 457 行:
msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);
这是来自 MessageQueue
的 enqueueMessage
:
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 为 null。
I have an AsyncTask
that fetches some data and then updates the UI with this new data. It has been working fine for months, but I recently added a feature that displays a notification when there is new data. Now when my app is launched through the notification, sometimes I get this exception and onPostExecute
is not called.
This is what happens when the app is launched:
1) Expand the UI and find views
2) Cancel the alarm (through AlarmManager
) that checks for new data and reset the alarm. (This is so that if the user disables the alarm it is cancelled before the next time he/she reboots.)
3) Start the AsyncTask
. If the app was launched from the notification, pass in a little bit of the data and then cancel the notification.
I'm stuck on what could be causing this exception. It seems that the exception is from the AsyncTask
code, so I'm not sure how I can fix it.
Thanks!
Here is the exception:
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: Here is my onCreate
method in my main activity (the one opened by the notification). There are some onClickListeners
that I omitted to save space. I don't think they should have any effect, since the buttons they are attached to are not being pressed.
@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: I've been digging through Android source code tracking down where the exception is coming from. This is lines 456 and 457 of sendMessageAtTime
in Handler
:
msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);
And this is enqueueMessage
from 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;
}
I'm a little confused about what mQuiting
is, but it looks like the previous time enqueueMessage
was called msg.target
was null.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
这是由于 Android 框架中 AsyncTask 的错误造成的。 AsyncTask.java 具有以下代码:
它期望在主线程上对其进行初始化,但这不能保证,因为它将在导致类运行其静态初始化器的任何线程上进行初始化。我重现了这个问题,其中处理程序引用了工作线程。
导致这种情况发生的常见模式是使用 IntentService 类。 C2DM 示例代码执行此操作。
一个简单的解决方法是将以下代码添加到应用程序的 onCreate 方法中:
这将强制 AsyncTask 在主线程中初始化。我在 android bug 数据库中提交了一个关于此的 bug。请参阅 http://code.google.com/p/android/issues/详情?id=20915。
This is due to a bug in AsyncTask in the Android framework. AsyncTask.java has the following code:
It expects this to be initialized on the main thread, but that is not guaranteed since it will be initialized on whichever thread happens to cause the class to run its static initializers. I reproduced this issue where the Handler references a worker thread.
A common pattern that causes this to happen is using the class IntentService. The C2DM sample code does this.
A simple workaround is to add the following code to the application's onCreate method:
This will force AsyncTask to be initialized in the main thread. I filed a bug on this in the android bug database. See http://code.google.com/p/android/issues/detail?id=20915.
为了概括 Jonathan Perlow 对他具体识别的错误的解决方案,我在使用 AsyncTask 的任何类中使用以下内容。通过 Looper/handler/post,您可以在 Android 应用程序中的任何位置的 UI 线程上运行某些内容,而无需将句柄传递给 Activity 或其他上下文。在类中添加这个静态初始化块:
我们在尝试运行单元测试时遇到了问题。我找到了解决方法,但没有具体找出问题所在。我们只知道尝试使用 AsyncTask<>在 Android JUnit 测试中导致 onPostExecute() 未被调用。现在我们知道为什么了。
这篇文章展示了如何在 Android JUnit 测试中运行多线程异步代码:
使用Android 基于 AsyncTask 的 JUnit 测试中的 CountDownLatch
为了与非 UI 单元测试一起使用,我创建了 android.test.InstrumentationTestCase 的一个简单子类。它有一个“ok”标志和一个 CountDownLatch。 Reset() 或 Reset(count) 创建一个新的 CountDownLatch({1,count})。 good() 设置 ok=true、count--,并在锁存器上调用.countDown()。 bad()设置ok=false,并一路倒计时。 waitForIt(秒) 等待超时或倒计时锁存为零。然后它调用assertTrue(ok)。
然后测试就像:
由于 AsyncTask 静态初始化错误,我们必须在传递给 runTestOnUiThread() 的 Runnable 中运行实际测试。通过上述正确的静态初始化,这应该不是必需的,除非正在测试的调用需要在 UI 线程上运行。
我现在使用的另一个习惯用法是测试当前线程是否是 UI 线程,然后无论如何在正确的线程上运行请求的操作。有时,允许调用者请求同步与异步,并在必要时覆盖是有意义的。例如,网络请求应始终在后台线程上运行。在大多数情况下,AsyncTask 线程池非常适合此目的。只需要意识到,一次只会运行一定数量的请求,从而阻止其他请求。测试当前线程是否为UI线程:
然后使用AsyncTask<>的一个简单子类(只需要doInBackground()和onPostExecute());在非 UI 线程上运行,或者 handler.post() 或 postDelayed() 在 UI 线程上运行。
为调用者提供运行同步或异步的选项,如下所示(获取此处未显示的本地有效的 onUiThread 值;如上所述添加本地布尔值):
此外,使用 AsyncTask 可以变得非常简单和简洁。使用下面 RunAsyncTask.java 的定义,然后编写如下代码:
或者简单地:new RunAsyncTask("").execute(new Runnable(){public void run(){ doSomethingInBackground(); }});
RunAsyncTask.java:
To generalize Jonathan Perlow's solution to the bug he identified specifically, I use the following in any class that uses AsyncTask. The looper/handler/post is how you can run something on the UI thread anywhere in an Android app without passing down a handle to an activity or other context. Add this static initialization block inside the class:
We had run into the problem when trying to get unit tests to run. I found a workaround for that, but hadn't specifically identified the problem. We only knew that trying to use AsyncTask<> in Android JUnit test caused onPostExecute() not to be called. Now we know why.
This post shows how to run multithreaded async code in an Android JUnit test:
Using CountDownLatch in Android AsyncTask-based JUnit tests
For use with non-UI unit tests, I created a simple subclass of android.test.InstrumentationTestCase. It has an "ok" flag and a CountDownLatch. reset() or reset(count) creates a new CountDownLatch({1,count}). good() sets ok=true, count--, and calls.countDown() on the latch. bad() sets ok=false, and counts down all the way. waitForIt(seconds) waits for timeout or the coundown latch to zero. Then it calls assertTrue(ok).
Then tests are like:
Because of the AsyncTask static initialization bug, we had to run our actual tests inside a Runnable passed to runTestOnUiThread(). With proper static initialization as above, this shouldn't be necessary, unless the call being tested needs to run on the UI thread.
The other idiom I now use is to test whether the current thread is the UI thread and then run the requested action on the proper thread regardless. Sometimes, it makes sense to allow the caller to request sync vs. async, overriding when necessary. For instance, network requests should always be run on a background thread. In most cases, AsyncTask thread pooling is perfect for this. Just realize that only a certain number will run at once, blocking additional requests. To test whether the current thread is the UI thread:
Then use a simple subclass (just doInBackground() and onPostExecute() are needed) of AsyncTask<> to run on a non-UI thread or handler.post() or postDelayed() to run on the UI thread.
Giving the caller the option to run sync or async looks like (getting a locally valid onUiThread value not shown here; add local booleans as above):
Also, using AsyncTask can be made very simple and concise. Use the definition of RunAsyncTask.java below, then write code like this:
Or simply:new RunAsyncTask("").execute(new Runnable(){public void run(){ doSomethingInBackground(); }});
RunAsyncTask.java:
我在带有 IntentService 的 Android 4.0.4 设备上遇到了同样的问题,并按照 sdw 所说的 Class.forName("android.os.AsyncTask") 解决了它。 Android 4.1.2、4.4.4 或 5.0 上没有发生同样的情况。我想知道这个 Google 是否解决了 2011 年的 Martin West 问题。
我在我的应用程序 onCreate 上添加了这段代码,它起作用了:
如果知道 Android 版本是否需要更改为其他版本,那就太好了。
I had the same problem on a device with Android 4.0.4 with the IntentService and solved it as sdw said with the Class.forName("android.os.AsyncTask"). The same didn't happen on Android 4.1.2, 4.4.4 or 5.0. I wonder if this Google resolved Martin West issue from 2011.
I added this code on my Application onCreate and it worked:
It would be nice to know if the version of Android need to be changed to something else.
AsyncTask.execute()
必须在 UI 线程上执行,即在 Activity 内执行。
AsyncTask.execute()
must be executed on UI thread, i.e. inside Activity.我有同样的问题,它似乎发生在 AsyncTask 在挂起/恢复期间运行时。
编辑:
是的,没想到我有,但我使用了这个 http://developer.android .com/guide/appendix/faq/commontasks.html#threading
始终在 UI 线程上启动 AsyncTask,问题就消失了。
添加授权功能后出现问题,siggghhhhh
谢谢
I have the same problem, it seems to happen when the AsyncTask is running during a suspend/resume.
EDIT:
Yeah, didnt think I had but I used this http://developer.android.com/guide/appendix/faq/commontasks.html#threading
to always start the AsyncTask on the UI thread and the problem has gone.
The problem appeared after I added the licensing function, siggghhhhh
Thanks
尽管这并不能直接回答OP的问题,但我认为这对于人们在运行测试时寻找同一问题的解决方案是有用的。
总的来说,Peter Knego 的回答总结得很好。
我的问题具体是在 Activity 外部的类上运行测试,该 Activity 使用 Android 的 AsyncTask 进行 API 调用。该类在应用程序中工作,因为它由 Activity 使用,但我想运行一个测试,从测试中进行实际的 API 调用。
虽然 Jonathan Perlow 的答案有效,但我不喜欢仅仅因为测试而对我的应用程序进行更改。
因此,在测试的情况下,可以使用
runTestOnUiThread
(不能使用@UiThreadTest
,因为您无法等待使用该注释的测试中的结果)。但有时,尤其是在功能测试中,乔纳森·珀洛的答案似乎是唯一有效的答案。
* 查看此处了解如何暂停测试等待结果。
Even though this doesn't directly answer the OP's question, I think it will be useful for people searching for the solution of the same problem when running tests.
Overall, Peter Knego's answer sums it up well.
My problem was specifically with running a test on a class outside an Activity that made use of Android's AsyncTask for an API call. The class works in the application, since it is used by an Activity, but I wanted to run a test making an actual API call from the test.
While Jonathan Perlow's answer worked, I didn't like introducing changes to my application due solely to a test.
So, in the case of a test
runTestOnUiThread
can be used (@UiThreadTest
cannot be used, since you cannot wait for a result in a test that uses that annotation).Sometimes though, especially in functional tests, Jonathan Perlow's answer seems to be the only one that works.
* Take a look here to see how to pause a test waiting for a result.