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


86

我正在开发一个智能手机/平板电脑应用程序,仅使用一个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个用户,没有任何抱怨,因此仅将逻辑复制到新的基于片段的设计中似乎很合逻辑,但是,当然这是行不通的。

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

我不能使用,onRetainNonConfigurationInstance()因为它必须从Activity而不是Fragment中调用,这与相同getLastNonConfigurationInstance()。我在这里读了一些类似的问题,没有答案。

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

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


1
也许有关如何使用AsyncTask处理配置更改的主题可能会有所帮助
rds

将AsyncTask与应用程序生命周期相关联。.因此可以在重新创建活动时恢复
Fred Grott 2011年

查看我有关此主题的文章:使用Fragments处理配置更改
Alex Lockwood

Answers:


75

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

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


请注意,仅当您不使用后退堆栈时才使用setRetainInstance。
尼尔2012年

4
在保留的Fragment的onCreateView运行之前,AsyncTask是否有可能将其结果发送回去?
jakk 2012年

6
@jakk活动,片段等的生命周期方法由主GUI线程的消息队列顺序调用,因此,即使任务在这些生命周期方法完成(甚至被调用)之前在后台并发完成,该onPostExecute方法仍需要等待最终被主线程的消息队列处理。
Alex Lockwood

如果要为每个方向加载不同的布局文件,则此方法(RetainInstance = true)将不起作用。
贾斯汀

仅在显式用户操作启动了“工作人员”片段内的异步任务的情况下,才可以在MainActivity的onCreate方法中启动异步任务。因为主线程和用户界面都可用。但是,在启动应用程序后立即启动asynctask-如果没有用户操作(如单击按钮),则会产生异常。在这种情况下,可以在MainActivity的onStart方法中而不是onCreate方法中调用asynctask。
2016年

66

我想您会喜欢下面详述的非常全面和有效的示例。

  1. 旋转有效,对话框继续存在。
  2. 您可以通过按“后退”按钮来取消任务和对话框(如果需要此行为)。
  3. 它使用片段。
  4. 当设备旋转时,活动下方片段的布局会正确更改。
  5. 有完整的源代码下载和预编译的APK,因此您可以查看行为是否正是您想要的。

编辑

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

使用AsyncTask与进度对话框和设备旋转。

一个可行的解决方案!

我终于有了一切工作。我的代码具有以下功能:

  1. Fragment,其布局方向变化。
  2. 一个AsyncTask可以在其中做一些工作。
  3. DialogFragment,显示任务的进度条(不只是一个不确定的微调)。
  4. 轮换工作不会中断任务或关闭对话框。
  5. 后退按钮可关闭对话框并取消任务(尽管您可以相当容易地更改此行为)。

我认为在其他任何地方都找不到工作的结合。

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

另外,我们有MyTask(从扩展AsyncTask)来完成所有工作。我们无法将其存储在其中,MainFragment因为它将被销毁,并且Google已弃用了类似的名称setRetainNonInstanceConfiguration()。无论如何,这并不总是可用,充其量是一个丑陋的hack。相反,我们将存储MyTask在另一个片段中,DialogFragment称为TaskFragment这个片段setRetainInstance()设置为true,以便在设备旋转该片段不被破坏,而MyTask被保留。

最后,我们需要告诉TaskFragment谁通知完成时间,并setTargetFragment(<the MainFragment>)在创建时使用通知方式。当旋转设备并MainFragment销毁并创建新实例时,我们使用FragmentManager来查找对话框(基于其标签)并执行setTargetFragment(<the new MainFragment>)。就是这样。

我还需要做两件事:首先,在关闭对话框时取消任务,其次将关闭消息设置为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();
        }
    }
}

下载示例项目

这是源代码APK。抱歉,ADT坚持要添加支持库,然后才能进行项目。我确定您可以删除它。


