即使 Activity 已销毁,AsyncTask 也不会停止
Posted
技术标签:
【中文标题】即使 Activity 已销毁,AsyncTask 也不会停止【英文标题】:AsyncTask won't stop even when the Activity has destroyed 【发布时间】:2011-02-01 15:30:21 【问题描述】:我有一个 AsyncTask
对象,它在创建 Activity
时开始执行并在后台执行操作(最多下载 100 个图像)。一切正常,但有一种我无法理解的特殊行为。
例如: 当 android 屏幕的方向改变时,Activity
被销毁并再次创建。所以我重写了onRetainNonConfigurationInstance()
方法并将所有下载的数据保存在AsyncTask
中执行。我这样做的目的是不让AsyncTask
在方向更改期间每次被销毁创建时都运行AsyncTask
,但正如我在日志中看到的那样,之前的AsyncTask
仍在执行。 (虽然数据保存正确)
我什至尝试在活动的onDestroy()
方法中取消AsyncTask
,但日志仍然显示AsyncTask
正在运行。
这是一种非常奇怪的行为,如果有人能告诉我停止/取消AsyncTask
的正确程序,我将非常感激。
【问题讨论】:
【参考方案1】:@Romain Guy 给出的答案是正确的。不过,我想补充一些信息,并给出一个或 2 个库的指针,这些库可用于长时间运行的 AsyncTask,甚至更多用于面向网络的 asynctask。
AsyncTasks 是为在后台做事而设计的。是的,您可以使用cancel
方法停止它。当你从网上下载东西时,我强烈建议你take care of your thread when it is the IO blocking state。您应该按如下方式组织您的下载:
public void download()
//get the InputStream from HttpUrlConnection or any other
//network related stuff
while( inputStream.read(buffer) != -1 && !Thread.interrupted() )
//copy data to your destination, a file for instance
//close the stream and other resources
使用Thread.interrupted
标志将帮助您的线程正确退出阻塞io 状态。您的线程将对cancel
方法的调用更加敏感。
AsyncTask 设计缺陷
但是如果您的 AsyncTask 持续时间过长,那么您将面临 2 个不同的问题:
-
Activity 与 Activity 生命周期的联系很差,如果您的 Activity 终止,您将无法获得 AsyncTask 的结果。确实,是的,你可以,但这是一种粗略的方式。
AsyncTask 没有很好的文档记录。 asynctask 的简单但直观的实现和使用会很快导致内存泄漏。
RoboSpice,我想介绍的库,使用后台服务来执行这种请求。它专为网络请求而设计。它提供了额外的功能,例如自动缓存请求的结果。
这就是为什么 AsyncTasks 不适合长时间运行的任务的原因。以下推理改编自 RoboSpice motivations 的摘录:该应用解释了为什么使用 RoboSpice 满足了 Android 平台上的需求。
AsyncTask 和 Activity 生命周期
AsyncTasks 不遵循 Activity 实例的生命周期。如果您在 Activity 中启动 AsyncTask 并旋转设备,则 Activity 将被销毁并创建一个新实例。但是 AsyncTask 不会死。它将继续存在,直到完成。
当它完成时,AsyncTask 不会更新新 Activity 的 UI。事实上,它更新了活动的前一个实例, 不再显示。这可能导致 java.lang.IllegalArgumentException: View not attach to window manager 类型的异常,如果您 例如,使用 findViewById 来检索 Activity 中的视图。
内存泄漏问题
将 AsyncTasks 创建为活动的内部类非常方便。因为 AsyncTask 需要操作视图 当任务完成或正在进行时,使用 Activity 的内部类似乎很方便:内部类可以 直接访问外部类的任何字段。
尽管如此,这意味着内部类将在其外部类实例上持有一个不可见的引用:Activity。
从长远来看,这会产生内存泄漏:如果 AsyncTask 持续很长时间,它会使活动保持“活动” 而Android想摆脱它,因为它不能再显示了。该活动不能被垃圾收集,这是一个中心 Android 在设备上保留资源的机制。
您的任务进度将丢失
您可以使用一些变通方法来创建一个长时间运行的异步任务,并根据活动的生命周期管理其生命周期。你可以cancel the AsyncTask in the onStop method of your activity 或者你可以让你的异步任务完成,而不是丢失它的进度和relink it to the next instance of your activity。
这是可能的,我们展示了 RobopSpice 的动机,但它变得复杂并且代码不是真正通用的。此外,如果用户离开活动并返回,您仍然会丢失任务的进度。 Loaders 也会出现同样的问题,尽管它更简单地等同于 AsyncTask 以及上面提到的重新链接解决方法。
使用 Android 服务
最好的选择是使用服务来执行您长时间运行的后台任务。而这正是 RoboSpice 提出的解决方案。同样,它是为网络设计的,但可以扩展到非网络相关的东西。这个库有一个large number of features。
感谢infographics,您甚至可以在 30 秒内了解它。
将 AsyncTasks 用于长时间运行的操作确实是一个非常非常糟糕的主意。不过,它们适用于短暂的生命周期,例如在 1 或 2 秒后更新视图。
我鼓励您下载RoboSpice Motivations app,它确实深入地解释了这一点,并提供了执行一些与网络相关的东西的不同方法的示例和演示。
如果您正在为非网络相关任务(例如没有缓存)寻找 RoboSpice 的替代品,您也可以查看 Tape。
【讨论】:
@Snicolas,你能在 Github 页面上添加droidQuery 作为 RoboSpice 的替代品吗?我去年开发了它,它提供了使用 jQuery 风格的 Ajax(用纯 Android Java 编写)执行网络任务等功能。 @Phil,完成。谢谢链接。顺便说一句,你的语法真的很接近离子(后来)。 @Snicolas,谢谢!看起来 droidQuery 和 Ion 都从 Picasso 中进行了一些语法设计。 github.com/koush/ion/search?q=picasso&ref=cmdform ;) “将 AsyncTasks 用于长时间运行的操作确实是一个非常非常糟糕的主意。不过,它们适用于短暂的操作,例如在 1 或 2 秒后更新视图。” .我认为这不一定是真的。 Android 建议这样做的原因是 AsyncTask 使用全局线程池执行程序。您可以简单地通过 execute 方法传递您的自定义线程池执行程序,并摆脱可能的阻塞问题。虽然 AsyncTask 不是你提到的“泄漏”的灵丹妙药。 问题不在于池,而在于泄漏。【参考方案2】:罗曼盖伊是对的。事实上,一个异步任务在任何情况下都负责完成它自己的工作。中断不是最好的方法,因此您应该不断检查是否有人要您取消或停止您的任务。
假设您的AsyncTask
多次循环执行某项操作。然后你应该在每个循环中检查isCancelled()
。
while ( true )
if ( isCancelled())
break;
doTheTask();
doTheTask()
是你真正的工作,在你每次循环之前,你检查你的任务是否应该被取消。
通常你应该在你的AsyncTask
类中设置一个标志或者从你的doInBackground()
返回一个适当的结果,这样你就可以在你的onPostExecute()
中检查你是否可以完成你想要的或者你的工作是否被取消在中间。
【讨论】:
【参考方案3】:以下内容不能解决您的问题,但可以防止它: 在应用清单中执行以下操作:
<activity
android:name=".(your activity name)"
android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden|screenSize" > //this line here
</activity>
当您添加这个时,您的活动不会在配置更改时重新加载,如果您想在方向更改时进行一些更改,您只需覆盖活动的以下方法:
@Override
public void onConfigurationChanged(Configuration newConfig)
super.onConfigurationChanged(newConfig);
//your code here
【讨论】:
o,... 一旦设备的方向改变了,这个方法会被调用吗? @FranePoljak...我c,我c,...【参考方案4】:活动在方向改变时重新创建,是的,这是真的。 但无论何时发生此事件,您都可以继续执行异步任务。
你去看看
@Override
protected void onCreate(Bundle savedInstanceState)
if ( savedInstanceState == null )
startAsyncTask()
else
// ** Do Nothing async task will just continue.
-干杯
【讨论】:
【参考方案5】:从MVC的角度来看,Activity就是Controller; Controller 执行超过 View 的操作是错误的(从 android.view.View 派生,通常你只是重用现有的类)。因此,启动 AsyncTasks 应该是 Model 的责任。
【讨论】:
【参考方案6】:您可以从this post 使用class MagicAppRestart
与所有 AsyncTask 一起杀死进程; Android 将恢复活动堆栈(用户不会提及任何内容)。需要注意的是,进程重启前的唯一通知是调用onPause()
;根据Android app lifecycle logic,无论如何,您的应用程序必须准备好终止。
我已经尝试过了,它似乎有效。不过,目前我计划使用“更文明”的方法,例如来自 Application 类的弱引用(我的 AsyncTask 的生命周期相当短,希望不会消耗太多内存)。
这里有一些你可以玩的代码:
MagicAppRestart.java
package com.xyz;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
/** This activity shows nothing; instead, it restarts the android process */
public class MagicAppRestart extends Activity
// Do not forget to add it to AndroidManifest.xml
// <activity android:name="your.package.name.MagicAppRestart"/>
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
System.exit(0);
public static void doRestart(Activity anyActivity)
anyActivity.startActivity(new Intent(anyActivity.getApplicationContext(), MagicAppRestart.class));
剩下的是 Eclipse 为 com.xyz.AsyncTaskTestActivity 的新 Android 项目创建的:
AsyncTaskTestActivity.java
package com.xyz;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
public class AsyncTaskTestActivity extends Activity
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
Log.d("~~~~","~~~onCreate ~~~ "+this);
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
public void onStartButton(View view)
Log.d("~~~~","~~~onStartButton ");
class MyTask extends AsyncTask<Void, Void, Void>
@Override
protected Void doInBackground(Void... params)
// TODO Auto-generated method stub
Log.d("~~~~","~~~doInBackground started");
try
for (int i=0; i<10; i++)
Log.d("~~~~","~~~sleep#"+i);
Thread.sleep(200);
Log.d("~~~~","~~~sleeping over");
catch (InterruptedException e)
// TODO Auto-generated catch block
e.printStackTrace();
Log.d("~~~~","~~~doInBackground ended");
return null;
@Override
protected void onPostExecute(Void result)
super.onPostExecute(result);
taskDone();
MyTask task = new MyTask();
task.execute(null);
Log.d("~~~~","~~~onStartButton ");
private void taskDone()
Log.d("~~~~","\n\n~~~taskDone ~~~ "+this+"\n\n");
public void onStopButton(View view)
Log.d("~~~~","~~~onStopButton ");
MagicAppRestart.doRestart(this);
Log.d("~~~~","~~~onStopButton ");
public void onPause() Log.d("~~~~","~~~onPause ~~~ "+this); super.onPause();
public void onStop() Log.d("~~~~","~~~onStop ~~~ "+this); super.onPause();
public void onDestroy() Log.d("~~~~","~~~onDestroy ~~~ "+this); super.onDestroy();
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:orientation="vertical" >
<Button android:text="Start" android:onClick="onStartButton" android:layout_ android:layout_/>
<Button android:text="Stop" android:onClick="onStopButton" android:layout_ android:layout_/>
<TextView
android:layout_
android:layout_
android:text="@string/hello" />
</LinearLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xyz"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="7" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".AsyncTaskTestActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MagicAppRestart"/>
</application>
</manifest>
以及日志的相关部分(注意只调用onPause
):
D/~~~~ (13667): ~~~onStartButton
D/~~~~ (13667): ~~~onStartButton
D/~~~~ (13667): ~~~doInBackground started
D/~~~~ (13667): ~~~sleep#0
D/~~~~ (13667): ~~~sleep#1
D/~~~~ (13667): ~~~sleep#2
D/~~~~ (13667): ~~~sleep#3
D/~~~~ (13667): ~~~sleep#4
D/~~~~ (13667): ~~~sleep#5
D/~~~~ (13667): ~~~sleep#6
D/~~~~ (13667): ~~~sleep#7
D/~~~~ (13667): ~~~sleep#8
D/~~~~ (13667): ~~~sleep#9
D/~~~~ (13667): ~~~sleeping over
D/~~~~ (13667): ~~~doInBackground ended
D/~~~~ (13667):
D/~~~~ (13667):
D/~~~~ (13667): ~~~taskDone ~~~ com.xyz.AsyncTaskTestActivity@40516988
D/~~~~ (13667):
D/~~~~ (13667): ~~~onStartButton
D/~~~~ (13667): ~~~onStartButton
D/~~~~ (13667): ~~~doInBackground started
D/~~~~ (13667): ~~~sleep#0
D/~~~~ (13667): ~~~sleep#1
D/~~~~ (13667): ~~~sleep#2
D/~~~~ (13667): ~~~sleep#3
D/~~~~ (13667): ~~~sleep#4
D/~~~~ (13667): ~~~sleep#5
D/~~~~ (13667): ~~~onStopButton
I/ActivityManager( 81): Starting: Intent cmp=com.xyz/.MagicAppRestart from pid 13667
D/~~~~ (13667): ~~~onStopButton
D/~~~~ (13667): ~~~onPause ~~~ com.xyz.AsyncTaskTestActivity@40516988
I/ActivityManager( 81): Process com.xyz (pid 13667) has died.
I/WindowManager( 81): WIN DEATH: Window4073ceb8 com.xyz/com.xyz.AsyncTaskTestActivity paused=false
I/ActivityManager( 81): Start proc com.xyz for activity com.xyz/.AsyncTaskTestActivity: pid=13698 uid=10101 gids=
I/ActivityManager( 81): Displayed com.xyz/.AsyncTaskTestActivity: +44ms (total +65ms)
D/~~~~ (13698): ~~~onCreate ~~~ com.xyz.AsyncTaskTestActivity@40517238
【讨论】:
2013 年(Android 2.x 时代)回答,2017 年(Android 6 和 7 时代)被否决。从那以后肯定发生了很多变化,我知道应用重启黑客从 Android 4 开始就不起作用了。以上是关于即使 Activity 已销毁,AsyncTask 也不会停止的主要内容,如果未能解决你的问题,请参考以下文章
AsyncTask 完成后关闭 Activity 并返回父 Activity
android:当Activity和Service 都被销毁后,如何控制其中生成的线程?