我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。
我如何处理屏幕方向的变化优雅?
下面的示例代码大致匹配我的实际程序:
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处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。
我尝试执行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);
我希望这能有所帮助。
我也遇到过同样的问题。我的活动需要从一个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设计的“正确”方法——他们声称这种“在屏幕旋转期间破坏/重建活动”实际上使事情变得更简单,所以我猜这应该不是太棘手。
如果您在我的代码中发现问题,请告诉我。如上所述,我真的不知道是否有任何副作用。
对于这些问题,我想出了一个坚如磐石的解决方案,它符合“Android方式”。我使用IntentService模式进行所有长时间运行的操作。
也就是说,我的活动广播意图,IntentService负责工作,将数据保存在DB中,然后广播粘性意图。粘性部分很重要,即使Activity在用户启动工作后暂停,并且错过了来自IntentService的实时广播,我们仍然可以响应并从调用Activity中获取数据。progressdialog可以很好地使用onSaveInstanceState()与此模式一起工作。
Basically, you need to save a flag that you have a progress dialog running in the saved instance bundle. Do not save the progress dialog object because this will leak the entire Activity. To have a persistent handle to the progress dialog, I store it as a weak reference in the application object. On orientation change or anything else that causes the Activity to pause (phone call, user hits home etc.) and then resume, I dismiss the old dialog and recreate a new dialog in the newly created Activity.
对于无限进度对话框来说,这很容易。对于进度条样式,您必须将最后一个已知的进度放在bundle中,并将您在活动中本地使用的任何信息用于跟踪进度。在恢复进度时,您将使用此信息以与之前相同的状态重新生成进度条,然后根据当前状态进行更新。
总之,将长时间运行的任务放到IntentService中,再加上明智地使用onSaveInstanceState(),可以让你有效地跟踪对话框,并在活动生命周期事件中恢复对话框。活动代码的相关部分如下。您还需要BroadcastReceiver中的逻辑来适当地处理Sticky意图,但这超出了本文的范围。
public void doSignIn(View view) {
waiting=true;
AppClass app=(AppClass) getApplication();
String logingon=getString(R.string.signon);
app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this, "", logingon, true));
...
}
@Override
protected void onSaveInstanceState(Bundle saveState) {
super.onSaveInstanceState(saveState);
saveState.putBoolean("waiting",waiting);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState!=null) {
restoreProgress(savedInstanceState);
}
...
}
private void restoreProgress(Bundle savedInstanceState) {
waiting=savedInstanceState.getBoolean("waiting");
if (waiting) {
AppClass app=(AppClass) getApplication();
ProgressDialog refresher=(ProgressDialog) app.Dialog.get();
refresher.dismiss();
String logingon=getString(R.string.signon);
app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this, "", logingon, true));
}
}
这是我面对这个问题时的解决方案:
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");
}
}
}