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

Posted

技术标签:

【中文标题】如何在屏幕旋转期间处理 AsyncTask?【英文标题】:How to handle an AsyncTask during Screen Rotation? 【发布时间】:2011-02-06 22:24:45 【问题描述】:

我阅读了很多关于如何保存我的实例状态或如何处理我的活动在屏幕旋转期间被破坏的信息。

似乎有很多可能性,但我还没有弄清楚哪一种最适合检索 AsyncTask 的结果。

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

问题是我有一个任务向网络服务发出请求,该请求可能会失败或成功,重新启动任务会导致用户蒙受经济损失。

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

【问题讨论】:

查看我的回答here。您可能还会发现 this information about what setRetainInstance(true) actually does 很有帮助。 我要做的只是实现一个本地服务,该服务执行您的 asyncTask 正在执行的处理(在线程中)。要显示结果,请将数据广播到您的活动。现在 Activity 只负责显示数据,并且处理过程从不被屏幕旋转中断。 用 AsyncTaskLoader 代替 AsyncTask 怎么样?? 【参考方案1】:

您可以在code.google.com/p/shelves 上查看我如何处理AsyncTasks 和方向更改。有多种方法可以做到这一点,我在这个应用程序中选择的一种是取消任何当前正在运行的任务,保存其状态并在创建新的Activity 时使用保存的状态启动一个新任务。这很容易做到,效果很好,而且作为奖励,它会在用户离开应用程序时停止您的任务。

您也可以使用onRetainNonConfigurationInstance() 将您的AsyncTask 传递给新的Activity(但请注意不要以这种方式泄露之前的Activity。)

【讨论】:

我试过了,在图书搜索中断期间旋转,结果比不旋转时少,太糟糕了 我在该代码中找不到 AsyncTask 的单一用法。有一个类 UserTask 看起来很相似。这个项目是否早于 AsyncTask? AsyncTask 来自 UserTask。我最初为自己的应用程序编写了 UserTask,后来将其变成了 AsyncTask。抱歉,我忘记了它已重命名。 @RomainGuy 嗨,希望你一切都好。根据您的代码,虽然第一个任务被取消但没有成功取消,但 2 个请求被发送到服务器。我不知道为什么。请告诉我有什么办法可以解决这个问题。【参考方案2】:

这是我见过的关于 android 的最有趣的问题!!!实际上,在过去的几个月里,我一直在寻找解决方案。还是没有解决。

小心,简单地覆盖

android:configChanges="keyboardHidden|orientation"

东西还不够。

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

【讨论】:

【参考方案3】:

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

android:configChanges="keyboardHidden|orientation"

它看起来很奇怪,但是它的作用是交给您的 onConfigurationChanged() 方法,如果您不提供它,它只会重新测量布局,这似乎是一种非常合适的处理方式大部分时间都在旋转。

【讨论】:

但这会阻止 Activity 改变布局。因此会迫使用户按照您的应用程序而不是他的需要指定的特定方向使用他的设备。 使用此技术会阻止您轻松使用特定于配置的资源。例如,如果您希望您的布局或可绘制对象或字符串或其他任何东西在纵向和横向上有所不同,您将需要默认行为。覆盖配置更改应该只在非常特定的情况下(游戏、网络浏览器等)进行,而不是因为你在限制自己的懒惰或方便。 好吧,罗曼。 “如果您希望您的布局或可绘制对象或字符串或任何在纵向和横向上有所不同,您将需要默认行为”,我相信这是一个比您想象的更罕见的用例。你所说的“非常具体的案例”是我相信的大多数开发人员。使用适用于所有维度的相对布局是最佳实践,而且并不难。谈论懒惰是非常错误的,这些技术是为了改善用户体验而不是减少开发时间。 我发现这对 LinearLayout 非常有效,但是当使用 RelativeLayout 时,它在切换到横向模式时无法正确重绘布局(至少在 N1 上不是)。看到这个问题:***.com/questions/2987049/… 我同意 Romain 的观点(他知道自己在说什么,他开发了操作系统)。当您想将应用程序移植到平板电脑并且 UI 在拉伸时看起来很糟糕时会发生什么?如果您采用这个答案的方法,您将需要重新编码您的整个解决方案,因为您使用了这个懒惰的黑客。【参考方案4】:

为什么不总是在 Android 提供的 Singleton 上保留对当前 AsyncTask 的引用?

每当任务开始时,在 PreExecute 或构建器上,您定义:

((Application) getApplication()).setCurrentTask(asyncTask);

当它完成时,你将它设置为 null。

