我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。
我如何处理屏幕方向的变化优雅?
下面的示例代码大致匹配我的实际程序:
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处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。
诀窍是在onPreExecute/onPostExecute期间像往常一样在AsyncTask中显示/解散对话框,尽管在方向改变的情况下,在活动中创建/显示一个对话框的新实例,并将其引用传递给任务。
public class MainActivity extends Activity {
private Button mButton;
private MyTask mTask = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
MyTask task = (MyTask) getLastNonConfigurationInstance();
if(task != null){
mTask = task;
mTask.mContext = this;
mTask.mDialog = ProgressDialog.show(this, "", "", true);
}
mButton = (Button) findViewById(R.id.button1);
mButton.setOnClickListener(new View.OnClickListener(){
public void onClick(View v){
mTask = new MyTask(MainActivity.this);
mTask.execute();
}
});
}
@Override
public Object onRetainNonConfigurationInstance() {
String str = "null";
if(mTask != null){
str = mTask.toString();
mTask.mDialog.dismiss();
}
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
return mTask;
}
private class MyTask extends AsyncTask<Void, Void, Void>{
private ProgressDialog mDialog;
private MainActivity mContext;
public MyTask(MainActivity context){
super();
mContext = context;
}
protected void onPreExecute() {
mDialog = ProgressDialog.show(MainActivity.this, "", "", true);
}
protected void onPostExecute(Void result) {
mContext.mTask = null;
mDialog.dismiss();
}
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(5000);
return null;
}
}
}
我有一个实现,允许活动在屏幕方向改变上被销毁,但仍然在重新创建的活动中成功地销毁对话框。
我用…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接口和/或集合,以允许不同类型的任务从相同的活动中运行。
我的解决方案是扩展ProgressDialog类来获得我自己的MyProgressDialog。
我重新定义了show()和dismiss()方法来在显示对话框之前锁定方向,并在对话框被解散时将其解锁。
因此,当显示对话框并且设备的方向发生变化时,屏幕的方向保持不变,直到调用dismiss(),然后屏幕方向根据传感器值/设备方向发生变化。
这是我的代码:
public class MyProgressDialog extends ProgressDialog {
private Context mContext;
public MyProgressDialog(Context context) {
super(context);
mContext = context;
}
public MyProgressDialog(Context context, int theme) {
super(context, theme);
mContext = context;
}
public void show() {
if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
else
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
super.show();
}
public void dismiss() {
super.dismiss();
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
}
最初的问题是代码无法在屏幕方向改变时存活下来。显然,这是“解决”的程序处理屏幕方向变化本身,而不是让UI框架做它(通过调用onDestroy)。
I would submit that if the underlying problem is that the program will not survive onDestroy(), then the accepted solution is just a workaround that leaves the program with serious other problems and vulnerabilities. Remember that the Android framework specifically states that your activity is at risk for being destroyed almost at any time due to circumstances outside your control. Therefore, your activity must be able to survive onDestroy() and subsequent onCreate() for any reason, not just a screen orientation change.
如果你要接受自己处理屏幕方向变化来解决OP的问题,你需要验证onDestroy()的其他原因不会导致相同的错误。你能做到吗?如果不是,我会质疑这个“公认的”答案是否真的是一个很好的答案。
我也遇到过同样的问题。我的活动需要从一个URL解析一些数据,它很慢。所以我创建了一个线程来完成这个任务,然后显示一个进度对话框。当线程完成时,我让线程通过处理程序发布消息回UI线程。在处理程序。handleMessage,我从线程获得数据对象(现在准备好了),并将其填充到UI。这和你的例子很相似。
经过反复试验,我似乎找到了解决办法。至少现在我可以在任何时候旋转屏幕,在线程完成之前或之后。在所有测试中,对话框都是正确关闭的,所有行为都符合预期。
我所做的如下所示。目标是填充我的数据模型(mDataObject),然后将其填充到UI。应该允许屏幕旋转在任何时候没有意外。
class MyActivity {
private MyDataObject mDataObject = null;
private static MyThread mParserThread = null; // static, or make it singleton
OnCreate() {
...
Object retained = this.getLastNonConfigurationInstance();
if(retained != null) {
// data is already completely obtained before config change
// by my previous self.
// no need to create thread or show dialog at all
mDataObject = (MyDataObject) retained;
populateUI();
} else if(mParserThread != null && mParserThread.isAlive()){
// note: mParserThread is a static member or singleton object.
// config changed during parsing in previous instance. swap handler
// then wait for it to finish.
mParserThread.setHandler(new MyHandler());
} else {
// no data and no thread. likely initial run
// create thread, show dialog
mParserThread = new MyThread(..., new MyHandler());
mParserThread.start();
showDialog(DIALOG_PROGRESS);
}
}
// http://android-developers.blogspot.com/2009/02/faster-screen-orientation-change.html
public Object onRetainNonConfigurationInstance() {
// my future self can get this without re-downloading
// if it's already ready.
return mDataObject;
}
// use Activity.showDialog instead of ProgressDialog.show
// so the dialog can be automatically managed across config change
@Override
protected Dialog onCreateDialog(int id) {
// show progress dialog here
}
// inner class of MyActivity
private class MyHandler extends Handler {
public void handleMessage(msg) {
mDataObject = mParserThread.getDataObject();
populateUI();
dismissDialog(DIALOG_PROGRESS);
}
}
}
class MyThread extends Thread {
Handler mHandler;
MyDataObject mDataObject;
// constructor with handler param
public MyHandler(..., Handler h) {
...
mHandler = h;
}
public void setHandler(Handler h) { mHandler = h; } // for handler swapping after config change
public MyDataObject getDataObject() { return mDataObject; } // return data object (completed) to caller
public void run() {
mDataObject = new MyDataObject();
// do the lengthy task to fill mDataObject with data
lengthyTask(mDataObject);
// done. notify activity
mHandler.sendEmptyMessage(0); // tell activity: i'm ready. come pick up the data.
}
}
这对我来说很有效。我不知道这是否是Android设计的“正确”方法——他们声称这种“在屏幕旋转期间破坏/重建活动”实际上使事情变得更简单,所以我猜这应该不是太棘手。
如果您在我的代码中发现问题,请告诉我。如上所述,我真的不知道是否有任何副作用。