Android碎片。在屏幕旋转或配置更改期间保留AsyncTask

86

我正在开发一款智能手机/平板电脑应用程序,使用一个APK,在需要时根据屏幕大小加载资源,最佳设计选择似乎是通过ACL使用Fragment。

这个应用程序一直运行良好,只基于活动。以下是我在活动中处理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
            }
        }
    }
}

这段代码很好用,在大约1万个用户中没有出现任何问题,所以直接将此逻辑复制到新的基于碎片的设计中似乎是合理的,但是,当然,它不起作用了。

这是LoginFragment:

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
                }
            }
        }
    }
}

由于必须从Activity而不是Fragment中调用onRetainNonConfigurationInstance()getLastNonConfigurationInstance(),因此我无法使用它们。我已经阅读了一些类似的问题,但没有答案。

我理解在片段中正确组织这些内容可能需要一些解决方法,但是,我希望保持相同的基本设计逻辑。

在配置更改期间保留AsyncTask的正确方法是什么?如果它仍在运行,则显示进度对话框,考虑到AsyncTask是片段的内部类,而且是片段本身调用AsyncTask.execute()的呢?


1
也许这个关于如何使用AsyncTask处理配置更改的线程可以帮到你:https://dev59.com/inNA5IYBdhLWcg3wC5Th#4481297 - rds
将AsyncTask与应用程序生命周期关联起来,这样当活动重新创建时可以恢复。 - Fred Grott
请查看我关于此主题的文章:使用Fragment处理配置更改 - Alex Lockwood
12个回答

75
片段可以使这变得更加容易。只需使用方法Fragment.setRetainInstance(boolean),让您的片段实例在配置更改时保留。请注意,文档中建议使用此方法替换Activity.onRetainnonConfigurationInstance()
如果由于某种原因您真的不想使用保留的片段,则可以采取其他方法。请注意,每个片段都有一个唯一的标识符,可通过Fragment.getId()返回。您还可以通过Fragment.getActivity().isChangingConfigurations()找出是否正在为配置更改而拆除片段。因此,在您决定停止AsyncTask的位置(最可能是在onStop()或onDestroy()中),例如,您可以检查配置是否正在更改,如果是,则将其置于静态SparseArray下的片段标识符,并且在您的onCreate()或onStart()中查看是否有可用的AsyncTask。

请注意,setRetainInstance仅适用于您不使用返回堆栈的情况。 - Neil
4
AsyncTask会在保留的Fragment的onCreateView方法运行之前发送其结果,这种情况有可能发生吗? - jakk
6
@jakk Activity和Fragment等的生命周期方法是通过主GUI线程的消息队列按顺序调用的,因此即使任务在这些生命周期方法完成(甚至调用)之前在后台并发完成,onPostExecute方法仍需要等待最终由主线程的消息队列处理。 - Alex Lockwood
如果您想为每个方向加载不同的布局文件,则此方法(RetainInstance = true)将无法使用。 - Justin
在MainActivity的onCreate方法中启动asynctask似乎只有在“worker”片段内的asynctask通过显式用户操作启动时才能正常工作。因为主线程和用户界面是可用的。然而,在启动应用程序后立即启动asynctask - 没有像按钮单击这样的用户操作 - 会导致异常。在这种情况下,asynctask可以在MainActivity的onStart方法中调用,而不是在onCreate方法中调用。 - ʕ ᵔᴥᵔ ʔ
@Justin:在那种情况下,为什么这个解决方案不起作用呢?在设备旋转时,活动会被销毁并创建一个新的活动。如果您使用工作片段解决方案(http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html),则新创建的活动实现了工作片段,并连接到其中的异步任务。这不依赖于布局。 - ʕ ᵔᴥᵔ ʔ

66
我认为您会喜欢下面详细的工作实例:
  1. 旋转功能正常,对话框不会消失。
  2. 您可以通过按下返回按钮来取消任务和对话框(如果您想要此行为)。
  3. 它使用了片段。
  4. 设备旋转时,活动下方的片段布局会正确更改。
  5. 有完整的源代码下载和已编译的APK,因此您可以查看行为是否符合您的要求。

编辑

