安卓碎片。在屏幕旋转或配置更改期间保留 AsyncTask
题
我正在开发一款智能手机/平板电脑应用程序,仅使用一个 APK,并根据屏幕尺寸加载所需的资源,最佳设计选择似乎是通过 ACL 使用片段。
这个应用程序一直运行良好,直到现在仅基于活动。这是我如何处理活动中的 AsyncTasks 和 ProgressDialogs 的模拟类,以便即使在屏幕旋转或通信过程中发生配置更改时也能让它们正常工作。
我不会更改清单以避免重新创建活动,我不想这样做的原因有很多,但主要是因为官方文档说不推荐它,并且到目前为止我已经成功地没有它,所以请不要推荐路线。
public class Login extends Activity {
static ProgressDialog pd;
AsyncTask<String, Void, Boolean> asyncLoginThread;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.login);
//SETUP UI OBJECTS
restoreAsyncTask();
}
@Override
public Object onRetainNonConfigurationInstance() {
if (pd != null) pd.dismiss();
if (asyncLoginThread != null) return (asyncLoginThread);
return super.onRetainNonConfigurationInstance();
}
private void restoreAsyncTask();() {
pd = new ProgressDialog(Login.this);
if (getLastNonConfigurationInstance() != null) {
asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
if (asyncLoginThread != null) {
if (!(asyncLoginThread.getStatus()
.equals(AsyncTask.Status.FINISHED))) {
showProgressDialog();
}
}
}
}
public class LoginThread extends AsyncTask<String, Void, Boolean> {
@Override
protected Boolean doInBackground(String... args) {
try {
//Connect to WS, recieve a JSON/XML Response
//Place it somewhere I can use it.
} catch (Exception e) {
return true;
}
return true;
}
protected void onPostExecute(Boolean result) {
if (result) {
pd.dismiss();
//Handle the response. Either deny entry or launch new Login Succesful Activity
}
}
}
}
这段代码工作正常,我有大约 10,000 个用户,没有任何抱怨,所以将这个逻辑复制到新的基于片段的设计中似乎是合乎逻辑的,但是,当然,它不起作用。
这是登录片段:
public class LoginFragment extends Fragment {
FragmentActivity parentActivity;
static ProgressDialog pd;
AsyncTask<String, Void, Boolean> asyncLoginThread;
public interface OnLoginSuccessfulListener {
public void onLoginSuccessful(GlobalContainer globalContainer);
}
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
//Save some stuff for the UI State
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setRetainInstance(true);
//If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
parentActivity = getActivity();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
return loginLayout;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//SETUP UI OBJECTS
if(savedInstanceState != null){
//Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
}
}
public class LoginThread extends AsyncTask<String, Void, Boolean> {
@Override
protected Boolean doInBackground(String... args) {
try {
//Connect to WS, recieve a JSON/XML Response
//Place it somewhere I can use it.
} catch (Exception e) {
return true;
}
return true;
}
protected void onPostExecute(Boolean result) {
if (result) {
pd.dismiss();
//Handle the response. Either deny entry or launch new Login Succesful Activity
}
}
}
}
}
我不能使用 onRetainNonConfigurationInstance()
因为它必须从 Activity 而不是 Fragment 调用,所以同样如此 getLastNonConfigurationInstance()
. 。我在这里读过一些类似的问题但没有答案。
我知道可能需要一些解决办法才能将这些东西正确组织在片段中,也就是说,我想保持相同的基本设计逻辑。
在配置更改期间保留 AsyncTask 的正确方法是什么,如果它仍在运行,则显示进度对话框,考虑到 AsyncTask 是 Fragment 的内部类,并且是 Fragment 本身调用 AsyncTask.execute ()?
解决方案
片段实际上可以使这变得容易得多。只需使用该方法 Fragment.setRetainInstance(布尔值) 在配置更改期间保留您的片段实例。请注意,这是推荐的替代品 Activity.onRetainnonConfigurationInstance() 在文档中。
如果由于某种原因您确实不想使用保留的片段,则可以采取其他方法。请注意,每个片段都有一个由以下方法返回的唯一标识符 片段.getId(). 。您还可以通过以下方式了解片段是否因配置更改而被拆除 Fragment.getActivity().isChangingConfigurations(). 。因此,当您决定停止 AsyncTask(最有可能在 onStop() 或 onDestroy() 中)时,您可以检查配置是否正在更改,如果是,则将其粘贴到片段标识符下的静态 SparseArray 中,然后在 onCreate() 或 onStart() 中查看稀疏数组中是否有 AsyncTask 可用。
其他提示
我想您会喜欢下面详细介绍的极其全面且有效的示例。
- 旋转有效,对话得以保留。
- 您可以通过按后退按钮取消任务和对话框(如果您想要此行为)。
- 它使用片段。
- 当设备旋转时,活动下方片段的布局会正确更改。
- 有完整的源代码下载 预编译的APK 这样您就可以查看该行为是否是您想要的。
编辑
根据 Brad Larson 的要求,我复制了下面大部分链接的解决方案。自从我发布它以来,我就被指出了 AsyncTaskLoader
. 。我不确定它是否完全适用于同样的问题,但无论如何你应该检查一下。
使用 AsyncTask
带有进度对话框和设备旋转。
一个可行的解决方案!
我终于让一切都开始工作了。我的代码有以下特点:
- A
Fragment
其布局随方向而变化。 - 一个
AsyncTask
您可以在其中做一些工作。 - A
DialogFragment
它在进度条中显示任务的进度(而不仅仅是不确定的旋转器)。 - 旋转不会中断任务或关闭对话框。
- 后退按钮会关闭对话框并取消任务(不过您可以相当轻松地更改此行为)。
我认为在其他任何地方都找不到这种工作性的结合。
基本思路如下。有一个 MainActivity
包含单个片段的类 - MainFragment
. MainFragment
水平和垂直方向有不同的布局,并且 setRetainInstance()
为 false 以便布局可以更改。这意味着当设备方向改变时,两者 MainActivity
和 MainFragment
被完全摧毁并重建。
我们分别有 MyTask
(扩展自 AsyncTask
) 完成所有工作。我们无法将其存储在 MainFragment
因为它会被销毁,谷歌已经不推荐使用类似的东西 setRetainNonInstanceConfiguration()
. 。无论如何,这并不总是可用,而且充其量只是一个丑陋的黑客行为。相反,我们将存储 MyTask
在另一个片段中, DialogFragment
被称为 TaskFragment
. 这 分段 将要 有 setRetainInstance()
设置为 true,因此当设备旋转时,该片段不会被破坏,并且 MyTask
被保留。
最后我们需要告诉 TaskFragment
完成后通知谁,我们使用 setTargetFragment(<the MainFragment>)
当我们创建它时。当设备旋转并且 MainFragment
被销毁并创建一个新实例,我们使用 FragmentManager
找到对话框(基于其标签)并执行 setTargetFragment(<the new MainFragment>)
. 。差不多就这样了。
我还需要做两件事:首先在对话框关闭时取消任务,然后将关闭消息设置为空,否则当设备旋转时对话框会奇怪地关闭。
代码
我不会列出布局,它们非常明显,您可以在下面的项目下载中找到它们。
主要活动
这非常简单。我在此活动中添加了一个回调,以便它知道任务何时完成,但您可能不需要它。主要我只是想展示片段活动回调机制,因为它非常简洁,您以前可能没有见过。
public class MainActivity extends Activity implements MainFragment.Callbacks
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onTaskFinished()
{
// Hooray. A toast to our success.
Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
// NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
// the duration in milliseconds. ANDROID Y U NO ENUM?
}
}
主片段
虽然很长但是值得!
public class MainFragment extends Fragment implements OnClickListener
{
// This code up to onDetach() is all to get easy callbacks to the Activity.
private Callbacks mCallbacks = sDummyCallbacks;
public interface Callbacks
{
public void onTaskFinished();
}
private static Callbacks sDummyCallbacks = new Callbacks()
{
public void onTaskFinished() { }
};
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (!(activity instanceof Callbacks))
{
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
mCallbacks = (Callbacks) activity;
}
@Override
public void onDetach()
{
super.onDetach();
mCallbacks = sDummyCallbacks;
}
// Save a reference to the fragment manager. This is initialised in onCreate().
private FragmentManager mFM;
// Code to identify the fragment that is calling onActivityResult(). We don't really need
// this since we only have one fragment to deal with.
static final int TASK_FRAGMENT = 0;
// Tag so we can find the task fragment again, in another instance of this fragment after rotation.
static final String TASK_FRAGMENT_TAG = "task";
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// At this point the fragment may have been recreated due to a rotation,
// and there may be a TaskFragment lying around. So see if we can find it.
mFM = getFragmentManager();
// Check to see if we have retained the worker fragment.
TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);
if (taskFragment != null)
{
// Update the target fragment so it goes to this fragment instead of the old one.
// This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
// keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
// use weak references. To be sure you aren't leaking, you may wish to make your own
// setTargetFragment() which does.
taskFragment.setTargetFragment(this, TASK_FRAGMENT);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_main, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
// Callback for the "start task" button. I originally used the XML onClick()
// but it goes to the Activity instead.
view.findViewById(R.id.taskButton).setOnClickListener(this);
}
@Override
public void onClick(View v)
{
// We only have one click listener so we know it is the "Start Task" button.
// We will create a new TaskFragment.
TaskFragment taskFragment = new TaskFragment();
// And create a task for it to monitor. In this implementation the taskFragment
// executes the task, but you could change it so that it is started here.
taskFragment.setTask(new MyTask());
// And tell it to call onActivityResult() on this fragment.
taskFragment.setTargetFragment(this, TASK_FRAGMENT);
// Show the fragment.
// I'm not sure which of the following two lines is best to use but this one works well.
taskFragment.show(mFM, TASK_FRAGMENT_TAG);
// mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
{
// Inform the activity.
mCallbacks.onTaskFinished();
}
}
任务片段
// This and the other inner class can be in separate files if you like.
// There's no reason they need to be inner classes other than keeping everything together.
public static class TaskFragment extends DialogFragment
{
// The task we are running.
MyTask mTask;
ProgressBar mProgressBar;
public void setTask(MyTask task)
{
mTask = task;
// Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
mTask.setFragment(this);
}
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Retain this instance so it isn't destroyed when MainActivity and
// MainFragment change configuration.
setRetainInstance(true);
// Start the task! You could move this outside this activity if you want.
if (mTask != null)
mTask.execute();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_task, container);
mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);
getDialog().setTitle("Progress Dialog");
// If you're doing a long task, you probably don't want people to cancel
// it just by tapping the screen!
getDialog().setCanceledOnTouchOutside(false);
return view;
}
// This is to work around what is apparently a bug. If you don't have it
// here the dialog will be dismissed on rotation, so tell it not to dismiss.
@Override
public void onDestroyView()
{
if (getDialog() != null && getRetainInstance())
getDialog().setDismissMessage(null);
super.onDestroyView();
}
// Also when we are dismissed we need to cancel the task.
@Override
public void onDismiss(DialogInterface dialog)
{
super.onDismiss(dialog);
// If true, the thread is interrupted immediately, which may do bad things.
// If false, it guarantees a result is never returned (onPostExecute() isn't called)
// but you have to repeatedly call isCancelled() in your doInBackground()
// function to check if it should exit. For some tasks that might not be feasible.
if (mTask != null) {
mTask.cancel(false);
}
// You don't really need this if you don't want.
if (getTargetFragment() != null)
getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
}
@Override
public void onResume()
{
super.onResume();
// This is a little hacky, but we will see if the task has finished while we weren't
// in this activity, and then we can dismiss ourselves.
if (mTask == null)
dismiss();
}
// This is called by the AsyncTask.
public void updateProgress(int percent)
{
mProgressBar.setProgress(percent);
}
// This is also called by the AsyncTask.
public void taskFinished()
{
// Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
// after the user has switched to another app.
if (isResumed())
dismiss();
// If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
// onResume().
mTask = null;
// Tell the fragment that we are done.
if (getTargetFragment() != null)
getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
}
}
我的任务
// This is a fairly standard AsyncTask that does some dummy work.
public static class MyTask extends AsyncTask<Void, Void, Void>
{
TaskFragment mFragment;
int mProgress = 0;
void setFragment(TaskFragment fragment)
{
mFragment = fragment;
}
@Override
protected Void doInBackground(Void... params)
{
// Do some longish task. This should be a task that we don't really
// care about continuing
// if the user exits the app.
// Examples of these things:
// * Logging in to an app.
// * Downloading something for the user to view.
// * Calculating something for the user to view.
// Examples of where you should probably use a service instead:
// * Downloading files for the user to save (like the browser does).
// * Sending messages to people.
// * Uploading data to a server.
for (int i = 0; i < 10; i++)
{
// Check if this has been cancelled, e.g. when the dialog is dismissed.
if (isCancelled())
return null;
SystemClock.sleep(500);
mProgress = i * 10;
publishProgress();
}
return null;
}
@Override
protected void onProgressUpdate(Void... unused)
{
if (mFragment == null)
return;
mFragment.updateProgress(mProgress);
}
@Override
protected void onPostExecute(Void unused)
{
if (mFragment == null)
return;
mFragment.taskFinished();
}
}
}
下载示例项目
我最近 发表了一篇文章 描述如何使用retained处理配置更改 Fragment
s。它解决了保留问题 AsyncTask
跨越一个旋转变化很好。
TL;DR 是使用主机 AsyncTask
里面一个 Fragment
, , 称呼 setRetainInstance(true)
于 Fragment
, ,并报告 AsyncTask
的进度/结果返回到它的 Activity
(或者它的目标 Fragment
, ,如果您选择通过保留使用@Timmmm描述的方法) Fragment
.
我的第一个建议是 避免内部 AsyncTasks, ,您可以阅读我就此问题提出的问题及其答案: 安卓:异步任务建议:私人课还是公开课?
之后我开始使用非内部和......现在我看到了很多好处。
第二个是,在中保留正在运行的 AsyncTask 的引用 Application
班级 - http://developer.android.com/reference/android/app/Application.html
每次启动 AsyncTask 时,将其设置在应用程序上,并在完成时将其设置为 null。
当片段/活动启动时,您可以检查是否有任何 AsyncTask 正在运行(通过检查应用程序上它是否为空),然后将内部引用设置为您想要的任何内容(活动、片段等,以便您可以执行回调)。
这将解决您的问题:如果您在任何确定的时间只有 1 个 AsyncTask 运行,您可以添加一个简单的引用:
AsyncTask<?,?,?> asyncTask = null;
否则,在应用程序中有一个包含对它们的引用的 HashMap。
进度对话框可以遵循完全相同的原则。
我想出了一种使用 AsyncTaskLoaders 的方法。在我看来,它非常易于使用,并且需要较少的开销。
基本上你创建一个 AsyncTaskLoader 是这样的:
public class MyAsyncTaskLoader extends AsyncTaskLoader {
Result mResult;
public HttpAsyncTaskLoader(Context context) {
super(context);
}
protected void onStartLoading() {
super.onStartLoading();
if (mResult != null) {
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
forceLoad();
}
}
@Override
public Result loadInBackground() {
SystemClock.sleep(500);
mResult = new Result();
return mResult;
}
}
然后,在单击按钮时使用上述 AsyncTaskLoader 的活动中:
public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {
private String username,password;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.mylayout);
//this is only used to reconnect to the loader if it already started
//before the orientation changed
Loader loader = getSupportLoaderManager().getLoader(0);
if (loader != null) {
getSupportLoaderManager().initLoader(0, null, this);
}
}
public void doBackgroundWorkOnClick(View button) {
//might want to disable the button while you are doing work
//to prevent user from pressing it again.
//Call resetLoader because calling initLoader will return
//the previous result if there was one and we may want to do new work
//each time
getSupportLoaderManager().resetLoader(0, null, this);
}
@Override
public Loader<Result> onCreateLoader(int i, Bundle bundle) {
//might want to start a progress bar
return new MyAsyncTaskLoader(this);
}
@Override
public void onLoadFinished(Loader<LoginResponse> loginLoader,
LoginResponse loginResponse)
{
//handle result
}
@Override
public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
{
//remove references to previous loader resources
}
}
这似乎可以很好地处理方向变化,并且您的后台任务将在旋转期间继续。
有几点需要注意:
- 如果在 onCreate 中您重新附加到 asynctaskloader,您将在 onLoadFinished() 中收到先前结果的回调(即使您已经被告知请求已完成)。在大多数情况下,这实际上是良好的行为,但有时处理起来可能很棘手。虽然我想象有很多方法可以处理这个问题,但我所做的是在 onLoadFinished 中调用 loader.abandon() 。然后我在 onCreate 中添加了检查,以便仅重新附加到加载程序(如果尚未放弃)。如果您再次需要结果数据,您将不想这样做。在大多数情况下,您需要数据。
我有更多关于使用它进行 http 调用的详细信息 这里
我创建了一个非常小的开源后台任务库,它很大程度上基于 Marshmallow AsyncTask
但具有附加功能,例如:
- 在配置更改时自动保留任务;
- UI 回调(监听器);
- 当设备旋转时不会重新启动或取消任务(就像 Loaders 所做的那样);
该库内部使用 Fragment
没有任何用户界面,在配置更改时保留该界面(setRetainInstance(true)
).
你可以在 GitHub 上找到它: https://github.com/NeoTech-Software/Android-Retainable-Tasks
最基本的例子(0.2.0版本):
此示例使用非常有限的代码完全保留了该任务。
任务:
private class ExampleTask extends Task<Integer, String> {
public ExampleTask(String tag){
super(tag);
}
protected String doInBackground() {
for(int i = 0; i < 100; i++) {
if(isCancelled()){
break;
}
SystemClock.sleep(50);
publishProgress(i);
}
return "Result";
}
}
活动:
public class Main extends TaskActivityCompat implements Task.Callback {
@Override
public void onClick(View view){
ExampleTask task = new ExampleTask("activity-unique-tag");
getTaskManager().execute(task, this);
}
@Override
public Task.Callback onPreAttach(Task<?, ?> task) {
//Restore the user-interface based on the tasks state
return this; //This Activity implements Task.Callback
}
@Override
public void onPreExecute(Task<?, ?> task) {
//Task started
}
@Override
public void onPostExecute(Task<?, ?> task) {
//Task finished
Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
}
}
我的方法是使用委托设计模式,一般来说,我们可以在 AysncTask.doInBackground() 方法中将实际业务逻辑(从互联网或数据库或其他任何方式读取数据)从 AsyncTask (委托者)隔离到 BusinessDAO (委托者) ,将实际任务委托给BusinessDAO,然后在BusinessDAO中实现单例进程机制,这样多次调用BusinessDAO.doSomething()每次只会触发一个实际任务运行并等待任务结果。这个想法是保留代表(即BusinessDAO)在配置更改期间,而不是委托人(即。异步任务)。
创建/实现我们自己的Application,目的是在这里创建/初始化BusinessDAO,以便我们的BusinessDAO的生命周期是应用程序范围,而不是活动范围,请注意,您需要更改AndroidManifest.xml以使用MyApplication:
public class MyApplication extends android.app.Application { private BusinessDAO businessDAO; @Override public void onCreate() { super.onCreate(); businessDAO = new BusinessDAO(); } pubilc BusinessDAO getBusinessDAO() { return businessDAO; } }
我们现有的 Activity/Fragement 大部分没有变化,仍然实现 AsyncTask 作为内部类,并涉及 Activity/Fragement 中的 AsyncTask.execute() ,现在的区别是 AsyncTask 会将实际任务委托给 BusinessDAO,因此在配置更改期间,第二个 AsyncTask将被初始化并执行,并第二次调用 BusinessDAO.doSomething(),但是,第二次调用 BusinessDAO.doSomething() 不会触发新的正在运行的任务,而是等待当前正在运行的任务完成:
public class LoginFragment extends Fragment { ... ... public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> { // get a reference of BusinessDAO from application scope. BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO(); @Override protected Boolean doInBackground(String... args) { businessDAO.doSomething(); return true; } protected void onPostExecute(Boolean result) { //Handle task result and update UI stuff. } } ... ... }
BusinessDAO内部,实现单例进程机制,例如:
public class BusinessDAO { ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1)); Future<MyTask> myFutureTask = null; public void doSomething() { if (myFutureTask == null) { // nothing running at the moment, submit a new callable task to run. MyTask myTask = new MyTask(); myFutureTask = completionExecutor.submit(myTask); } // Task already submitted and running, waiting for the running task to finish. myFutureTask.get(); } // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception. private class MyTask extends Callable<MyTask> { public MyAsyncTask call() { // do your job here. return this; } } }
我不能 100% 确定这是否有效,而且,示例代码片段应被视为伪代码。我只是想从设计层面给你一些线索。欢迎并赞赏任何反馈或建议。
您可以将 AsyncTask 设置为静态字段。如果您需要上下文,您应该发送您的应用程序上下文。这将避免内存泄漏,否则您将保留对整个活动的引用。
如果有人找到了进入该线程的方法,那么我发现一个干净的方法是从 app.Service
(从 START_STICKY 开始),然后在重新创建时迭代正在运行的服务以查明服务(以及异步任务)是否仍在运行;
public boolean isServiceRunning(String serviceClassName) {
final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (RunningServiceInfo runningServiceInfo : services) {
if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
return true;
}
}
return false;
}
如果是,则重新添加 DialogFragment
(或其他),如果不能确保对话框已被关闭。
如果您使用的是 v4.support.*
图书馆因为(在撰写本文时)他们已经知道以下问题 setRetainInstance
方法和视图分页。 此外,通过不保留实例,您可以使用一组不同的资源(即新方向的不同视图布局)
我写了samepl代码来解决这个问题
第一步是创建应用程序类:
public class TheApp extends Application {
private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();
@Override
public void onCreate() {
super.onCreate();
sTheApp = this;
}
public static TheApp get() {
return sTheApp;
}
public void registerTask(String tag, AsyncTask<?,?,?> task) {
tasks.put(tag, task);
}
public void unregisterTask(String tag) {
tasks.remove(tag);
}
public AsyncTask<?,?,?> getTask(String tag) {
return tasks.get(tag);
}
}
在AndroidManifest.xml中
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name="com.example.tasktest.TheApp">
活动中的代码:
public class MainActivity extends Activity {
private Task1 mTask1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTask1 = (Task1)TheApp.get().getTask("task1");
}
/*
* start task is not running jet
*/
public void handletask1(View v) {
if (mTask1 == null) {
mTask1 = new Task1();
TheApp.get().registerTask("task1", mTask1);
mTask1.execute();
} else
Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();
}
/*
* cancel task if is not finished
*/
public void handelCancel(View v) {
if (mTask1 != null)
mTask1.cancel(false);
}
public class Task1 extends AsyncTask<Void, Void, Void>{
@Override
protected Void doInBackground(Void... params) {
try {
for(int i=0; i<120; i++) {
Thread.sleep(1000);
Log.i("tests", "loop=" + i);
if (this.isCancelled()) {
Log.e("tests", "tssk cancelled");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onCancelled(Void result) {
TheApp.get().unregisterTask("task1");
mTask1 = null;
}
@Override
protected void onPostExecute(Void result) {
TheApp.get().unregisterTask("task1");
mTask1 = null;
}
}
}
当 Activity 方向发生变化时,变量 mTask 将从应用程序上下文中初始化。任务完成后,变量设置为 null 并从内存中删除。
对我来说就足够了。
看看下面的例子,如何使用保留片段来保留后台任务:
public class NetworkRequestFragment extends Fragment {
// Declare some sort of interface that your AsyncTask will use to communicate with the Activity
public interface NetworkRequestListener {
void onRequestStarted();
void onRequestProgressUpdate(int progress);
void onRequestFinished(SomeObject result);
}
private NetworkTask mTask;
private NetworkRequestListener mListener;
private SomeObject mResult;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Try to use the Activity as a listener
if (activity instanceof NetworkRequestListener) {
mListener = (NetworkRequestListener) activity;
} else {
// You can decide if you want to mandate that the Activity implements your callback interface
// in which case you should throw an exception if it doesn't:
throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
// or you could just swallow it and allow a state where nobody is listening
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this Fragment so that it will not be destroyed when an orientation
// change happens and we can keep our AsyncTask running
setRetainInstance(true);
}
/**
* The Activity can call this when it wants to start the task
*/
public void startTask(String url) {
mTask = new NetworkTask(url);
mTask.execute();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// If the AsyncTask finished when we didn't have a listener we can
// deliver the result here
if ((mResult != null) && (mListener != null)) {
mListener.onRequestFinished(mResult);
mResult = null;
}
}
@Override
public void onDestroy() {
super.onDestroy();
// We still have to cancel the task in onDestroy because if the user exits the app or
// finishes the Activity, we don't want the task to keep running
// Since we are retaining the Fragment, onDestroy won't be called for an orientation change
// so this won't affect our ability to keep the task running when the user rotates the device
if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
mTask.cancel(true);
}
}
@Override
public void onDetach() {
super.onDetach();
// This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
// When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
// we don't want to keep any references to it
// When the Activity is being re-created, onAttach will be called and we will get our listener back
mListener = null;
}
private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {
@Override
protected void onPreExecute() {
if (mListener != null) {
mListener.onRequestStarted();
}
}
@Override
protected SomeObject doInBackground(String... urls) {
// Make the network request
...
// Whenever we want to update our progress:
publishProgress(progress);
...
return result;
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (mListener != null) {
mListener.onRequestProgressUpdate(progress[0]);
}
}
@Override
protected void onPostExecute(SomeObject result) {
if (mListener != null) {
mListener.onRequestFinished(result);
} else {
// If the task finishes while the orientation change is happening and while
// the Fragment is not attached to an Activity, our mListener might be null
// If you need to make sure that the result eventually gets to the Activity
// you could save the result here, then in onActivityCreated you can pass it back
// to the Activity
mResult = result;
}
}
}
}