我的程序在后台线程中执行一些网络活动.在开始之前,它会弹出一个进度对话框.该对话框在处理程序上被关闭.这一切都很好,除非在对话框启动时屏幕方向发生变化(后台线程正在运行).此时,应用程序崩溃或死锁,或进入一个奇怪的阶段,在应用程序完全无法工作之前,直到所有线程都被杀死.
如何优雅地处理屏幕方向变化?
下面的示例代码大致匹配我的真实程序:
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.(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处于部分绘制状态.需要在重新开始工作之前杀死整个应用程序.
编辑:谷歌工程师不推荐这种方法,正如Dianne Hackborn(又名hackbod)在这篇StackOverflow帖子中所描述的那样.查看此博客文章了解更多信息.
您必须将此添加到清单中的活动声明:
android:configChanges="orientation|screenSize"
所以它看起来像
问题是当配置发生变化时,系统会破坏活动.请参阅ConfigurationChanges.
因此,将其放在配置文件中可以避免系统破坏您的活动.相反,它会调用该onConfigurationChanged(Configuration)
方法.
当您切换方向时,Android将创建一个新的视图.您可能正在崩溃,因为您的后台线程正在尝试更改旧的状态.(它可能也有问题,因为你的后台线程不在UI线程上)
我建议使mHandler易变,并在方向改变时更新它.
我想出了一个坚如磐石的解决方案来解决这些问题,这些解决方案符合"Android方式"的要求.我使用IntentService模式进行了所有长时间运行的操作.
也就是说,我的活动广播意图,IntentService完成工作,将数据保存在数据库中,然后广播粘性意图.粘性部分很重要,这样即使在用户启动工作期间暂停活动并且错过了IntentService的实时广播,我们仍然可以响应并从调用活动中获取数据.ProgressDialog
s可以很好地使用这种模式onSaveInstanceState()
.
基本上,您需要保存一个标志,您在已保存的实例包中运行了一个进度对话框.不要保存进度对话框对象,因为这会泄漏整个Activity.要拥有进度对话框的持久句柄,我将其存储为应用程序对象中的弱引用.在方向更改或导致活动暂停的任何其他内容(电话呼叫,用户点击回家等)然后恢复时,我会关闭旧对话框并在新创建的活动中重新创建一个新对话框.
对于无限期的进度对话,这很容易.对于进度条样式,您必须在捆绑中放置最后的已知进度以及您在活动中本地使用的任何信息以跟踪进度.在恢复进度时,您将使用此信息以与以前相同的状态重新生成进度条,然后根据事物的当前状态进行更新.
总而言之,将长时间运行的任务放入IntentService并加上明智的使用,onSaveInstanceState()
可以让您有效地跟踪对话并在整个Activity生命周期事件中恢复.活动代码的相关位如下.你还需要在BroadcastReceiver中使用逻辑来适当地处理Sticky意图,但这超出了这个范围.
public void doSignIn(View view) { waiting=true; AppClass app=(AppClass) getApplication(); String logingon=getString(R.string.signon); app.Dialog=new WeakReference(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.show(AddAccount.this, "", logingon, true)); } }
我遇到了同样的问题.我的活动需要从URL解析一些数据并且速度很慢.所以我创建了一个线程来执行此操作,然后显示进度对话框.我让线程Handler
在完成后将消息发回UI线程.在Handler.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设计的"正确"方法 - 他们声称这种"在屏幕旋转期间破坏/重新创建活动"实际上使事情变得更容易,所以我想它不应该太棘手.
如果您在我的代码中发现问题,请告诉我.如上所述,我真的不知道是否有任何副作用.
最初的感知问题是代码无法在屏幕方向更改中存活.显然,通过让程序自己处理屏幕方向更改而不是让UI框架执行它(通过调用onDestroy)来"解决"这个问题.
我想提出的是,如果根本问题是,该方案将无法生存的onDestroy(),然后接受的解决方案就是这样留下严重的其他问题和漏洞的程序的解决方法.请记住,Android框架明确指出,由于您无法控制的情况,您的活动几乎可以随时被销毁.因此,您的活动必须能够以任何理由存在onDestroy()和后续onCreate(),而不仅仅是屏幕方向更改.
如果您要自己接受处理屏幕方向更改以解决OP的问题,则需要验证onDestroy()的其他原因不会导致相同的错误.你能做到吗?如果没有,我会质疑"接受"的答案是否真的是一个非常好的答案.
我的解决方案是扩展ProgressDialog
课程以获得自己的课程MyProgressDialog
.
我重新定义show()
和dismiss()
方法,以显示之前锁定方向Dialog
和解锁回来时Dialog
被驳回.因此,当Dialog
显示并且设备的方向改变时,屏幕的方向保持不变直到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); } }
我遇到了同样的问题,我想出了一个没有使用ProgressDialog进行访问的解决方案,我得到了更快的结果.
我所做的是创建一个包含ProgressBar的布局.
然后在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,那么这个答案不适合您.
我发现了一个解决方案,我还没有在其他地方看到过.您可以使用自定义应用程序对象,该对象知道您是否有后台任务,而不是尝试在方向更改中被销毁和重新创建的活动中执行此操作.我在这里写博客.
我将贡献我的方法来处理这个轮换问题.这可能与OP没有关系AsyncTask
,因为他没有使用,但也许其他人会觉得它很有用.这很简单,但它似乎为我做的工作:
我有一个AsyncTask
名为嵌套类的登录活动BackgroundLoginTask
.
在我看来,BackgroundLoginTask
我不做任何与众不同的事情,除非在调用ProgressDialog
解雇时添加一个空检查:
@Override protected void onPostExecute(Boolean result) { if (pleaseWaitDialog != null) pleaseWaitDialog.dismiss(); [...] }
这是为了处理后台任务在Activity
不可见时完成的情况,因此,该onPause()
方法已经取消了进度对话框.
接下来,在我的父Activity
类中,我为我的AsyncTask
类创建全局静态句柄,而我ProgressDialog
(AsyncTask
嵌套,可以访问这些变量):
private static BackgroundLoginTask backgroundLoginTask; private static ProgressDialog pleaseWaitDialog;
这有两个目的:首先,它允许我Activity
始终访问AsyncTask
对象,即使是新的后旋转活动.其次,它允许我在旋转后BackgroundLoginTask
访问和关闭ProgressDialog
.
接下来,我将其添加到onPause()
,导致进度对话框在我们Activity
离开前景时消失(防止丑陋的"强制关闭"崩溃):
if (pleaseWaitDialog != null) pleaseWaitDialog.dismiss();
最后,我的方法中有以下内容onResume()
:
if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING)) { if (pleaseWaitDialog != null) pleaseWaitDialog.show(); }
这允许在Dialog
重新Activity
创建之后重新出现.
这是整个班级:
public class NSFkioskLoginActivity extends NSFkioskBaseActivity { private static BackgroundLoginTask backgroundLoginTask; private static ProgressDialog pleaseWaitDialog; private Controller cont; // This is the app entry point. /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (CredentialsAvailableAndValidated()) { //Go to main menu and don't run rest of onCreate method. gotoMainMenu(); return; } setContentView(R.layout.login); populateStoredCredentials(); } //Save current progress to options when app is leaving foreground @Override public void onPause() { super.onPause(); saveCredentialsToPreferences(false); //Get rid of progress dialog in the event of a screen rotation. Prevents a crash. if (pleaseWaitDialog != null) pleaseWaitDialog.dismiss(); } @Override public void onResume() { super.onResume(); if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING)) { if (pleaseWaitDialog != null) pleaseWaitDialog.show(); } } /** * Go to main menu, finishing this activity */ private void gotoMainMenu() { startActivity(new Intent(getApplicationContext(), NSFkioskMainMenuActivity.class)); finish(); } /** * * @param setValidatedBooleanTrue If set true, method will set CREDS_HAVE_BEEN_VALIDATED to true in addition to saving username/password. */ private void saveCredentialsToPreferences(boolean setValidatedBooleanTrue) { SharedPreferences settings = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE); SharedPreferences.Editor prefEditor = settings.edit(); EditText usernameText = (EditText) findViewById(R.id.editTextUsername); EditText pswText = (EditText) findViewById(R.id.editTextPassword); prefEditor.putString(USERNAME, usernameText.getText().toString()); prefEditor.putString(PASSWORD, pswText.getText().toString()); if (setValidatedBooleanTrue) prefEditor.putBoolean(CREDS_HAVE_BEEN_VALIDATED, true); prefEditor.commit(); } /** * Checks if user is already signed in */ private boolean CredentialsAvailableAndValidated() { SharedPreferences settings = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE); if (settings.contains(USERNAME) && settings.contains(PASSWORD) && settings.getBoolean(CREDS_HAVE_BEEN_VALIDATED, false) == true) return true; else return false; } //Populate stored credentials, if any available private void populateStoredCredentials() { SharedPreferences settings = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE); settings.getString(USERNAME, ""); EditText usernameText = (EditText) findViewById(R.id.editTextUsername); usernameText.setText(settings.getString(USERNAME, "")); EditText pswText = (EditText) findViewById(R.id.editTextPassword); pswText.setText(settings.getString(PASSWORD, "")); } /** * Validate credentials in a seperate thread, displaying a progress circle in the meantime * If successful, save credentials in preferences and proceed to main menu activity * If not, display an error message */ public void loginButtonClick(View view) { if (phoneIsOnline()) { EditText usernameText = (EditText) findViewById(R.id.editTextUsername); EditText pswText = (EditText) findViewById(R.id.editTextPassword); //Call background task worker with username and password params backgroundLoginTask = new BackgroundLoginTask(); backgroundLoginTask.execute(usernameText.getText().toString(), pswText.getText().toString()); } else { //Display toast informing of no internet access String notOnlineMessage = getResources().getString(R.string.noNetworkAccessAvailable); Toast toast = Toast.makeText(getApplicationContext(), notOnlineMessage, Toast.LENGTH_SHORT); toast.show(); } } /** * * Takes two params: username and password * */ public class BackgroundLoginTask extends AsyncTask
我绝不是经验丰富的Android开发人员,所以请随时发表评论.