根据Brad Larson的要求,我在下面复制了大部分链接的解决方案。自从我发布它以来,我被指向了AsyncTaskLoader。我不确定它是否完全适用于相同的问题,但无论如何,您都应该检查一下。

在进度对话框和设备旋转中使用AsyncTask

一个可用的解决方案!

我最终让所有东西都能够正常工作。我的代码具有以下功能:

  1. 一个Fragment,其布局随方向变化而变化。
  2. 您可以在其中执行一些工作的AsyncTask
  3. 一个DialogFragment,其中以进度条的形式显示任务的进度(不仅仅是不定期的旋转器)。
  4. 旋转不会中断任务或关闭对话框。
  5. 返回按钮会关闭对话框并取消任务(您可以相对容易地更改此行为)。

我认为这种可行性的组合在任何其他地方都找不到。

基本思想如下。有一个包含单个片段MainFragmentMainActivity类。 MainFragment在水平和垂直方向上具有不同的布局,并且setRetainInstance()为false,以便布局可以更改。这意味着当设备方向更改时,MainActivityMainFragment都将被完全销毁并重新创建。

分别有MyTask(从AsyncTask扩展)执行所有工作。我们无法将其存储在MainFragment中,因为那将被销毁,而Google已经停用了使用任何类似于setRetainNonInstanceConfiguration()的东西。无论如何,它并不总是可用的,最多只是一种丑陋的黑客。相反,我们将MyTask存储在另一个片段中,名为TaskFragmentDialogFragment中。该片段的setRetainInstance()设置为true,因此随着设备旋转,该片段不会被销毁,并且MyTask会被保留。

最后我们需要告诉TaskFragment在完成任务时通知谁,我们使用setTargetFragment(<the MainFragment>)来创建它。当设备旋转并且MainFragment被销毁并创建一个新实例时,我们使用FragmentManager根据标签查找对话框,并执行setTargetFragment(<the new MainFragment>)。就这样。

我还需要做两件事:首先,在对话框关闭时取消任务,其次将关闭消息设置为null,否则在设备旋转时对话框会出现奇怪的关闭。

代码

我不会列出布局,它们非常明显,您可以在下面的项目下载中找到它们。

MainActivity

这很简单。我在这个活动中添加了一个回调,以便它知道何时完成任务,但您可能不需要那个。主要是我想展示片段-活动回调机制,因为它非常整洁,您可能以前没有见过。

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? 
    }
}

MainFragment

这可能很长,但一定值得!

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();
        }
    }

TaskFragment

    // 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();
        }
    }
}

下载示例项目

这里是源代码APK安装包的下载链接。很抱歉,由于ADT(Android开发工具包)要求添加支持库才能创建项目,因此我已经将其添加进来了。不过您可以轻松地移除它。


4
我建议不要继续使用进度条DialogFragment,因为它包含了UI元素,会持有对旧上下文的引用。相反,我会将AsyncTask存储在另一个空的fragment中,并将DialogFragment设置为其目标。 - S.D.
1
您手中的asynctask引用是progressdialog片段,对吗?因此有两个问题: 1- 如果我想更改调用progressdialog的实际片段怎么办; 2- 如果我要向asynctask传递参数怎么办?问候, - Maxrunner
1
@Maxrunner,传递参数最简单的方法可能是将mTask.execute()移动到MainFragment.onClick()中。或者你可以允许在setTask()中传递参数,甚至将它们存储在MyTask本身中。我不确定你的第一个问题是什么意思,但也许你想使用TaskFragment.getTargetFragment()?我非常确定它会在使用ViewPager时起作用。但是,ViewPagers并没有被很好地理解或记录,所以祝你好运!请记住,直到第一次可见时,你的片段才会被创建。 - Timmmm
链接已经消失了。不过,这里提供的代码对我来说是有效的。我相信这是解决问题最全面的方法。哦,我改变了一行 - 我是通过这种方式获取Manager的:mFM = getActivity().getSupportFragmentManager() - Rekin
抱歉,网站很快就会恢复,但这实际上是最新的代码,所以您并没有错过任何东西! - Timmmm
显示剩余15条评论

16
我最近发表了一篇文章,介绍如何使用保留的Fragment处理配置更改。它能很好地解决在旋转屏幕时保留AsyncTask的问题。
简而言之,将您的AsyncTask放在Fragment中,调用setRetainInstance(true)方法,将AsyncTask的进度/结果报告给其所属的Activity(或目标Fragment,如果您选择使用@Timmmm所描述的方法)通过保留的Fragment传递。

