屏幕旋转时如何处理AsyncTask?

88

我阅读了很多关于如何保存实例状态以及如何处理屏幕旋转时活动被销毁的内容。

看起来有很多可能性,但我还没有弄清楚哪种方法最适合检索AsyncTask的结果。

我有一些AsyncTasks只是简单地重新启动并调用活动的isFinishing()方法,如果活动正在完成,它们就不会更新任何内容。

问题在于,我有一个任务要求向Web服务发出请求,这可能成功或失败,重新启动任务将导致用户财务损失。

你会如何解决这个问题?可能的解决方案有什么优缺点?


1
请看我的回答这里。你也可以参考这篇关于setRetainInstance(true)的实际作用的信息,可能会有所帮助。 - Timmmm
我会简单地实现一个本地服务来执行您的asyncTask正在进行的处理(在线程中)。为了显示结果,将数据广播到您的活动。现在,活动只负责显示数据,处理永远不会被屏幕旋转打断。 - Someone Somewhere
使用AsyncTaskLoader代替AsyncTask怎么样? - Sourangshu Biswas
13个回答

46

你可以在code.google.com/p/shelves查看我如何处理AsyncTask和屏幕方向变化。有许多方法可以做到这一点,但我选择在此应用中取消任何当前正在运行的任务,保存其状态并在创建新的Activity时使用保存的状态启动一个新任务。这很容易做到,它运行良好,而且作为一个额外的奖励,它可以在用户离开应用程序时停止您的任务。

你也可以使用onRetainNonConfigurationInstance()将你的AsyncTask传递给新的Activity(但要注意不要通过这种方式泄漏先前的Activity)。


1
我尝试了一下,在搜索书籍时旋转会中断并给出比不旋转时更少的结果,太糟糕了。 - max4ever
1
我在那段代码中找不到任何一个AsyncTask的使用。有一个名为UserTask的类看起来很相似。这个项目是在AsyncTask之前创建的吗? - devconsole
7
AsyncTask 来源于 UserTask。我最初为自己的应用编写了 UserTask,后来将其转换为了 AsyncTask。很抱歉我忘记了它被重命名了。 - Romain Guy
@RomainGuy 你好,希望你一切都好。根据你的代码,虽然第一个任务被取消了,但仍然向服务器发送了2个请求,且未能成功取消。我不知道为什么。请问有没有解决这个问题的方法? - iamcrypticcoder

10
这是我在关于Android方面看到的最有趣的问题!!!实际上,在过去的几个月中,我已经一直在寻找解决方案。但仍未解决。
请注意,仅简单地覆盖

标签可能会破坏您的代码。
android:configChanges="keyboardHidden|orientation"

东西不够用。

考虑当用户在您的AsyncTask运行时接收电话的情况。您的请求已经由服务器处理,因此AsyncTask正在等待响应。此时,您的应用程序进入后台,因为电话应用刚刚出现在前台。操作系统可能会杀死您的活动,因为它在后台运行。


7

我的第一条建议是确保您实际上需要在屏幕旋转时重置活动(默认行为)。每当我遇到旋转问题时,我都会将此属性添加到AndroidManifest.xml文件中的<activity>标记中,并且一切正常。

android:configChanges="keyboardHidden|orientation"

这看起来很奇怪,但它实际上是将控制权交给你的onConfigurationChanged()方法。如果您没有提供此方法,它只会重新测量布局,这似乎是处理旋转的完全足够的方式。


5
但这会防止 Activity 更改布局,从而迫使用户按照您的应用程序所规定的特定方向使用设备,而不是根据他的需要。 - Janusz
77
使用这种技术可以防止您轻易使用与配置相关的资源。例如,如果您希望在纵向和横向方向上使用不同的布局、可绘制图像或字符串等内容,则应该采用默认行为。仅在非常特殊的情况下(如游戏、Web浏览器等)才应覆盖配置更改,并且不要因懒惰或方便而限制自己。 - Romain Guy
38
没错,Romain。"如果您希望横屏和竖屏时布局、绘图或者字符串等元素有所不同,您将需要默认的行为",我相信这种情况比您预想的要罕见得多。大多数开发人员都会遇到您所说的 "非常特殊的情况"。使用适用于所有维度的相对布局是最佳实践,并且并不难。谈论懒惰是非常误导人的,这些技术旨在提高用户体验而不是减少开发时间。 - Jim Blackler
2
我发现这对LinearLayout非常有效,但在使用RelativeLayout时,在切换到横向模式时无法正确重新绘制布局(至少在N1上不行)。请参见此问题:http://stackoverflow.com/questions/2987049/how-to-refresh-an-android-relativelayout-when-orientation-changes-without-restart - JohnRock
9
我同意Romain的观点(他知道自己在说什么,他开发操作系统)。当你想将应用程序移植到平板电脑时,如果你的用户界面在拉伸时看起来很糟糕,那会发生什么?如果你采用这个回答中提到的方法,你将需要重新编写整个解决方案,因为你采用了这种懒惰的方法。 - Austyn Mahoney
显示剩余11条评论

