我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。
我如何处理屏幕方向的变化优雅?
下面的示例代码大致匹配我的实际程序:
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处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。
我什么都试过了。花了几天时间做实验。我不想阻止活动旋转。我的设想是:
向用户显示动态信息的进度对话框。例如:“连接到服务器…”,“下载数据…”,等等。
一个线程做繁重的工作,更新对话框
在最后用结果更新UI。
The problem was, when rotating the screen, every solution on the book failed. Even with the AsyncTask class, which is the correct Android way of dealing with this situations. When rotating the screen, the current Context that the starting thread is working with, is gone, and that messes up with the dialog that is showing. The problem was always the Dialog, no matter how many tricks I added to the code (passing new contexts to running threads, retaining thread states through rotations, etc...). The code complexity at the end was always huge and there was always something that could go wrong.
对我来说唯一有效的解决方案就是“活动/对话”技巧。这很简单,很天才,而且都是旋转证明:
Instead of creating a Dialog and ask to show it, create an Activity that has been set in the manifest with android:theme="@android:style/Theme.Dialog". So, it just looks like a dialog.
Replace showDialog(DIALOG_ID) with startActivityForResult(yourActivityDialog, yourCode);
Use onActivityResult in the calling Activity to get the results from the executing thread (even the errors) and update the UI.
On your 'ActivityDialog', use threads or AsyncTask to execute long tasks and onRetainNonConfigurationInstance to save "dialog" state when rotating the screen.
这是快速和工作良好。我仍然使用对话框的其他任务和AsyncTask的一些东西,不需要一个固定的对话框在屏幕上。但在这种情况下,我总是使用活动/对话框模式。
而且,我没有尝试它,但它甚至可以阻止活动/对话框旋转,当线程运行时,加速事情,同时允许调用活动旋转。
如今,有一种更加独特的方式来处理这类问题。典型的方法是:
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生命周期以确定侦听器的设置最适合于您的个人实现。
这是一种通用方法,用于为本问题中问到的一般问题提供健壮而完整的解决方案。根据您的具体情况,这个答案中可能还缺少一些细节,但这通常是正确处理方向更改事件的最正确方法。
最简单和最灵活的解决方案是使用带有ProgressBar静态引用的AsyncTask。这为方向更改问题提供了一个封装的、因此可重用的解决方案。这个解决方案很好地帮助我完成了各种异步任务,包括互联网下载、与服务通信和文件系统扫描。该解决方案已经在多个安卓版本和手机型号上进行了良好的测试。完整的演示可以在DownloadFile.java中找到
下面是一个概念示例
public class SimpleAsync extends AsyncTask<String, Integer, String> {
private static ProgressDialog mProgressDialog = null;
private final Context mContext;
public SimpleAsync(Context context) {
mContext = context;
if ( mProgressDialog != null ) {
onPreExecute();
}
}
@Override
protected void onPreExecute() {
mProgressDialog = new ProgressDialog( mContext );
mProgressDialog.show();
}
@Override
protected void onPostExecute(String result) {
if ( mProgressDialog != null ) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
@Override
protected void onProgressUpdate(Integer... progress) {
mProgressDialog.setProgress( progress[0] );
}
@Override
protected String doInBackground(String... sUrl) {
// Do some work here
publishProgress(1);
return null;
}
public void dismiss() {
if ( mProgressDialog != null ) {
mProgressDialog.dismiss();
}
}
}
在Android Activity中的使用很简单
public class MainActivity extends Activity {
DemoServiceClient mClient = null;
DownloadFile mDownloadFile = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.main );
mDownloadFile = new DownloadFile( this );
Button downloadButton = (Button) findViewById( R.id.download_file_button );
downloadButton.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
mDownloadFile.execute( "http://www.textfiles.com/food/bakebred.txt");
}
});
}
@Override
public void onPause() {
super.onPause();
mDownloadFile.dismiss();
}
}