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

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

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

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处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。


当前回答

我也遇到了同样的问题,我想出了一个不使用ProgressDialog的解决方案,我得到了更快的结果。

我所做的是创建一个布局,其中有一个ProgressBar。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ProgressBar
    android:id="@+id/progressImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    />
</RelativeLayout>

然后在onCreate方法中执行以下操作

public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.progress);
}

然后在线程中执行长任务,完成后使用Runnable将内容视图设置为您想要用于此活动的实际布局。

例如:

mHandler.post(new Runnable(){

public void run() {
        setContentView(R.layout.my_layout);
    } 
});

这就是我所做的,我发现它比显示ProgressDialog运行得更快,在我看来,它的侵入性更小,外观更好。

然而,如果你想要使用ProgressDialog,那么这个答案不适合你。

其他回答

我也遇到了同样的问题,我想出了一个不使用ProgressDialog的解决方案,我得到了更快的结果。

我所做的是创建一个布局,其中有一个ProgressBar。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ProgressBar
    android:id="@+id/progressImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    />
</RelativeLayout>

然后在onCreate方法中执行以下操作

public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.progress);
}

然后在线程中执行长任务,完成后使用Runnable将内容视图设置为您想要用于此活动的实际布局。

例如:

mHandler.post(new Runnable(){

public void run() {
        setContentView(R.layout.my_layout);
    } 
});

这就是我所做的,我发现它比显示ProgressDialog运行得更快,在我看来,它的侵入性更小,外观更好。

然而,如果你想要使用ProgressDialog,那么这个答案不适合你。

我也遇到过同样的情况。我所做的就是在整个应用程序中只获取一个进度对话框的实例。

首先,我创建了一个DialogSingleton类来只获得一个实例(Singleton模式)

public class DialogSingleton
{
    private static Dialog dialog;

    private static final Object mLock = new Object();
    private static DialogSingleton instance;

    private DialogSingleton()
    {

    }

    public static DialogSingleton GetInstance()
    {
        synchronized (mLock)
        {
            if(instance == null)
            {
                instance = new DialogSingleton();
            }

            return instance;
        }
    }

    public void DialogShow(Context context, String title)
    {
        if(!((Activity)context).isFinishing())
        {
            dialog = new ProgressDialog(context, 2);

            dialog.setCanceledOnTouchOutside(false);

            dialog.setTitle(title);

            dialog.show();
        }
    }

    public void DialogDismiss(Context context)
    {
        if(!((Activity)context).isFinishing() && dialog.isShowing())
        {
            dialog.dismiss();
        }
    }
}

正如我在这个类中所展示的,我将进度对话框作为属性。每次我需要显示一个进度对话框时,我都会获得唯一的实例并创建一个新的ProgressDialog。

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

当我完成后台任务时,我再次调用惟一实例并关闭它的对话框。

DialogSingleton.GetInstance().DialogDismiss(this);

我将后台任务状态保存在共享首选项中。当我旋转屏幕时,我问我是否有这个活动的任务运行:(onCreate)

if(Boolean.parseBoolean(preference.GetValue(IS_TASK_NAME_EXECUTED_KEY, "boolean").toString()))
{
    DialogSingleton.GetInstance().DialogShow(this, "Checking credentials!");
} // preference object gets the info from shared preferences (my own implementation to get and put data to shared preferences) and IS_TASK_NAME_EXECUTED_KEY is the key to save this flag (flag to know if this activity has a background task already running).

当我开始运行后台任务时:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true, "boolean");

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

当我完成运行后台任务:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false, "boolean");

DialogSingleton.GetInstance().DialogDismiss(ActivityName.this);

我希望这能有所帮助。

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

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

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

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

我有一个实现,允许活动在屏幕方向改变上被销毁,但仍然在重新创建的活动中成功地销毁对话框。 我用…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接口和/或集合,以允许不同类型的任务从相同的活动中运行。

将长任务移动到单独的类中。将其实现为主题-观察者模式。每当创建活动时,注册并在关闭时注销任务类。任务类可以使用AsyncTask。