后台任务、进度对话框、屏幕旋转 - 是否有任何100%可行的解决方案?

236

我在后台线程中使用AsyncTask下载一些数据,并在下载时显示一个进度对话框。当屏幕方向改变、Activity被重新启动,此时我的AsyncTask已完成 - 我想关闭进度对话框并启动一个新的Activity。但是调用dismissDialog有时会抛出异常(可能是因为Activity已经被销毁,而新的Activity还没有启动)。

如何处理这种问题的最佳方法(从后台线程更新UI,即使用户改变方向也能正常工作)?Google是否提供了某种“官方解决方案”?


4
我的博客文章或许能对这个话题有所帮助。它是关于在配置更改时保留长时间运行任务的内容。 - Alex Lockwood
1
这个问题与这个问题也有关联。 - Alex Lockwood
只是提一下,这里有一个相关的谜团.. http://stackoverflow.com/q/23742412/294884 - Fattie
8个回答

338

步骤#1:将您的AsyncTask作为static嵌套类或完全单独的类,而不是内部(非静态嵌套)类。

步骤#2:通过数据成员保留Activity,并通过构造函数和setter设置。

步骤#3:在创建AsyncTask时,将当前Activity提供给构造函数。

步骤#4:在onRetainNonConfigurationInstance()中,在将其从原始的将要消失的活动中分离后,返回AsyncTask

步骤#5:在onCreate()中,如果getLastNonConfigurationInstance()不为null,则将其转换为您的AsyncTask类,并调用setter将新的活动与任务关联起来。

步骤#6:不要从doInBackground()引用活动数据成员。

如果按照上述步骤进行操作,就会顺利运行。在onRetainNonConfigurationInstance()开始和随后的onCreate()结束之间,onProgressUpdate()onPostExecute()将被暂停。

这里是一个演示该技术的示例项目

另一种方法是放弃使用 AsyncTask,并将工作移到 IntentService中。如果要执行的工作可能很长,并且无论用户在活动方面做什么(例如下载大文件),都应该继续进行,那么这种方法特别有用。您可以使用有序广播 Intent,以便活动响应正在进行的工作(如果它仍在前台),或者引发一个 Notification,让用户知道工作是否已完成。有关此模式的更多信息,请参见此博客文章

8
非常感谢您对这个常见问题的出色回答!只是为了更加详尽,您可以在第四步中添加将AsyncTask中的activity分离(设置为null)的说明。虽然这在示例项目中已经很好地说明了。 - Kevin Gaudin
3
如果我需要访问Activity的成员变量怎么办? - Eugene
11
onRetainNonConfigurationInstance()方法已被弃用,建议使用setRetainInstance()方法作为替代方案,但它不会返回对象。使用setRetainInstance()方法能否在配置更改时处理asyncTask - Indrek Kõue
10
@SYLARRR:完全正确。让“Fragment”持有“AsyncTask”。让“Fragment”对自身调用setRetainInstance(true)。让“AsyncTask”仅与“Fragment”通信。这样,在配置更改时,“Fragment”不会被销毁和重新创建(即使活动被销毁和重新创建),因此“AsyncTask”将跨越配置更改保留下来。 - CommonsWare
3
不,AsyncTaskLoader 不是推荐的方法,因为它并不完全相同。AsyncTask执行单个异步操作,而 AsyncTaskLoader 则为 Activity 和/或 Fragment 执行异步加载,跨配置更改保留已加载的数据,并在检测到数据源更改时自动执行新的加载。如果您需要执行单个、一次性、可能昂贵的操作,那么使用 AsyncTaskLoader 就没有意义了...请使用 AsyncTask 代替。 - Alex Lockwood
显示剩余24条评论

13

这个被接受的答案非常有帮助,但是它没有一个进度对话框。

幸运的是,亲爱的读者,我创建了一个非常详细且可行的带有进度对话框的AsyncTask示例

  1. 旋转正常工作,并且对话框存活。
  2. 您可以通过按下后退按钮来取消任务和对话框(如果您希望这样做)。
  3. 它使用碎片。
  4. 设备旋转时,活动下面的碎片布局会正确更改。

被接受的答案是关于静态类(而不是成员变量)。这些类 是必要的,以避免AsyncTask具有对外部类实例的(隐藏)指针,从而在销毁活动时导致内存泄漏。 - Bananeweizen
你能否更新一下链接?我真的很需要它。 - Romain Pellerin
抱歉,我还没有恢复我的网站 - 我会尽快处理!但与此同时,它基本上与此答案中的代码相同:https://dev59.com/92oy5IYBdhLWcg3wo_gn#12303649 - Timmmm
1
链接是虚假的;只会导致一个无用的索引,没有任何代码所在的指示。 - FractalBob
你能否在回答中发布博客文章的内容?外部链接作为回答的方式有点不受欢迎。 - Diti
显示剩余2条评论

