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

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

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

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


当前回答

当你切换方向时,Android会创建一个新的View。你可能会崩溃,因为你的后台线程试图改变旧线程的状态。(它也可能有麻烦,因为你的后台线程不在UI线程上)

我建议将mHandler设为volatile,并在方向改变时更新它。

其他回答

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

我尝试执行jfelectron的解决方案,因为它是“解决这些问题的坚如磐石的解决方案,符合‘Android方式’”,但我花了一些时间来查找并整合所有提到的元素。最终得到了这个略有不同的,我认为更优雅的解决方案,完整地贴在这里。

使用从活动中触发的IntentService在单独的线程上执行长时间运行的任务。该服务向更新对话框的活动发出sticky Broadcast intent。Activity使用showDialog(), onCreateDialog()和onPrepareDialog()来消除在应用程序对象或savedInstanceState包中传递持久化数据的需要。无论应用程序如何中断,这都应该工作。

活动类:

public class TesterActivity extends Activity {
private ProgressDialog mProgressDialog;
private static final int PROGRESS_DIALOG = 0;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button b = (Button) this.findViewById(R.id.test_button);
    b.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            buttonClick();
        }
    });
}

private void buttonClick(){
    clearPriorBroadcast();
    showDialog(PROGRESS_DIALOG);
    Intent svc = new Intent(this, MyService.class);
    startService(svc);
}

protected Dialog onCreateDialog(int id) {
    switch(id) {
    case PROGRESS_DIALOG:
        mProgressDialog = new ProgressDialog(TesterActivity.this);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mProgressDialog.setMax(MyService.MAX_COUNTER);
        mProgressDialog.setMessage("Processing...");
        return mProgressDialog;
    default:
        return null;
    }
}

@Override
protected void onPrepareDialog(int id, Dialog dialog) {
    switch(id) {
    case PROGRESS_DIALOG:
        // setup a broadcast receiver to receive update events from the long running process
        IntentFilter filter = new IntentFilter();
        filter.addAction(MyService.BG_PROCESS_INTENT);
        registerReceiver(new MyBroadcastReceiver(), filter);
        break;
    }
}

public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.hasExtra(MyService.KEY_COUNTER)){
            int count = intent.getIntExtra(MyService.KEY_COUNTER, 0);
            mProgressDialog.setProgress(count);
            if (count >= MyService.MAX_COUNTER){
                dismissDialog(PROGRESS_DIALOG);
            }
        }
    }
}

/*
 * Sticky broadcasts persist and any prior broadcast will trigger in the 
 * broadcast receiver as soon as it is registered.
 * To clear any prior broadcast this code sends a blank broadcast to clear 
 * the last sticky broadcast.
 * This broadcast has no extras it will be ignored in the broadcast receiver 
 * setup in onPrepareDialog()
 */
private void clearPriorBroadcast(){
    Intent broadcastIntent = new Intent();
    broadcastIntent.setAction(MyService.BG_PROCESS_INTENT);
    sendStickyBroadcast(broadcastIntent);
}}

IntentService经济舱:

public class MyService extends IntentService {

public static final String BG_PROCESS_INTENT = "com.mindspiker.Tester.MyService.TEST";
public static final String KEY_COUNTER = "counter";
public static final int MAX_COUNTER = 100;

public MyService() {
  super("");
}

@Override
protected void onHandleIntent(Intent intent) {
    for (int i = 0; i <= MAX_COUNTER; i++) {
        Log.e("Service Example", " " + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(BG_PROCESS_INTENT);
        broadcastIntent.putExtra(KEY_COUNTER, i);
        sendStickyBroadcast(broadcastIntent);
    }
}}

清单文件条目:

应用前部分:

uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"

应用程序内部部分

service android:name=".MyService"

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

首先,我创建了一个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);

我希望这能有所帮助。

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

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生命周期以确定侦听器的设置最适合于您的个人实现。

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

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

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

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