4
我会避免保留进度条DialogFragment,因为它具有UI元素,其中包含对旧上下文的引用。相反,我将其存储AsyncTask在另一个空片段中,并将DialogFragment其设置为目标。
SD

旋转设备并onCreateView()再次调用时,是否会清除这些引用?mProgressBar至少旧的将被新的覆盖。
Timmmm 2012年

没有明确说明,但我对此很确定。您可以添加mProgressBar = null;onDestroyView(),如果你想成为额外的肯定。奇异的方法可能是一个好主意,但它会进一步增加代码复杂性!
Timmmm

1
您对asynctask持有的参考是progressdialog片段,对不对?所以有两个问题:1-如果我想更改称为progressdialog的实际片段,该怎么办?2-如果我想将参数传递给asynctask怎么办?此致
Maxrunner 2012年

1
@Maxrunner,对于传递参数,最简单的办法可能是对移动mTask.execute()MainFragment.onClick()。或者,您可以允许传入参数,setTask()甚至可以将其MyTask自身存储。我不确定您的第一个问题是什么意思,但也许您想使用TaskFragment.getTargetFragment()?我很确定使用可以正常使用ViewPager。但是ViewPagers并没有很好的理解或记录,祝您好运!请记住,您的片段直到第一次可见才创建。
Timmmm

16

我最近发布了一篇文章,描述了如何使用reserved来处理配置更改Fragment。它AsyncTask很好地解决了保持跨度变化的问题。

该TL; DR是使用你的主机AsyncTaskFragment,呼叫setRetainInstance(true)Fragment,并报告AsyncTask的进度/结果返回到它的Activity(或它的目标Fragment,如果你选择使用由@Timmmm描述的方法),通过保留Fragment


5
您将如何处理嵌套片段?就像AsyncTask从另一个Fragment(Tab)中的RetainedFragment开始。
雷金2014年

如果该片段已经被保留,那为什么不仅仅从保留的片段中执行异步任务呢?如果已经保留,那么即使发生配置更改,异步任务也将能够向其报告。
亚历克斯·洛克伍德

@AlexLockwood感谢您的博客。不必处理onAttachand onDetach,而如果要使用in会更好TaskFragment,我们只getActivity在需要触发回调时调用。(通过检查instaceof TaskCallbacks
Cheok Yan Cheng

1
您可以采用任何一种方式进行操作。我只是这样做了onAttach()onDetach()因此可以避免在TaskCallbacks每次要使用该活动时都不断投放该活动。
Alex Lockwood 2014年

1
@AlexLockwood如果我的应用程序遵循单个活动-多个片段设计,那么我应该为我的每个UI片段分配一个单独的任务片段吗?因此,基本上每个任务片段的生命周期将由其目标片段管理,并且不会与活动进行通信。
Manas Bajaj

13

我的第一个建议是避免使用内部AsyncTasks,您可以阅读有关此问题和答案的问题:Android:AsyncTask建议:私有类还是公共类?

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

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

每次启动AsyncTask时,都在应用程序上进行设置,完成后将其设置为null。

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

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

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

否则,在Aplication中有一个HashMap及其引用。

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


2
我同意,只要您将AsyncTask的生命周期绑定到其父项(通过将AsyncTask定义为Activity / Fragment的内部类),就很难使AsyncTask脱离其父项的生命周期,但是,我不喜欢您的解决方案,看起来太黑了。
yorkw 2011年

问题是..您有更好的解决方案吗?
neteinstein

1
我将不得不在这里与@yorkw达成共识,这是我不久前在不使用片段(基于活动的应用程序)处理该问题时向我提出的解决方案。这个问题:stackoverflow.com/questions/2620917/…具有相同的答案,我同意以下其中一项评论:“应用程序实例具有其自身的生命周期-它也可能被操作系统杀死,因此此解决方案可能会导致难以复制的错误”
blindstuff 2011年

1
正如@yorkw所说,我仍然看不到其他任何不那么“ hacky”的方式。我一直在几个应用程序中使用它,并且对可能出现的问题给予了一定的关注,它们都很好用。
neteinstein 2011年

也许@hackbod解决方案可以为您提供更多服务。
neteinstein 2011年

4

我想出了一种使用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

    }
}