5
你如何处理嵌套的Fragment?就像在另一个Fragment(选项卡)中的RetainedFragment启动的AsyncTask一样。 - Rekin
@AlexLockwood 感谢您的博客。我们是否可以在 TaskFragment 中直接调用 getActivity 来触发回调,而不是处理 onAttachonDetach 方法呢?(通过检查 TaskCallbacks 的实例) - Cheok Yan Cheng
@CheokYanCheng 这样做的好处是什么?听起来就像是同样的事情...只是代码行数更少而已。 :) - Alex Lockwood
1
你可以任选一种方式。我只是在 onAttach()onDetach() 中这样做,这样我就可以避免每次想要使用它时都将活动转换为 TaskCallbacks - Alex Lockwood
1
@AlexLockwood 如果我的应用程序遵循单个活动-多个片段的设计,我是否应该为每个UI片段设置单独的任务片段?因此,每个任务片段的生命周期将由其目标片段管理,并且不会与活动进行通信。 - Manas Bajaj
显示剩余3条评论

13

我的第一个建议是避免使用内部AsyncTasks,您可以阅读我在StackOverflow上提出的问题以及其中的答案:Android: AsyncTask recommendations: private class or public class?

之后我开始使用非内部AsyncTask...现在我看到了很多好处。

第二个建议是,在Application类中保持正在运行的AsyncTask的引用- http://developer.android.com/reference/android/app/Application.html

每次启动AsyncTask时,请将其设置为应用程序,并在完成后将其设置为空。

当片段/活动启动时,您可以检查任何正在运行的AsyncTask(通过在Application上检查它是否为null或不为null),然后将内部引用设置为您想要的任何内容(活动、片段等),以便您可以执行回调操作。

这将解决您的问题: 如果您在任何确定时间只有1个正在运行的AsyncTask,则可以添加一个简单的引用:

AsyncTask<?,?,?> asyncTask = null;

否则,在应用程序中使用一个HashMap来引用它们。

进度对话框可以遵循完全相同的原理。


2
只要您将AsyncTask的生命周期绑定到其父级(通过将AsyncTask定义为Activity / Fragment的内部类),我同意这个方案,这样很难使AsyncTask逃脱其父级的生命周期重建。然而,我不喜欢您的解决方案,它看起来很不专业。 - yorkw
问题是...你有更好的解决方案吗? - neteinstein
1
我必须同意 @yorkw 的观点,当我处理这个问题时,有人向我提供了这个解决方案而没有使用片段(基于活动的应用程序)。这个问题:https://dev59.com/bHE85IYBdhLWcg3wwWet 有相同的答案,我同意其中一个评论说“应用实例有自己的生命周期-它也可以被操作系统杀死,因此这个解决方案可能会导致一个难以复现的错误”。 - blindstuff
1
我仍然没有看到其他比@yorkw说的更少“hacky”的方法。我在几个应用程序中使用它,并且注意可能出现的问题,一切都运行良好。 - neteinstein
也许@hackbod的解决方案更适合您。 - neteinstein
显示剩余5条评论

4
我想到了一种使用AsyncTaskLoader的方法来解决这个问题。它非常易于使用,而且在我看来需要较少的开销。
基本上,您可以像这样创建一个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

    }
}

这似乎很好地处理了方向变化,您的后台任务将在旋转期间继续运行。

需要注意以下几点:

  1. 如果在onCreate中重新附加到asynctaskloader,则会在onLoadFinished()中收到先前的结果(即使已告知请求已完成)。大多数情况下,这实际上是良好的行为,但有时可能会很棘手。虽然我想象有很多方法可以处理这个问题,但我所做的是在onLoadFinished中调用loader.abandon()。然后我在onCreate中添加了检查,只有在loader没有被放弃时才重新附加到loader。如果您需要再次使用结果数据,则不希望这样做。在大多数情况下,您需要数据。

我在此处提供了有关使用此功能进行http调用的更多详细信息:here