6
为什么不在Android提供的Singleton中始终保留对当前AsyncTask的引用?
每当任务开始时,在PreExecute或builder上,您都需要定义:
((Application) getApplication()).setCurrentTask(asyncTask);
每当它完成时,将其设置为null。
这样,您始终拥有一个引用,使您可以根据特定逻辑执行onCreate或onResume之类的操作:
this.asyncTaskReference = ((Application) getApplication()).getCurrentTask();
如果它为null,则表示当前没有运行任何任务!
:-)

这个会有效吗?有人测试过吗?如果发生电话中断或我们导航到新的活动然后返回,任务是否仍会被系统杀死? - Robert
6
Application实例有自己的生命周期 - 它也可能被操作系统杀死,因此这种解决方案可能会导致难以复现的错误。 - Vit Khudenko
7
我想:如果应用程序被杀死,整个应用程序都会被杀死(因此,所有的AsyncTasks也会被杀死)? - manmal
我认为应用程序可以在所有异步任务完成之前被终止(这种情况非常罕见)。但是,通过对每个异步任务的开始和结束进行一些简单的验证,@Arhimed可以避免出现错误。 - neteinstein

5
最合适的方法是使用片段(fragment)来保留异步任务实例,以便在旋转后继续使用。这里提供一个非常简单的示例链接,使您可以轻松地将此技术集成到您的应用程序中:https://gist.github.com/daichan4649/2480065

这是另一个利用保留片段的教程:http://blogactivity.wordpress.com/2011/09/01/proper-use-of-asynctask/ - devconsole

3

3
依据我的看法,最好通过onRetainNonConfigurationInstance来存储asynctask,以将其从当前Activity对象解耦,并在方向更改后将其绑定到新的Activity对象上。这里有一个非常好的例子,介绍了如何使用AsyncTask和ProgressDialog:这里

2

安卓:后台处理 / 异步操作与配置更改

为了在后台进程中维护异步操作的状态,您可以借助片段。

请参照以下步骤:

第一步:创建一个没有标题的片段,例如后台任务,并在其中添加一个私有异步任务类。

第二步(可选步骤):如果您想在活动顶部放置一个加载游标,请使用以下代码:

第三步:在主活动中实现步骤 1 中定义的 BackgroundTaskCallbacks 接口。

class BackgroundTask extends Fragment {
public BackgroundTask() {

}

// Add a static interface 

static interface BackgroundTaskCallbacks {
    void onPreExecute();

    void onCancelled();

    void onPostExecute();

    void doInBackground();
}

private BackgroundTaskCallbacks callbacks;
private PerformAsyncOpeation asyncOperation;
private boolean isRunning;
private final String TAG = BackgroundTask.class.getSimpleName();

/**
 * Start the async operation.
 */
public void start() {
    Log.d(TAG, "********* BACKGROUND TASK START OPERATION ENTER *********");
    if (!isRunning) {
        asyncOperation = new PerformAsyncOpeation();
        asyncOperation.execute();
        isRunning = true;
    }
    Log.d(TAG, "********* BACKGROUND TASK START OPERATION EXIT *********");
}

/**
 * Cancel the background task.
 */
public void cancel() {
    Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION ENTER *********");
    if (isRunning) {
        asyncOperation.cancel(false);
        asyncOperation = null;
        isRunning = false;
    }
    Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION EXIT *********");
}

/**
 * Returns the current state of the background task.
 */
public boolean isRunning() {
    return isRunning;
}

/**
 * Android passes us a reference to the newly created Activity by calling
 * this method after each configuration change.
 */
public void onAttach(Activity activity) {
    Log.d(TAG, "********* BACKGROUND TASK ON ATTACH ENTER *********");
    super.onAttach(activity);
    if (!(activity instanceof BackgroundTaskCallbacks)) {
        throw new IllegalStateException(
                "Activity must implement the LoginCallbacks interface.");
    }

    // Hold a reference to the parent Activity so we can report back the
    // task's
    // current progress and results.
    callbacks = (BackgroundTaskCallbacks) activity;
    Log.d(TAG, "********* BACKGROUND TASK ON ATTACH EXIT *********");
}

public void onCreate(Bundle savedInstanceState) {
    Log.d(TAG, "********* BACKGROUND TASK ON CREATE ENTER *********");
    super.onCreate(savedInstanceState);
    // Retain this fragment across configuration changes.
    setRetainInstance(true);
    Log.d(TAG, "********* BACKGROUND TASK ON CREATE EXIT *********");
}

public void onDetach() {
    super.onDetach();
    callbacks = null;
}

private class PerformAsyncOpeation extends AsyncTask<Void, Void, Void> {
    protected void onPreExecute() {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE ENTER *********");
        if (callbacks != null) {
            callbacks.onPreExecute();
        }
        isRunning = true;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE EXIT *********");
    }

