我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。
我如何处理屏幕方向的变化优雅?
下面的示例代码大致匹配我的实际程序:
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不是一个Fragment子,所以我的自定义类“ProgressDialogFragment”可以扩展DialogFragment,以保持对话框显示的配置更改。
import androidx.annotation.NonNull;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
/**
* Usage:
* To display the dialog:
* >>> ProgressDialogFragment.showProgressDialogFragment(
* getSupportFragmentManager(),
* "fragment_tag",
* "my dialog title",
* "my dialog message");
*
* To hide the dialog
* >>> ProgressDialogFragment.hideProgressDialogFragment();
*/
public class ProgressDialogFragment extends DialogFragment {
private static String sTitle, sMessage;
private static ProgressDialogFragment sProgressDialogFragment;
public ProgressDialogFragment() {
}
private ProgressDialogFragment(String title, String message) {
sTitle = title;
sMessage = message;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return ProgressDialog.show(getActivity(), sTitle, sMessage);
}
public static void showProgressDialogFragment(FragmentManager fragmentManager, String fragmentTag, String title, String message) {
if (sProgressDialogFragment == null) {
sProgressDialogFragment = new ProgressDialogFragment(title, message);
sProgressDialogFragment.show(fragmentManager, fragmentTag);
} else { // case of config change (device rotation)
sProgressDialogFragment = (ProgressDialogFragment) fragmentManager.findFragmentByTag(fragmentTag); // sProgressDialogFragment will try to survive its state on configuration as much as it can, but when calling .dismiss() it returns NPE, so we have to reset it on each config change
sTitle = title;
sMessage = message;
}
}
public static void hideProgressDialogFragment() {
if (sProgressDialogFragment != null) {
sProgressDialogFragment.dismiss();
}
}
}
我们面临的挑战是在屏幕上保留对话框标题和消息
当它们重置为默认空字符串时旋转,尽管对话框仍然显示
有两种方法可以解决这个问题:
第一种方法:
使利用对话框的活动在manifest文件中的配置更改期间保留状态:
android:configChanges="orientation|screenSize|keyboardHidden"
谷歌不喜欢此方法。
第二种方法:
在活动的onCreate()方法上,如果savedInstanceState不为空,你需要通过重新构建ProgressDialogFragment来保留你的对话片段。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_deal);
if (savedInstanceState != null) {
ProgressDialogFragment saveProgressDialog = (ProgressDialogFragment) getSupportFragmentManager()
.findFragmentByTag("fragment_tag");
if (saveProgressDialog != null) {
showProgressDialogFragment(getSupportFragmentManager(), "fragment_tag", "my dialog title", "my dialog message");
}
}
}
如果你维护两个布局,所有的UI线程都应该终止。
如果你使用AsynTask,那么你可以很容易地在当前活动的onDestroy()方法中调用.cancel()方法。
@Override
protected void onDestroy (){
removeDialog(DIALOG_LOGIN_ID); // remove loading dialog
if (loginTask != null){
if (loginTask.getStatus() != AsyncTask.Status.FINISHED)
loginTask.cancel(true); //cancel AsyncTask
}
super.onDestroy();
}
对于AsyncTask,请在“取消任务”部分阅读更多信息。
更新:
增加了检查状态的条件,因为只有当它处于运行状态时才能取消。
还要注意,AsyncTask只能执行一次。
我有一个实现,允许活动在屏幕方向改变上被销毁,但仍然在重新创建的活动中成功地销毁对话框。
我用…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接口和/或集合,以允许不同类型的任务从相同的活动中运行。
我也遇到过同样的问题。我的活动需要从一个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设计的“正确”方法——他们声称这种“在屏幕旋转期间破坏/重建活动”实际上使事情变得更简单,所以我猜这应该不是太棘手。
如果您在我的代码中发现问题,请告诉我。如上所述,我真的不知道是否有任何副作用。
我的解决方案是扩展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);
}
}