这样你总是有一个参考,允许你根据你的特定逻辑执行类似 onCreate 或 onResume 的操作:

this.asyncTaskReference = ((Application) getApplication()).getCurrentTask();

如果它为空,你就知道当前没有运行!

:-)

【讨论】:

这行得通吗?有没有人测试过这个?如果发生电话中断或我们导航到新活动然后返回,任务是否仍会被系统杀死? Application 实例有自己的生命周期——它也可以被操作系统杀死,所以这个解决方案可能会导致难以重现的错误。 我想:如果应用程序被杀死,整个应用程序也被杀死(因此,所有的 AsyncTasks 也被杀死)? 我认为可以在没有所有异步任务的情况下杀死应用程序(非常罕见)。但是@Arhimed 在每个异步任务的开始和结束时进行一些易于执行的验证,您可以避免这些错误。【参考方案5】:

最合适的方法是使用片段来保留异步任务的实例,而不是旋转。

这是一个非常简单的示例的链接,可以很容易地将此​​技术集成到您的应用程序中。

https://gist.github.com/daichan4649/2480065

【讨论】:

这是另一个使用保留片段的教程:blogactivity.wordpress.com/2011/09/01/proper-use-of-asynctask【参考方案6】:

Pro android 4。作者提出了一个不错的方法,您应该使用weak reference

Weak reference note

【讨论】:

【参考方案7】:

在我看来,最好通过onRetainNonConfigurationInstance 将它与当前的 Activity 对象解耦并在方向更改后将其绑定到新的 Activity 对象来存储 asynctask。 Here 我找到了一个非常好的示例,如何使用 AsyncTask 和 ProgressDialog。

【讨论】:

【参考方案8】:

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

在后台进程中保持异步操作的状态: 您可以借助 Fragments。

请看以下步骤:

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

第 2 步(可选步骤):如果要将加载光标放在 Activity 顶部,请使用以下代码:

第 3 步:在您的主 Activity 中实现第 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

【讨论】:

【参考方案9】:

需要考虑的一点是,AsyncTask 的结果是否应该仅对启动该任务的活动可用。如果是,那么Romain Guy's answer 是最好的。如果它应该可用于您应用程序的其他活动,那么在onPostExecute 中您可以使用LocalBroadcastManager

LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent("finished"));

您还需要确保 Activity 正确处理在 Activity 暂停时发送广播的情况。

【讨论】:

【参考方案10】:

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

上获得

【讨论】:

【参考方案11】:

我的解决方案。

在我的情况下,我有一个具有相同上下文的 AsyncTask 链。 Activity 只能访问第一个。要取消任何正在运行的任务,我执行了以下操作:

public final class TaskLoader 

private static AsyncTask task;

     private TaskLoader() 
         throw new UnsupportedOperationException();
     

     public static void setTask(AsyncTask task) 
         TaskLoader.task = task;
     

    public static void cancel() 
         TaskLoader.task.cancel(true);
     

任务doInBackground()

protected Void doInBackground(Params... params) 
    TaskLoader.setTask(this);
    ....

活动onStop()onPause()

protected void onStop() 
    super.onStop();
    TaskLoader.cancel();

【讨论】:

【参考方案12】:
@Override
protected void onSaveInstanceState(Bundle outState) 
    super.onSaveInstanceState(outState);
    final AddTask task = mAddTask;
    if (task != null && task.getStatus() != UserTask.Status.FINISHED) 
        final String bookId = task.getBookId();
        task.cancel(true);

        if (bookId != null) 
            outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
            outState.putString(STATE_ADD_BOOK, bookId);
        

        mAddTask = null;
    


@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) 
        super.onRestoreInstanceState(savedInstanceState);
    if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) 
        final String id = savedInstanceState.getString(STATE_ADD_BOOK);
        if (!BooksManager.bookExists(getContentResolver(), id)) 
            mAddTask = (AddTask) new AddTask().execute(id);
        
    

【讨论】:

【参考方案13】:

您还可以添加 android:configChanges="keyboardHidden|orientation|screenSize"

对你的清单示例我希望它有所帮助

 <application
    android:name=".AppController"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:configChanges="keyboardHidden|orientation|screenSize"
    android:theme="@style/AppTheme">

【讨论】:

以上是关于如何在屏幕旋转期间处理 AsyncTask?的主要内容,如果未能解决你的问题,请参考以下文章

Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案

Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案

Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案

AsyncTask 中代码崩溃的旋转屏幕

API 调用期间的 Android 屏幕旋转会产生 kotlinx.coroutines.JobCancellationException

防止Android上的屏幕旋转