    protected Void doInBackground(Void... params) {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND ENTER *********");
        if (callbacks != null) {
            callbacks.doInBackground();
        }
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND EXIT *********");
        return null;
    }

    protected void onCancelled() {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL ENTER *********");
        if (callbacks != null) {
            callbacks.onCancelled();
        }
        isRunning = false;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL EXIT *********");
    }

    protected void onPostExecute(Void ignore) {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE ENTER *********");
        if (callbacks != null) {
            callbacks.onPostExecute();
        }
        isRunning = false;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE EXIT *********");
    }
}

public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    setRetainInstance(true);
}

public void onStart() {
    super.onStart();
}

public void onResume() {
    super.onResume();
}

public void onPause() {
    super.onPause();
}

public void onStop() {
    super.onStop();
}

public class ProgressIndicator extends Dialog {

public ProgressIndicator(Context context, int theme) {
    super(context, theme);
}

private ProgressBar progressBar;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.progress_indicator);
    this.setCancelable(false);
    progressBar = (ProgressBar) findViewById(R.id.progressBar);
    progressBar.getIndeterminateDrawable().setColorFilter(R.color.DarkBlue, android.graphics.PorterDuff.Mode.SCREEN);
}

@Override
public void show() {
    super.show();
}

@Override
public void dismiss() {
    super.dismiss();
}

@Override
public void cancel() {
    super.cancel();
}

public class MyActivity extends FragmentActivity implements BackgroundTaskCallbacks,{

private static final String KEY_CURRENT_PROGRESS = "current_progress";

ProgressIndicator progressIndicator = null;

private final static String TAG = MyActivity.class.getSimpleName();

private BackgroundTask task = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(//"set your layout here");
    initialize your views and widget here .............



    FragmentManager fm = getSupportFragmentManager();
    task = (BackgroundTask) fm.findFragmentByTag("login");

    // If the Fragment is non-null, then it is currently being
    // retained across a configuration change.
    if (task == null) {
        task = new BackgroundTask();
        fm.beginTransaction().add(task, "login").commit();
    }

    // Restore saved state
    if (savedInstanceState != null) {
        Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON CREATE :: "
                + task.isRunning());
        if (task.isRunning()) {
            progressIndicator = new ProgressIndicator(this,
                    R.style.TransparentDialog);
            if (progressIndicator != null) {
                progressIndicator.show();
            }
        }
    }
}

@Override
protected void onPause() {
    // TODO Auto-generated method stub
    super.onPause();

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    // save the current state of your operation here by saying this 

    super.onSaveInstanceState(outState);
    Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON SAVE INSTANCE :: "
            + task.isRunning());
    outState.putBoolean(KEY_CURRENT_PROGRESS, task.isRunning());
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
    }
    progressIndicator = null;
}


private void performOperation() {

            if (!task.isRunning() && progressIndicator == null) {
                progressIndicator = new ProgressIndicator(this,
                        R.style.TransparentDialog);
                progressIndicator.show();
            }
            if (task.isRunning()) {
                task.cancel();
            } else {
                task.start();
            }
        }


@Override
protected void onDestroy() {
    super.onDestroy();
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
    }
    progressIndicator = null;
}

@Override
public void onPreExecute() {
    Log.i(TAG, "CALLING ON PRE EXECUTE");
}

@Override
public void onCancelled() {
    Log.i(TAG, "CALLING ON CANCELLED");
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();

}

public void onPostExecute() {
    Log.i(TAG, "CALLING ON POST EXECUTE");
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
        progressIndicator = null;
    }
}

@Override
public void doInBackground() {
    // put your code here for background operation
}

}


1
请看这个帖子。这篇文章涉及到在一个示例应用程序中执行长时间运行操作和屏幕旋转发生内存泄漏的AsyncTask。该示例应用程序可在source forge上获得。

1
需要考虑的一件事是,AsyncTask 的结果是否仅应该对启动任务的活动可用。如果是,则Romain Guy's answer 是最好的选择。如果它应该对您的应用程序的其他活动可用,则可以在 onPostExecute 中使用 LocalBroadcastManager
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent("finished"));

您还需要确保在活动暂停时发送广播时,活动能够正确处理该情况。


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