安卓片段。在屏幕旋转或配置更改期间保留 AsyncTask

Posted

技术标签:

【中文标题】安卓片段。在屏幕旋转或配置更改期间保留 AsyncTask【英文标题】:Android Fragments. Retaining an AsyncTask during screen rotation or configuration change 【发布时间】:2012-01-15 02:47:01 【问题描述】:

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

这个应用程序一直运行良好,直到现在只是基于活动。这是我如何在活动中处理 AsyncTasks 和 ProgressDialogs 的模拟类,以便即使在屏幕旋转或在通信过程中发生配置更改时它们也能正常工作。

我不会更改清单以避免重新创建 Activity,我不想这样做的原因有很多,但主要是因为官方文档说它不推荐并且我到目前为止没有它,所以请不要推荐那条路线。

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(),因为它必须从活动而不是片段中调用,getLastNonConfigurationInstance() 也是如此。我在这里读过一些类似的问题,但没有答案。

我知道可能需要一些工作才能将这些东西正确组织成片段,也就是说,我想保持相同的基本设计逻辑。

在配置更改期间保留 AsyncTask 的正确方法是什么,如果它仍在运行,则显示一个 progressDialog,考虑到 AsyncTask 是 Fragment 的内部类并且它是 Fragment 本身谁调用了 AsyncTask.execute()?

【问题讨论】:

也许how to handle configuration change with AsyncTask 上的帖子可以提供帮助 将 AsyncTask 与应用程序生命周期相关联。因此可以在重新创建活动时恢复 查看我关于此主题的帖子:Handling Configuration Changes with Fragments 【参考方案1】:

片段实际上可以使这变得容易得多。只需使用方法Fragment.setRetainInstance(boolean) 让您的片段实例在配置更改时保留。请注意,这是文档中推荐的 Activity.onRetainnonConfigurationInstance() 替换。

如果出于某种原因您真的不想使用保留片段,您可以采取其他方法。请注意,每个片段都有一个由Fragment.getId() 返回的唯一标识符。您还可以通过Fragment.getActivity().isChangingConfigurations() 了解是否正在拆除片段以进行配置更改。因此,当您决定停止 AsyncTask(最有可能在 onStop() 或 onDestroy() 中)时,您可以检查配置是否正在更改,如果是这样,请将其粘贴在片段标识符下的静态 SparseArray 中,然后在 onCreate() 或 onStart() 中查看稀疏数组中是否有可用的 AsyncTask。

【讨论】:

请注意,setRetainInstance 仅在您不使用后退堆栈时使用。 在保留的Fragment的onCreateView运行之前,AsyncTask是否有可能将其结果发回? @jakk Activity、Fragments 等的生命周期方法是由主 GUI 线程的消息队列顺序调用的,所以即使任务在这些生命周期方法完成(甚至被调用)之前在后台并发完成),onPostExecute 方法在最终被主线程的消息队列处理之前仍然需要等待。 如果您想为每个方向加载不同的布局文件,这种方法 (RetainInstance = true) 将不起作用。 在 MainActivity 的 onCreate 方法中启动 asynctask 似乎只有在 asynctask(在“worker”片段内)由显式用户操作启动时才有效。因为主线程和用户界面都可用。但是,在启动应用程序后立即启动 asynctask - 没有像按钮单击这样的用户操作 - 会出现异常。在这种情况下,可以在 MainActivity 的 onStart 方法中调用 asynctask,而不是在 onCreate 方法中。【参考方案2】:

我想你会喜欢我下面详细介绍的极其全面和有效的示例。

    旋转有效,对话框继续存在。 您可以通过按后退按钮取消任务和对话框(如果您想要这种行为)。 它使用片段。 当设备旋转时,Activity 下方的 Fragment 布局会正确更改。 有完整的源代码下载和a precompiled APK,所以您可以查看行为是否符合您的要求。

编辑

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

AsyncTask 与进度对话框和设备轮换一起使用。

一个可行的解决方案!

我终于把一切都搞定了。我的代码有以下特点:

    Fragment,其布局随方向变化。 AsyncTask,您可以在其中做一些工作。 DialogFragment 在进度条中显示任务的进度(不仅仅是不确定的微调器)。 旋转不会中断任务或关闭对话框。 后退按钮关闭对话框并取消任务(不过您可以相当轻松地更改此行为)。

我认为在其他任何地方都找不到这种工作能力的组合。

基本思路如下。有一个 MainActivity 类,它包含一个片段 - MainFragmentMainFragment 水平和垂直方向有不同的布局,setRetainInstance() 为 false 以便布局可以改变。这意味着当设备方向改变时,MainActivityMainFragment 都被完全销毁并重新创建。