这似乎可以很好地处理方向更改,并且背景任务将在旋转期间继续。

注意事项:

  1. 如果在onCreate中重新连接到asynctaskloader,则会在onLoadFinished()中使用先前的结果(即使已经告知您请求已完成)返回调用。实际上,在大多数情况下,这是一种良好的行为,但有时很难处理。虽然我想象有很多方法可以处理此问题,但是我在onLoadFinished中称为loader.abandon()。然后,我在inCreate中添加了检查功能,以便仅在尚未放弃的情况下才重新附加到装载程序。如果您再次需要结果数据,则不需要这样做。在大多数情况下,您需要数据。

我在这里有更多详细信息用于http呼叫


确定getSupportLoaderManager().getLoader(0);不会返回null(因为具有该ID 0的加载程序尚不存在)?
EmmanuelMess

1
是的,除非配置更改导致在加载程序进行过程中活动重新启动,否则它将为null。这就是为什么我要检查null的原因。
马特·沃尔夫

3

我创建了一个很小的开源后台任务库,该库很大程度上基于棉花糖,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

我的方法是使用委托设计模式,通常,我们可以在AysncTask.doInBackground()方法中将实际的业务逻辑(从Internet或数据库中读取数据)从AsyncTask(委托者)隔离到BusinessDAO(委托)。 ,将实际任务委托给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> {
        // 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.
        }
      }
    
      ... ...
    }
    
  3. 在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%是否可以使用,此外,示例代码段应视为伪代码。我只是想从设计层面上为您提供一些线索。欢迎任何反馈或建议。


似乎很不错的解决方案。自从大约两年半前您回答此问题以来,您是否进行过测试?您是在说我不确定它是否有效,事实也不是!!我正在锁定一个经过良好测试的解决方案来解决此问题。你有什么建议吗?
Alireza A. Ahmadi 2014年

1

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


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来运行Service会不会过分杀人?服务在其自己的流程中运行,而且要付出额外的代价。
WeNeigh 2012年

有趣的Vinay。还没有注意到该应用程序会占用更多的资源(无论如何目前它还是轻量级的)。你发现了什么?我将服务视为可预测的环境,无论系统处于何种UI状态,都可以通过繁重的工作或I / O使系统继续运行。与服务进行通信以查看何时完成某件事似乎是“正确的”。我开始执行的服务会在任务完成时停止,因此通常会在10到30秒钟左右存活。
BrantApps 2012年

Commonsware在这里的回答似乎暗示服务是个坏主意。我现在正在考虑AsyncTaskLoaders,但它们似乎存在自身的问题(不灵活,仅用于数据加载等)
WeNeigh 2012年

1
我懂了。值得注意的是,您链接到的服务已明确设置为在其自己的进程中运行。主人似乎不喜欢这种模式被频繁使用。我没有明确提供那些“每次都在新过程中运行”的属性,因此希望我免受那部分批评。我将努力量化影响。服务作为一个概念当然不是“坏主意”,并且对于每个应用程序执行远程有趣的事情(不是双关语)都是至关重要的。如果您仍然不确定,他们的JDoc将提供有关其用法的更多指导。
BrantApps 2012年

0

我写了samepl代码来解决这个问题

第一步是制作Application类:

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并从内存中删除。

对我来说足够了。


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

    }
}

-1

在这里看看。

有一个基于Timmmm解决方案解决方案。

但是我改进了它:

  • 现在该解决方案是可扩展的-您只需要扩展 FragmentAbleToStartTask

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

    在我看来,这就像startActivityForResult一样容易并接收结果

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

对不起我的英语不好


第一条链接已断开
Tejzeratul
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.