9
我为了解决这个问题已经辛苦了一个星期,而且没有使用编辑清单文件的方法。此解决方案的假设如下:
  1. 您始终需要使用进度对话框
  2. 一次只执行一个任务
  3. 当手机旋转时,您需要任务持久化,进度对话框自动关闭。

实现

您需要将本帖底部找到的两个文件复制到您的工作区。请确保:

  1. 所有Activity都应该扩展BaseActivity

  2. onCreate()中,在初始化任何需要由您的ASyncTask访问的成员之后,应该调用super.onCreate()。另外,重写getContentViewId()以提供表单布局ID。

  3. 像往常一样覆盖onCreateDialog() 来创建由活动管理的对话框

  4. 有关示例静态内部类的代码,请参见下面。您可以将结果存储在mResult中以便以后访问。

final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

最后,启动您的新任务:
mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

就是这样了!我希望这个强大的解决方案能帮助到某些人。

BaseActivity.java(自己组织导入)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}

4

有谷歌的人提供了“官方解决方案”吗?

是的。

这个解决方案更像是一个应用程序架构建议,而不仅仅是一些代码。

他们提出了3种设计模式,允许应用程序与服务器同步工作,无论应用程序的状态如何(即使用户完成应用程序、用户更改屏幕、应用程序被终止,以及其他可能导致后台数据操作中断的状态,都可以正常工作)。

该提案在Virgil Dobjanschi在Google I/O 2010期间的Android REST客户端应用程序演讲中详细说明。虽然演讲时长为1小时,但非常值得观看。

其基础是将网络操作抽象到一个Service中,该服务独立于应用程序中的任何Activity。如果您正在使用数据库,则使用ContentResolverCursor可以为您提供开箱即用的Observer pattern,方便地更新UI,而无需任何其他逻辑,一旦您使用获取的远程数据更新了本地数据库。任何其他的后续操作代码都将通过传递给Service的回调来运行(我使用一个ResultReceiver子类来实现)。

无论如何,我的解释实际上相当模糊,您应该一定要观看演讲。


2
虽然Mark(CommonsWare)的答案确实适用于屏幕旋转,但如果Activity直接被销毁(比如在接电话的情况下),它就会失败。
您可以通过使用Application对象引用ASyncTask来处理方向更改和罕见的销毁Activity事件。
这里有一个关于该问题和解决方案的优秀解释:这里
完全应该归功于Ryan找出了这个问题。

1
在经过4年的时间后,Google终于解决了这个问题,只需在Activity onCreate中调用setRetainInstance(true)。这将在设备旋转期间保留您的活动实例。我还有一个简单的解决方案适用于旧版Android。

1
人们观察到的问题是因为在旋转、键盘扩展和其他事件时,Android会销毁Activity类,但异步任务仍然保留对已销毁实例的引用,并尝试使用它进行UI更新。您可以在清单或编程方式下指示Android不要销毁活动。在这种情况下,异步任务引用仍然有效,没有观察到任何问题。由于旋转可能需要一些额外的工作,如重新加载视图等,Google不建议保留Activity。所以由您决定。 - Singagirl
谢谢,我知道这种情况,但不知道setRetainInstance()。我不明白的是你声称Google使用它来解决问题。你能提供信息来源吗?谢谢。 - Redoman
http://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance%28%29 - Singagirl
onRetainNonConfigurationInstance() 这个函数仅作为优化而被调用,您不能依赖它被调用。 < 来自同一来源:https://developer.android.com/reference/android/app/Activity#onRetainNonConfigurationInstance() - Dhananjay M

0

你应该使用Activity Handler调用所有的Activity操作。因此,如果你在某个线程中,你应该创建一个Runnable并使用Activity的Handler进行发布。否则,你的应用程序有时会崩溃并出现致命异常。


0

这是我的解决方案: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

基本步骤如下:

  1. 我使用onSaveInstanceState来保存任务,如果它仍在处理中。
  2. onCreate中,如果任务已被保存,我获取该任务。
  3. onPause中,如果显示了ProgressDialog,我将其丢弃。
  4. onResume中,如果任务仍在处理中,我会显示ProgressDialog

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