另外,我们有MyTask(从AsyncTask 扩展)来完成所有工作。我们不能将它存储在MainFragment 中,因为它会被销毁,而且谷歌已经弃用了像setRetainNonInstanceConfiguration() 这样的东西。无论如何,这并不总是可用的,充其量只是一个丑陋的黑客。相反,我们将MyTask 存储在另一个片段中,一个名为TaskFragmentDialogFragment这个片段setRetainInstance()设置为true,所以当设备旋转时这个片段不会被破坏,而MyTask会被保留。

最后,我们需要告诉TaskFragment 在完成时通知谁,我们在创建它时使用setTargetFragment(&lt;the MainFragment&gt;) 来做到这一点。当设备旋转并且MainFragment 被销毁并创建一个新实例时,我们使用FragmentManager 找到对话框(基于其标签)并执行setTargetFragment(&lt;the new MainFragment&gt;)。差不多就这些了。

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

代码

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

主要活动

这很简单。我在此活动中添加了一个回调,以便它知道任务何时完成,但您可能不需要它。主要是想展示一下fragment-activity的回调机制,因为它很简洁,你可能没见过。

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

下载示例项目

这是source code 和the APK。抱歉,ADT 坚持要添加支持库才让我做项目。我确定你可以删除它。

【讨论】:

我会避免保留进度条 DialogFragment ,因为它的 UI 元素包含对旧上下文的引用。相反,我会将AsyncTask 存储在另一个空片段中,并将DialogFragment 设置为其目标。 当设备旋转并再次调用onCreateView() 时,这些引用不会被清除吗?旧的mProgressBar 至少会被新的覆盖。 没有明确表示,但我很确定。如果您想更加确定,可以在onDestroyView() 中添加mProgressBar = null;。 Singularity 的方法可能是个好主意,但它会更加增加代码复杂度! 你在 asynctask 上的引用是 progressdialog 片段,对吧?所以2个问题:1-如果我想改变调用progressdialog的真实片段怎么办? 2-如果我想将参数传递给异步任务怎么办?问候, @Maxrunner,对于传递参数,最简单的可能是将mTask.execute()移动到MainFragment.onClick()。或者,您可以允许在setTask() 中传递参数,甚至将它们存储在MyTask 本身中。我不确定您的第一个问题是什么意思,但也许您想使用TaskFragment.getTargetFragment()?我很确定它可以使用ViewPager。但是ViewPagers 并没有得到很好的理解或记录,祝你好运!请记住,您的片段只有在第一次可见时才会创建。【参考方案3】:

我最近posted an article 描述了如何使用保留的Fragments 来处理配置更改。它很好地解决了在旋转更改中保留AsyncTask 的问题。

TL;DR 是在Fragment 中使用您的AsyncTask,在Fragment 上调用setRetainInstance(true),并将AsyncTask 的进度/结果报告给Activity(或它是目标Fragment,如果您选择使用@Timmmm 描述的方法)通过保留的Fragment

【讨论】:

你会如何处理嵌套的片段?就像从另一个 Fragment (Tab) 中的 RetainedFragment 开始的 AsyncTask。 如果片段已经被保留,那么为什么不从保留的片段中执行异步任务呢?如果它已被保留,那么即使发生配置更改,异步任务也将能够向其报告。 @AlexLockwood 感谢您的博客。不必处理onAttachonDetach,如果在TaskFragment 内部会更好,我们只需在需要触发回调时调用getActivity。 (通过检查 instaceof TaskCallbacks 无论哪种方式都可以。我只是在onAttach()onDetach() 中完成了它,这样我就可以避免每次我想使用它时不断地将活动投射到TaskCallbacks @AlexLockwood 如果我的应用程序遵循单个活动 - 多个片段设计,我是否应该为每个 UI 片段设置一个单独的任务片段?所以基本上每个任务片段的生命周期都由其目标片段管理,不会与活动进行通信。【参考方案4】:

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

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

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

每次启动 AsyncTask 时,将其设置在 Application 上,完成后将其设置为 null。

当片段/活动启动时,您可以检查是否有任何 AsyncTask 正在运行(通过检查它是否在应用程序上为空),然后将内部引用设置为您想要的任何内容(活动、片段等,以便您可以进行回调) .

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

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

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

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

【讨论】:

我同意,只要你将 AsyncTask 的生命周期绑定到它的父级(通过将 AsyncTask 定义为 Activity/Fragment 的内部类),就很难让你的 AsyncTask 脱离其父级的生命周期重新创建,但是,我不喜欢你的解决方案,它看起来太老套了。 问题是..你有更好的解决方案吗? 我将不得不同意@yorkw 的观点,这个解决方案是我不久前在不使用片段(基于活动的应用程序)处理这个问题时向我提出的。这个问题:***.com/questions/2620917/… 有相同的答案,我同意其中一个 cmet 说“应用程序实例有自己的生命周期 - 它也可以被操作系统杀死,所以这个解决方案可能会导致难以重现的错误" 我仍然没有看到像@yorkw 所说的那样“hacky”的任何其他方式。我一直在几个应用程序中使用它,并注意可能出现的问题,一切都很好。 也许@hackbod 解决方案更适合您。【参考方案5】:

为此,我想出了一个使用 AsyncTaskLoaders 的方法。它非常易于使用,并且需要更少的 IMO 开销..

基本上你会像这样创建一个 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 调用 here

的详细信息

【讨论】:

确定getSupportLoaderManager().getLoader(0); 不会返回null(因为id 为0 的加载器还不存在)? 是的,除非配置更改导致活动在加载程序正在进行时重新启动,否则它将为 null。这就是我检查 null 的原因。【参考方案6】:

我创建了一个非常小的开源后台任务库,它主要基于 Marshmallow AsyncTask,但具有以下附加功能:

    跨配置更改自动保留任务; UI 回调(监听器); 在设备旋转时不会重新启动或取消任务(就像装载机那样);

库在内部使用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();
    

【讨论】:

【参考方案7】:

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

    创建/实现我们自己的Application,目的是在这里创建/初始化BusinessDAO,使我们的BusinessDAO的生命周期是application scoped,而不是activity scoped,注意需要修改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% 确定这是否可行,此外,示例代码 sn-p 应被视为伪代码。我只是想从设计层面给你一些线索。欢迎和赞赏任何反馈或建议。

【讨论】:

似乎非常好的解决方案。自从您在大约 2 年半前回答这个问题以来,您是否对其进行了测试?!你是说我不确定它是否有效,事实是我也没有!我正在为这个问题锁定一个经过良好测试的解决方案。你有什么建议吗?【参考方案8】:

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

【讨论】:

【参考方案9】:

如果有人找到了进入该线程的方法,那么我发现一个干净的方法是从 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 方法和视图分页的问题。此外,通过不保留您可以使用一组不同的资源(即新方向的不同视图布局)重新创建活动的实例

【讨论】:

仅仅为了保留一个AsyncTask而运行一个Service是不是有点矫枉过正?服务在自己的进程中运行,而这并非没有额外的成本。 有趣的维奈。没有注意到该应用程序的资源更加繁重(目前它非常轻量级)。你发现了什么?我将服务视为一个可预测的环境,让系统无论 UI 状态如何都可以进行一些繁重的工作或 I/O。与服务进行通信以查看某事何时完成似乎是“正确的”。我开始执行一些工作的服务在任务完成时停止,因此通常会在 10-30 秒左右存活。 Commonsware 的回答here 似乎暗示服务是个坏主意。我现在正在考虑 AsyncTaskLoaders,但它们似乎有自己的问题(不灵活,仅用于数据加载等) 我明白了。值得注意的是,您链接到的此服务已明确设置为在其自己的进程中运行。大师似乎不喜欢这种经常使用的模式。我没有明确提供那些“每次都在新进程中运行”的属性,所以希望我不会受到批评。我将努力量化效果。服务作为一个概念当然不是“坏主意”,它是每个应用程序做任何远程有趣的事情的基础,没有双关语的意思。如果您仍然不确定,他们的 JDoc 会提供更多关于其使用的指导。【参考方案10】:

我写了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;
    



当活动方向改变时,变量 mTask 是从应用上下文中初始化的。当任务完成时,变量设置为 null 并从内存中删除。

对我来说已经足够了。

【讨论】:

【参考方案11】:

看看下面的例子,如何使用保留片段保留后台任务:

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;
            
        

    

【讨论】:

【参考方案12】:

看看here。

有一个基于Timmmm's的解决方案。

但我改进了它:

现在解决方案是可扩展的——你只需要扩展FragmentAbleToStartTask

您可以同时运行多个任务。

在我看来,它就像 startActivityForResult 和接收结果一样简单

您还可以停止正在运行的任务并检查特定任务是否正在运行

对不起我的英语

【讨论】:

第一个链接坏了

以上是关于安卓片段。在屏幕旋转或配置更改期间保留 AsyncTask的主要内容,如果未能解决你的问题,请参考以下文章

屏幕方向期间片段内的异步任务

在recyclerview片段Reandroid中屏幕旋转后进度条不显示

后堆栈中的配置更改片段现在正在共享 FrameLayout?

旋转时在 TextView 中保留文本

Android 片段不保存状态,在旋转/屏幕锁定/返回时崩溃

如何在屏幕旋转期间处理 AsyncTask?