你确定 getSupportLoaderManager().getLoader(0); 不会返回 null 吗(因为该 id 为 0 的 loader 还不存在)? - EmmanuelMess
1
是的,除非在加载器正在进行时发生配置更改导致活动重新启动,否则它将为空。这就是为什么我要检查 null 的原因。 - Matt Wolfe

3
我创建了一个非常小的开源后台任务库,它在很大程度上基于Marshmallow的AsyncTask,但具有其他功能,例如:
  1. 自动保留跨配置更改的任务;
  2. UI回调(侦听器);
  3. 设备旋转时不会重新启动或取消任务(就像加载器会做的那样);
该库内部使用一个没有用户界面的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();
    }
}

1

你可以将AsyncTask设置为静态字段。如果需要上下文,应该使用应用程序上下文。这样可以避免内存泄漏,否则你会保留对整个活动的引用。


1

我的方法是使用委托设计模式,通常情况下,我们可以将实际的业务逻辑(从互联网或数据库中读取数据等)与AsyncTask(委托者)隔离到BusinessDAO(代表者)中,在您的AysncTask.doInBackground()方法中,将实际任务委托给BusinessDAO,然后在BusinessDAO中实现单例进程机制,这样多次调用BusinessDAO.doSomething()将只触发一次实际任务运行,并等待任务结果。这个想法是保留代表(即BusinessDAO)在配置更改期间,而不是委托者(即AsyncTask)。

  1. 创建/实现我们自己的应用程序,目的是在此处创建/初始化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;
      }
    
    }
    
  2. 我们现有的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> {
        // 从应用程序范围获取BusinessDAO的引用。
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //处理任务结果并更新UI内容。
        }
      }
    
      ... ...
    }
    
  3. 在BusinessDAO内部,实现单例进程机制,例如:

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          //当前没有运行任何任务,提交一个新的可调用任务来运行。
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        //任务已经提交并正在运行,等待正在运行的任务完成。
        myFutureTask.get();
      }
    
      //如果您以前从未使用过它,Callable类似于Runnable,具有返回结果和抛出异常的能力。
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          //在此处执行您的工作。
          return this;
        }
      }
    
    }
    

我不确定这是否有效,此外,示例代码片段应被视为伪代码。我只是试图从设计层面给你一些线索。欢迎并感谢任何反馈或建议。


看起来是个不错的解决方案。由于您在2年半前回答了这个问题,您是否测试过它?!您说我不确定它是否有效,事实上我也不确定!!我正在寻找一个经过充分测试的解决方案来解决这个问题。您有什么建议吗? - Alireza Ahmadi

1
如果有人找到了这个帖子,那么我发现一个干净的方法是从一个app.Service(使用START_STICKY启动)运行Async任务,然后在重新创建时迭代运行中的服务,以找出服务(因此异步任务)是否仍在运行;
    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方法和视图分页存在已知问题。此外,通过不保留实例,您可以使用不同的资源集(即新方向的不同视图布局)重新创建活动。


运行一个服务来保留一个AsyncTask是否有些过度了?服务在其自己的进程中运行,这并不是没有额外成本的。 - WeNeigh
有趣的Vinay。我没有注意到这个应用程序会更加占用资源(目前它已经非常轻量级了)。你找到了什么?我认为服务是一个可预测的环境,让系统在不考虑UI状态的情况下进行一些重型工作或I/O操作。与服务通信以查看何时完成某些操作似乎是“正确”的。我启动的服务执行一些工作后会在任务完成时停止,因此通常会存活10-30秒左右。 - BrantApps
Commonsware在这里的回答似乎表明了Services不是一个好主意。我现在正在考虑使用AsyncTaskLoaders,但它们似乎有自己的问题(缺乏灵活性,仅适用于数据加载等)。 - WeNeigh
1
我明白了。显然,你提供的这个服务是明确地被设置为在自己的进程中运行的。主程序似乎不喜欢经常使用这种模式。我没有明确提供那些“每次都在新进程中运行”的属性,所以希望我可以免受那部分批评的影响。我将努力量化影响。当然,作为一个概念,服务并不是“坏主意”,而且对于任何做任何有趣事情的应用程序来说都是基本的。如果您仍然不确定,他们的JDoc提供了更多关于它们使用的指导。 - BrantApps

0
请看下面的示例,如何使用保留片段来保留后台任务:
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;
            }
        }

    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接