我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。

我如何处理屏幕方向的变化优雅?

下面的示例代码大致匹配我的实际程序:

public class MyAct extends Activity implements Runnable {
    public ProgressDialog mProgress;

    // UI has a button that when pressed calls send

    public void send() {
         mProgress = ProgressDialog.show(this, "Please wait", 
                      "Please wait", 
                      true, true);
        Thread thread = new Thread(this);
        thread.start();
    }

    public void run() {
        Thread.sleep(10000);
        Message msg = new Message();
        mHandler.sendMessage(msg);
    }

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            mProgress.dismiss();
        }
    };
}

栈:

E/WindowManager(  244): Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244): android.view.WindowLeaked: Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244):     at android.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager(  244):     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager(  244):     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager(  244):     at android.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager(  244):     at android.app.Dialog.show(Dialog.java:212)
E/WindowManager(  244):     at android.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager(  244):     at android.app.ProgressDialog.show(ProgressDialog.java:91)
E/WindowManager(  244):     at MyAct.send(MyAct.java:294)
E/WindowManager(  244):     at MyAct$4.onClick(MyAct.java:174)
E/WindowManager(  244):     at android.view.View.performClick(View.java:2129)
E/WindowManager(  244):     at android.view.View.onTouchEvent(View.java:3543)
E/WindowManager(  244):     at android.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager(  244):     at android.view.View.dispatchTouchEvent(View.java:3198)

我已经尝试在onSaveInstanceState中取消进度对话框,但这只是防止了立即崩溃。背景线程仍在运行,UI处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。


当前回答

如果你创建了一个后台服务来完成所有繁重的工作(tcp请求/响应,解组),视图和活动可以被销毁并重新创建,而不会泄漏窗口或丢失数据。这允许Android推荐的行为,即在每次配置更改时销毁一个Activity。对于每个方向变化)。

它有点复杂,但它是调用服务器请求、数据预处理/后处理等的最佳方式。

您甚至可以使用您的服务将每个请求排队到服务器,因此它使处理这些事情变得简单和高效。

开发指南有一个关于服务的完整章节。

其他回答

这是一个非常古老的问题,出于某种原因出现在侧栏上。

如果后台任务只需要在活动处于前台时存活,“新的”解决方案是在保留的片段中托管后台线程(或者,最好是AsyncTask),如本开发指南和大量q&a中所述。

如果活动因配置更改而被销毁,则保留的片段将继续存在,但当活动在后台或后台堆栈中被销毁时则不存在。因此,如果onPause()中的isChangingConfigurations()为false,后台任务仍然应该被中断。

当你改变方向时,Android会终止那个活动并创建新的活动。 我建议使用Rx java的retrofit。自动处理崩溃。

在改造调用时使用这些方法。

.subscribeOn (Schedulers.io ()) .observeOn (AndroidSchedulers.mainThread ())

如今,有一种更加独特的方式来处理这类问题。典型的方法是:

1. 确保你的数据与用户界面正确分离:

任何后台进程都应该在一个保留的Fragment中(使用Fragment. setretaininstance()设置此值)。这就成为了你的“持久数据存储”,你想要保留的任何数据都会被保留。在方向改变事件之后,这个片段仍然可以通过FragmentManager.findFragmentByTag()调用以其原始状态访问(当你创建它时,你应该给它一个标签而不是一个ID,因为它没有附加到一个视图)。

请参阅处理运行时更改开发指南,了解如何正确执行此操作,以及为什么这是最佳选择。

2. 确保你在后台进程和你的UI之间正确和安全的接口:

You must reverse your linking process. At the moment your background process attaches itself to a View - instead your View should be attaching itself to the background process. It makes more sense right? The View's action is dependent on the background process, whereas the background process is not dependent on the View.This means changing the link to a standard Listener interface. Say your process (whatever class it is - whether it is an AsyncTask, Runnable or whatever) defines a OnProcessFinishedListener, when the process is done it should call that listener if it exists.

这个答案很好地简洁地描述了如何定制侦听器。

3.每当UI被创建(包括方向的改变)时,将你的UI链接到数据过程中:

现在你必须考虑如何将后台任务与当前View结构连接起来。如果你正确地处理了你的方向变化(而不是人们经常推荐的configChanges黑客),那么你的对话框将被系统重新创建。这很重要,它意味着在方向改变时,所有Dialog的生命周期方法都会被召回。所以在这些方法中(onCreateDialog通常是一个好地方),你可以像下面这样调用:

DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
    f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
        public void onProcessFinished() {
            dismiss();
        }
    });
 }

查看Fragment生命周期以确定侦听器的设置最适合于您的个人实现。

这是一种通用方法,用于为本问题中问到的一般问题提供健壮而完整的解决方案。根据您的具体情况,这个答案中可能还缺少一些细节,但这通常是正确处理方向更改事件的最正确方法。

我发现了一个我还没有在其他地方见过的解决方案。你可以使用一个自定义应用程序对象,它知道你是否有后台任务在进行,而不是试图在活动中做这件事,在方向改变时被销毁和重新创建。我在这里写过博客。

我有一个实现,允许活动在屏幕方向改变上被销毁,但仍然在重新创建的活动中成功地销毁对话框。 我用…NonConfigurationInstance将后台任务附加到重新创建的活动。 正常的Android框架处理重新创建对话框本身,没有任何改变。

我子类化了AsyncTask,为“拥有”活动添加了一个字段,以及一个更新这个所有者的方法。

class MyBackgroundTask extends AsyncTask<...> {
  MyBackgroundTask (Activity a, ...) {
    super();
    this.ownerActivity = a;
  }

  public void attach(Activity a) {
    ownerActivity = a;
  }

  protected void onPostExecute(Integer result) {
    super.onPostExecute(result);
    ownerActivity.dismissDialog(DIALOG_PROGRESS);
  }

  ...
}

在我的活动类中,我添加了一个字段backgroundTask引用'owned' backgroundTask,我使用onRetainNonConfigurationInstance和getLastNonConfigurationInstance更新这个字段。

class MyActivity extends Activity {
  public void onCreate(Bundle savedInstanceState) {
    ...
    if (getLastNonConfigurationInstance() != null) {
      backgroundTask = (MyBackgroundTask) getLastNonConfigurationInstance();
      backgroundTask.attach(this);
    }
  }

  void startBackgroundTask() {
    backgroundTask = new MyBackgroundTask(this, ...);
    showDialog(DIALOG_PROGRESS);
    backgroundTask.execute(...);
  }

  public Object onRetainNonConfigurationInstance() {
    if (backgroundTask != null && backgroundTask.getStatus() != Status.FINISHED)
      return backgroundTask;
    return null;
  }
  ...
}

进一步改进建议:

在任务完成后,清除活动中的backgroundTask引用以释放与之关联的所有内存或其他资源。 在活动被销毁之前,清除后台任务中的ownerActivity引用,以防它不会立即被重新创建。 创建一个BackgroundTask接口和/或集合,以允许不同类型的任务从相同的活动中运行。