进度对话框和后台线程处于活动状态时如何处理屏幕方向更改?

Posted

技术标签:

【中文标题】进度对话框和后台线程处于活动状态时如何处理屏幕方向更改?【英文标题】:How to handle screen orientation change when progress dialog and background thread active? 【发布时间】:2010-11-09 20:45:06 【问题描述】:

我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除非在对话框启动时屏幕方向发生变化(并且后台线程正在运行)。此时,应用程序要么崩溃,要么死锁,或者进入一个奇怪的阶段,在所有线程都被杀死之前,应用程序根本无法工作。

如何优雅地处理屏幕方向变化?

下面的示例代码与我的实际程序大致匹配:

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 处于部分绘制状态。需要在它再次开始工作之前杀死整个应用程序。

【问题讨论】:

考虑到您收到的答案,您应该更改已接受的答案以支持最好的答案,不是吗? 另见前一个问题***.com/questions/456211/… 所有,得到了一个非常好的解释和可能的解决方案来解决这个问题。通过http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/让我知道这是否有帮助。 this blog post 中有一个关于如何跨屏幕方向保留异步后台任务的相当完整的解释。看看吧! 只需在 manifest 中将 android:configChanges="orientation|screenSize" 设置为 Activity。它将停止 android 重新创建其活动 【参考方案1】:

编辑: Google 工程师不推荐这种方法,正如 Dianne Hackborn(又名 hackbod)在此 *** post 中所述。查看this blog post了解更多信息。


您必须将此添加到清单中的活动声明中:

android:configChanges="orientation|screenSize"

看起来像

<activity android:label="@string/app_name" 
        android:configChanges="orientation|screenSize|keyboardHidden" 
        android:name=".your.package">

问题在于,当配置发生更改时,系统会破坏活动。见ConfigurationChanges。

因此,将其放入配置文件可避免系统破坏您的活动。相反,它调用onConfigurationChanged(Configuration) 方法。

【讨论】:

这绝对是最好的解决方案;因为它只是旋转布局(您首先期望的行为)。请务必输入 android:configChanges="orientation|keyboardHidden" (因为手机有横向键盘) 这似乎是我所期望的行为。但是,文档表明该活动被销毁“因为任何应用程序资源,包括布局文件,都可以根据任何配置值进行更改。因此,处理配置更改的唯一安全方法是重新检索所有资源”。除了orientation,还有更多的原因导致配置改变:keyboardHidden(我已经编辑了wiki答案),uiMode(例如,进入或退出汽车模式;夜间模式改变),等等。我现在想知道这是否真的是一个很好的答案。 这不是一个可接受的解决方案。它只是掩盖了真正的问题。 有效,但 Google 不推荐。 请不要在这里遵循这种方法。 DDosAttack 是完全正确的。想象一下,您正在为下载或其他需要很长时间的东西创建一个进度对话框。作为用户,您不会停留在该活动上并盯着它看。您将切换到主屏幕或另一个应用程序,例如游戏或可能会打来电话或其他资源匮乏的东西,最终会破坏您的活动。然后呢?您面临着同样的老问题,这个巧妙的小技巧无法解决。当用户回来时,该活动将重新创建。【参考方案2】:

当您切换方向时,Android 将创建一个新视图。您可能会崩溃,因为您的后台线程正在尝试更改旧线程的状态。 (也可能因为你的后台线程不在 UI 线程上而有问题)

我建议将 mHandler 设置为 volatile 并在方向改变时更新它。

【讨论】:

您可能已经查明了崩溃的原因。我摆脱了崩溃,但我仍然没有弄清楚如何以可靠的方式将 UI 恢复到方向更改之前的状态。但是你的回答让我前进了,所以把它作为答案。 当方向改变时,你应该在你的活动中获得一个 onStart。本质上,您必须使用旧数据重新配置视图。因此,我建议从进度条请求数字状态更新,并在您获得新的“onStart”时重建新视图,如果您也获得新活动,我不记得了,但是通过文档进行一些搜索应该会有所帮助。 最近玩过它,我可以告诉你,当你的应用改变方向时,你确实会得到一个新的活动。 (您还将获得一个新视图)如果您尝试更新旧视图,您将收到异常,因为旧视图具有无效的应用程序上下文(您的旧活动)您可以通过传入 myActivity.getApplicationContext()而不是指向活动本身的指针。 有人能解释一下 volatile 在这种情况下的用法/好处吗 @Nepster 是的,我也想知道这一点。如果有人能解释一下 volatile,那就太好了。【参考方案3】:

对于这些问题,我想出了一个坚如磐石的解决方案,符合“Android 方式”。我的所有长时间运行的操作都使用 IntentService 模式。

也就是说,我的活动广播意图,IntentService 完成工作,将数据保存在数据库中,然后广播 sticky 意图。粘性部分很重要,这样即使 Activity 在用户启动工作后暂停并错过了 IntentService 的实时广播,我们仍然可以响应并从调用 Activity 中获取数据。 ProgressDialogs 可以很好地与 onSaveInstanceState() 一起使用这种模式。

基本上,您需要保存一个标志,表明您在保存的实例包中运行了一个进度对话框。 不要保存进度对话框对象,因为这会泄漏整个 Activity。为了获得进度对话框的持久句柄,我将它作为弱引用存储在应用程序对象中。在方向更改或任何其他导致 Activity 暂停(电话、用户回家等)然后恢复时,我会关闭旧对话框并在新创建的 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>(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));
    

【讨论】:

似乎是一个不错的解决方案 “我的所有长时间运行的操作都使用 IntentService 模式。”这不是一个完美的解决方案,因为它就像从大炮中射出麻雀和大量样板代码,更多您可以观看youtube.com/watch?v=NJsq0TU0qeg【参考方案4】:

我遇到了同样的问题。我的活动需要从 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 设计的“正确”方法——他们声称这种“在屏幕旋转期间销毁/重新创建活动”实际上使事情变得更容易,所以我想这应该不会太棘手。

如果您在我的代码中发现问题,请告诉我。如上所述,我真的不知道是否有任何副作用。

【讨论】:

非常感谢! onRetainNonConfigurationInstance()getLastNonConfigurationInstance() 的提示帮助我解决了我的问题。竖起大拇指!【参考方案5】:

最初感知到的问题是代码无法在屏幕方向更改后继续存在。显然,这是通过让程序自己处理屏幕方向更改来“解决”的,而不是让 UI 框架来处理(通过调用 onDestroy))。

我会提出,如果根本问题是程序无法在 onDestroy() 中继续存在,那么可接受的解决方案只是一种解决方法,它会使程序存在严重的其他问题和漏洞。请记住,Android 框架明确指出,由于您无法控制的情况,您的 Activity 几乎随时都有被销毁的风险。因此,无论出于何种原因,您的活动都必须能够在 onDestroy() 和后续 onCreate() 中继续存在,而不仅仅是屏幕方向更改。

如果您要自己接受处理屏幕方向更改以解决 OP 的问题,则需要验证 onDestroy() 的其他原因不会导致相同的错误。你能做到吗?如果不是,我会质疑“接受”的答案是否真的是一个非常好的答案。

【讨论】:

【参考方案6】:

我的解决方案是扩展 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);
    

【讨论】:

【参考方案7】:

我遇到了同样的问题,我想出了一个不使用 ProgressDialog 的解决方案,并且我得到了更快的结果。

我所做的是创建一个包含 ProgressBar 的布局。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_>
<ProgressBar
    android:id="@+id/progressImage"
    android:layout_
    android:layout_
    android:layout_centerInParent="true"
    />
</RelativeLayout>

然后在 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,那么这个答案不适合您。

【讨论】:

这个解决方案在一个简单的用例中很优雅,但也有缺点。您需要重建完整的内容视图。 setContentView(R.layout.my_layout); 不够;你需要设置所有的监听器,重置数据等。 @rds 你是对的。这实际上只是一个简单案例的解决方案,或者如果您需要在显示视图之前在 onCreate 方法中做一些繁重的工作。 我不太明白。不像我们通常做的那样在 onCreate() 中设置监听器,我们可以在 run() 中设置它们。我在这里错过了什么吗?【参考方案8】:

我发现了一个我在其他地方还没有见过的解决方案。您可以使用一个自定义应用程序对象,该对象知道您是否有后台任务正在执行,而不是尝试在因方向更改而被破坏和重新创建的活动中执行此操作。我在here 上写了一篇博客。

【讨论】:

创建自定义Application 通常用于维护全局应用程序状态。我并不是说它不起作用,但它似乎过于复杂。来自文档“通常不需要子类应用程序。”。我在很大程度上更喜欢sonxurxo的回答。【参考方案9】:

我将贡献我的方法来处理这个轮换问题。这可能与 OP 无关,因为他没有使用AsyncTask,但也许其他人会发现它很有用。这很简单,但似乎对我有用:

我有一个登录活动,其中包含一个名为 BackgroundLoginTask 的嵌套 AsyncTask 类。

在我的BackgroundLoginTask 中,除了在调用ProgressDialog 的dismiss 时添加一个空检查之外,我没有做任何不寻常的事情:

@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();
        

这允许DialogActivity 重新创建后重新出现。

这是整个班级:

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<Object, String, Boolean>
           
       private Exception e = null;

       @Override
       protected void onPreExecute()
       
           cont = Controller.getInstance();
           //Show progress dialog
           String pleaseWait = getResources().getString(R.string.pleaseWait);
           String commWithServer = getResources().getString(R.string.communicatingWithServer);
            if (pleaseWaitDialog == null)
              pleaseWaitDialog= ProgressDialog.show(NSFkioskLoginActivity.this, pleaseWait, commWithServer, true);

       

        @Override
        protected Boolean doInBackground(Object... params)
        
        try 
            //Returns true if credentials were valid. False if not. Exception if server could not be reached.
            return cont.validateCredentials((String)params[0], (String)params[1]);
         catch (Exception e) 
            this.e=e;
            return false;
        
        

        /**
         * result is passed from doInBackground. Indicates whether credentials were validated.
         */
        @Override
        protected void onPostExecute(Boolean result)
        
        //Hide progress dialog and handle exceptions
        //Progress dialog may be null if rotation has been switched
        if (pleaseWaitDialog != null)
             
            pleaseWaitDialog.dismiss();
                pleaseWaitDialog = null;
             

        if (e != null)
        
         //Show toast with exception text
                String networkError = getResources().getString(R.string.serverErrorException);
                Toast toast = Toast.makeText(getApplicationContext(), networkError, Toast.LENGTH_SHORT);
            toast.show();
        
        else
        
            if (result == true)
            
            saveCredentialsToPreferences(true);
            gotoMainMenu();
            
            else
            
            String toastText = getResources().getString(R.string.invalidCredentialsEntered);
                Toast toast = Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_SHORT);
            toast.show();
             
        
        

    

我绝不是一个经验丰富的 Android 开发者,请随时发表评论。

【讨论】:

有趣!特别是对于我们这些使用 AsyncTask 的人。刚刚尝试了您的解决方案,它似乎大部分都有效。有一个问题:ProgressDialog 似乎在旋转后提前终止,而 ProgressDialog 仍然处于活动状态。我要四处看看到底发生了什么以及如何解决它。但我不再遇到那些崩溃了! 找到了一个修复程序。看来这里的问题是静态ProgressDialog。当旋转中断 ProgressDialog 时,它有时会在新 Activity 中重新启动后调用其 .dismiss() 方法。通过为每个 Activity 创建 ProgressDialog,我们确保这个新的 ProgressDialog 不会与旧的 Activity 一起被杀死。我还确保 ProgressDialog 在被解除时设置为 null(以帮助垃圾收集)。所以我们在这里有一个解决方案!向那些使用 AsyncTask 的人干杯!【参考方案10】:

将长任务移至单独的班级。将其实现为主题观察者模式。每当创建活动时注册并在关闭任务类时取消注册。任务类可以使用 AsyncTask。

【讨论】:

我看不出这有什么帮助。您能否更详细地解释一下这如何防止我看到的问题。 正如 Haseman 所说,它阻止后端访问 UI 元素,我们可以将 UI 与后端分离,后端在单独的线程中运行,即使在屏幕重新定向和注册后它也会继续运行 -取消注册后端任务以获取状态更新。我使用它解决的真实示例是我有一个下载任务,我已将其移至单独的线程,每当创建线程时,我都会向它注册-注销。 好的,我正在重新审视这个问题,但我认为我仍然不能完全理解这个答案。假设我们让主 Activity 启动 AsyncTask 来执行我们不想在屏幕方向更改期间中断的长时间运行的网络操作。我看不到新活动如何向旧活动启动的 AsyncTask 发送消息。能给个代码示例吗? @Heikki,我的实现低于你的意思吗?【参考方案11】:

诀窍是像往常一样在 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;
               
    

【讨论】:

【参考方案12】:

我是这样做的:

    package com.palewar;
    import android.app.Activity;
    import android.app.ProgressDialog;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;

    public class ThreadActivity extends Activity 


        static ProgressDialog dialog;
        private Thread downloadThread;
        final static Handler handler = new Handler() 

            @Override
            public void handleMessage(Message msg) 

                super.handleMessage(msg);

                dialog.dismiss();

            

        ;

        protected void onDestroy() 
    super.onDestroy();
            if (dialog != null && dialog.isShowing()) 
                dialog.dismiss();
                dialog = null;
            

        

        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) 
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);

            downloadThread = (Thread) getLastNonConfigurationInstance();
            if (downloadThread != null && downloadThread.isAlive()) 
                dialog = ProgressDialog.show(ThreadActivity.this, "",
                        "Signing in...", false);
            

            dialog = ProgressDialog.show(ThreadActivity.this, "",
                    "Signing in ...", false);

            downloadThread = new MyThread();
            downloadThread.start();
            // processThread();
        

        // Save the thread
        @Override
        public Object onRetainNonConfigurationInstance() 
            return downloadThread;
        


        static public class MyThread extends Thread 
            @Override
            public void run() 

                try 
                    // Simulate a slow network
                    try 
                        new Thread().sleep(5000);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    handler.sendEmptyMessage(0);

                 finally 

                
            
        

    

您也可以尝试告诉我它是否适合您

【讨论】:

onDestroy 代码可能根本不会执行,来自Developer's page: "注意上表中的“Killable”列——对于那些被标记为可杀死的方法,在后面该方法返回托管该活动的进程,该进程可能随时被系统杀死没有另一行代码被执行" 我坚信“onRetainNonConfigurationInstance()”是用于此类情况的方法......纽约工作 我面临着类似的问题,就像 Sachin Gurnani 一样,我使用静态声明来解决我的问题。 ***.com/questions/12058774/…【参考方案13】:

这是我提出的解决方案:

将 AsyncTask 或线程移动到保留的 Fragment,如 here 所述。我相信将所有网络调用移动到片段是一个很好的做法。如果您已经在使用片段,则可以让其中之一负责调用。否则,您可以按照链接文章的建议创建一个仅用于执行请求的片段。 片段将使用侦听器接口来发出任务完成/失败的信号。您不必担心那里的方向变化。片段将始终具有指向当前活动的正确链接,并且可以安全地恢复进度对话框。 让您的进度对话框成为您班级的成员。事实上,您应该对所有对话框都这样做。在 onPause 方法中,您应该关闭它们,否则您将在配置更改时泄漏一个窗口。忙碌状态应该由片段保持。当片段附加到活动时,如果调用仍在运行,您可以再次调出进度对话框。为此,可以将void showProgressDialog() 方法添加到片段活动侦听器接口中。

【讨论】:

完美的解决方案,但不明白为什么这个答案被其他人所掩盖!!!【参考方案14】:

如果您创建一个后台 Service 来完成所有繁重的工作(tcp 请求/响应、解组),则可以销毁并重新创建 ViewActivity 而不会泄漏窗口或丢失数据。这允许 Android 推荐的行为,即destroy an Activity on each configuration change(例如,对于每个方向更改)。

它有点复杂,但它是调用服务器请求、数据预处理/后处理等的最佳方式。

您甚至可以使用您的Service 将每个请求排队到服务器,这样就可以轻松高效地处理这些事情。

开发指南有完整的chapter on Services

【讨论】:

ServiceAsyncTask 更有效,但在某些情况下可能是更好的方法。它不一定更好,是吗?话虽如此,我不明白这如何解决从主要Activity 泄漏的ProgressDialog 的问题。你在哪里实例化ProgressDialog?你在哪里解雇它?【参考方案15】:

我有一个实现,它允许在屏幕方向更改时销毁活动,但仍然成功销毁重新创建的活动中的对话框。 我使用...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 引用“拥有”背景任务,并使用 onRetainNonConfigurationInstancegetLastNonConfigurationInstance 更新此字段。

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;
  
  ...

进一步改进的建议:

任务完成后清除 Activity 中的backgroundTask 引用以释放与其关联的任何内存或其他资源。 在销毁活动之前清除后台任务中的ownerActivity引用,以防它不会立即重新创建。 创建一个BackgroundTask 接口和/或集合以允许不同类型的任务从同一个拥有的活动中运行。

【讨论】:

【参考方案16】:

如果您维护两个布局,则应终止所有 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,请在 here 的“取消任务”部分阅读更多内容。

更新: 添加了检查状态的条件,因为它只有在运行状态下才能取消。 另请注意,AsyncTask 只能执行一次。

【讨论】:

【参考方案17】:

尝试实施 jfelectron 的解决方案,因为它是“这些问题的坚如磐石的解决方案,符合‘Android 方式’的事物”但它花了一些时间来查找并将所有提到的元素放在一起。最终得到了这个稍微不同的,我认为更优雅的解决方案在这里完整发布。

使用从 Activity 触发的 IntentService 在单独的线程上执行长时间运行的任务。该服务将粘性广播意图返回到更新对话框的活动。 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"

【讨论】:

【参考方案18】:

我遇到了同样的情况。我所做的只是在整个应用程序中为我的进度对话框获取一个实例。

首先,我创建了一个 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);

希望对你有帮助。

【讨论】:

【参考方案19】:

这是一个非常古老的问题,出于某种原因出现在侧边栏上。

如果后台任务只需要在活动处于前台时继续存在,“新”解决方案是将后台线程(或者,最好是AsyncTask)托管在保留片段中,如 developer guide 和 numerous Q&As 中所述。

如果活动因配置更改而被销毁,则保留的片段仍然存在,但不会当活动在后台或后台堆栈中销毁时。因此,如果onPause()中的isChangingConfigurations()为false,后台任务仍应中断。

【讨论】:

【参考方案20】:

我是 android 的新手,我尝试了这个并且它有效。

public class loadTotalMemberByBranch extends AsyncTask<Void, Void,Void> 
        ProgressDialog progressDialog = new ProgressDialog(Login.this);
        int ranSucess=0;
        @Override
        protected void onPreExecute() 
            // TODO Auto-generated method stub
            super.onPreExecute();
            progressDialog.setTitle("");    
            progressDialog.isIndeterminate();
            progressDialog.setCancelable(false);
            progressDialog.show();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);

        
        @Override
        protected Void doInBackground(Void... params) 
            // TODO Auto-generated method stub

            return null;
        
        @Override
        protected void onPostExecute(Void result) 
            // TODO Auto-generated method stub
            super.onPostExecute(result);
            progressDialog.dismiss();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        

【讨论】:

【参考方案21】:

我什么都试过了。花了几天的时间做实验。我不想阻止活动旋转。我的情况是:

    向用户显示动态信息的进度对话框。例如:“正在连接服务器...”、“正在下载数据...”等 执行繁重工作并更新对话框的线程 使用最后的结果更新 UI。

问题是,当旋转屏幕时,书中的每个解决方案都失败了。即使使用 AsyncTask 类,这是处理这种情况的正确 Android 方式。旋转屏幕时,启动线程正在使用的当前上下文消失了,并且与正在显示的对话框混淆。问题始终是对话框,无论我在代码中添加了多少技巧(将新上下文传递给正在运行的线程,通过旋转保持线程状态等......)。最后的代码复杂度总是很大,总有可能出错的地方。

唯一对我有用的解决方案是 Activity/Dialog 技巧。它既简单又天才,而且都是轮换证明:

    与其创建一个对话框并要求显示它,不如创建一个已在清单中设置的 Activity,并使用 android:theme="@android:style/Theme.Dialog"。所以,它看起来就像一个对话框。

    将 showDialog(DIALOG_ID) 替换为 startActivityForResult(yourActivityDialog, yourCode);

    在调用 Activity 中使用 onActivityResult 从执行线程中获取结果(甚至是错误)并更新 UI。

    在“ActivityDialog”上,使用线程或 AsyncTask 执行长任务,使用 onRetainNonConfigurationInstance 在旋转屏幕时保存“对话框”状态。

这速度很快,效果很好。我仍然将对话框用于其他任务,将 AsyncTask 用于不需要在屏幕上显示恒定对话框的任务。但在这种情况下,我总是选择 Activity/Dialog 模式。

而且,我没有尝试过,但它甚至可以阻止 Activity/Dialog 旋转,当线程运行时,加快速度,同时允许调用 Activity 旋转。

【讨论】:

不错,只是参数必须通过Intent 传递,这比AsyncTask 允许的Object 更严格 @Rui 我去年也用过这个方法。我现在意识到这也是不正确的。如果这是“解决”这个问题的方法,为什么谷歌甚至会有一个对话框?我看到的问题是,如果您从 ActivityA 打开 ActivityB (Theme.Dialog),那么 ActivityA 会在 Activity 堆栈上向下移动,因此如果需要,操作系统会将其标记为准备好终止。因此,如果您有一个长时间运行的进程并且正在显示某种虚假的进度“对话框”并且花费了太长时间并且内存不足...... ActivityA 被杀死并且在进度完成后也没有任何东西可以返回。【参考方案22】:

如今,处理此类问题的方式更加独特。典型的做法是:

1。确保您的数据与 UI 正确分离:

任何后台进程都应该在保留的Fragment 中(使用Fragment.setRetainInstance() 设置。这将成为您的“持久数据存储”,其中保留您希望保留的任何数据。在方向更改事件之后,这个Fragment 仍然可以通过FragmentManager.findFragmentByTag() 调用以其原始状态访问(当你创建它时,你应该给它一个标签不是ID,因为它没有附加到View )。

请参阅 Handling Runtime Changes 开发指南,了解有关正确执行此操作以及为什么它是最佳选择的信息。

2。确保您在后台进程和 UI 之间正确且安全地进行交互:

您必须反转您的链接过程。目前,您的后台进程将自己附加到 View - 而您的 View 应该将自己附加到后台进程。这更有意义对吧? View 的动作依赖于后台进程,而后台进程不依赖于View。这意味着将链接更改为标准的Listener 接口。假设您的流程(无论是什么类 - 无论是 AsyncTaskRunnable 还是其他)定义了一个 OnProcessFinishedListener,当流程完成后,它应该调用该侦听器(如果存在)。

answer 是对如何自定义监听器的简洁描述。

3。每当创建 UI(包括方向更改)时,将您的 UI 链接到数据流程:

现在您必须担心将后台任务与您当前的View 结构相连接。如果您正确处理您的方向更改(不是人们总是推荐的configChanges hack),那么系统将重新创建您的Dialog。这很重要,这意味着在方向更改时,您的所有Dialog 的生命周期方法都会被调用。因此,在任何这些方法中(onCreateDialog 通常是一个好地方),您都可以进行如下调用:

DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) 
    f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() 
        public void onProcessFinished() 
            dismiss();
        
    );
 

请参阅Fragment lifecycle 以决定在您的个人实施中设置侦听器的最佳位置。

这是一种通用方法,可为该问题中提出的一般问题提供稳健且完整的解决方案。根据您的个人情况,此答案中可能缺少一些小部分,但这通常是正确处理方向更改事件的最正确方法。

【讨论】:

【参考方案23】:

我找到了在方向改变时处理线程的更简单的解决方案。您可以只保留对您的活动/片段的静态引用,并在对 ui 进行操作之前验证其是否为空。我建议也使用 try catch:

 public class DashListFragment extends Fragment 
     private static DashListFragment ACTIVE_INSTANCE;

     @Override
     public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);

        ACTIVE_INSTANCE = this;

        new Handler().postDelayed(new Runnable() 
            public void run() 
                try 
                        if (ACTIVE_INSTANCE != null) 
                            setAdapter(); // this method do something on ui or use context
                        
                
                catch (Exception e) 


            
        , 1500l);

    

    @Override
    public void onDestroy() 
        super.onDestroy();

        ACTIVE_INSTANCE = null;
    



【讨论】:

【参考方案24】:

如果您在检测独立于活动参考对话框的方向变化事件时遇到了困难,那么这种方法非常有效。我使用它是因为我有自己的对话框类,可以在多个不同的活动中显示,所以我并不总是知道它显示在哪个活动中。使用这种方法,您无需更改 AndroidManifest,担心活动引用,而且您不需要自定义对话框(就像我一样)。但是,您确实需要一个自定义内容视图,以便您可以使用该特定视图检测方向变化。这是我的例子:

设置

public class MyContentView extends View
    public MyContentView(Context context)
        super(context);
    

    @Override
    public void onConfigurationChanged(Configuration newConfig)
        super.onConfigurationChanged(newConfig);

        //DO SOMETHING HERE!! :D
    

实现 1 - 对话框

Dialog dialog = new Dialog(context);
//set up dialog
dialog.setContentView(new MyContentView(context));
dialog.show();

实现 2 - AlertDialog.Builder

AlertDialog.Builder builder = new AlertDialog.Builder(context);
//set up dialog builder
builder.setView(new MyContentView(context));        //Can use this method
builder.setCustomTitle(new MycontentView(context)); // or this method
builder.build().show();

实现 3 - ProgressDialog / AlertDialog

ProgressDialog progress = new ProgressDialog(context);
//set up progress dialog
progress.setView(new MyContentView(context));        //Can use this method
progress.setCustomTitle(new MyContentView(context)); // or this method
progress.show();

【讨论】:

【参考方案25】:

当我遇到它时,这是我的解决方案: 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();
        
    

挑战是在屏幕上保留对话框标题和消息 旋转,因为他们重置为默认的空字符串,虽然对话框仍然显示

有两种方法可以解决这个问题:

第一种方法: 在清单文件中的配置更改期间使利用对话框保持状态的活动:

android:configChanges="orientation|screenSize|keyboardHidden"

Google 不喜欢这种方法。

第二种方法: 在活动的onCreate() 方法上,如果savedInstanceState 不为空,您需要通过再次使用标题和消息重建ProgressDialogFragment 来保留DialogFragment,如下所示:

@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");
      
  

【讨论】:

【参考方案26】:

似乎太“又快又脏”而不是真的,所以请指出缺陷,但我发现有效的是......

在我的 AsyncTask 的 onPostExecute 方法中,我只是将进度对话框的“.dismiss”包装在一个 try/catch 块中(带有一个空的 catch),然后简单地忽略引发的异常。似乎做错了,但似乎没有不良影响(至少对于我随后所做的事情是启动另一个活动,将我长时间运行的查询的结果作为额外的传递)

【讨论】:

你是说Android平台告诉窗口泄漏时实际上没有内存泄漏? 我的进度对话框是活动类的成员变量,所以我假设当活动被销毁并重新创建时,它将被垃圾收集并且没有泄漏。我错了吗? 是的,我认为这是错误的。正如您所说,Activity 引用了Dialog。当配置改变时,第一个Activity被销毁,这意味着所有字段都设置为null。但是低级别的WindowManager 也有对Dialog 的引用(因为它还没有被解除)。新的Activity 尝试创建一个新的Dialog(在preExecute() 中),并且窗口管理器引发了一个致命异常,阻止您这样做。实际上,如果这样做,就无法彻底销毁Dialog,因此保留对初始Activity 的引用。我说的对吗?【参考方案27】:

最简单、最灵活的解决方案是使用AsyncTask 和对ProgressBar 的静态引用。这为方向改变问题提供了一种封装并因此可重复使用的解决方案。这个解决方案非常适合各种异步任务,包括互联网下载、与Services 通信以及文件系统扫描。该解决方案已在多个 android 版本和手机型号上进行了很好的测试。完整的演示可以在here 找到,特别感兴趣的是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();
    

【讨论】:

【参考方案28】:

当您更改方向时,Android 会终止该活动并创建新活动。 我建议对 Rx java 使用改造。它会自动处理崩溃。

改造调用时使用这些方法。

.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())

【讨论】:

以上是关于进度对话框和后台线程处于活动状态时如何处理屏幕方向更改?的主要内容,如果未能解决你的问题,请参考以下文章

如何处理后台线程上显示的 LWUIT 对话框

当应用程序处于未运行或后台状态时,如何处理推送通知有效负载而不点击它

使用选项卡活动和片段时如何处理后按

应用程序在后台或暂停时的位置更新

侦听firebase数据库节点时如何处理kotlin中的竞争条件

需要 iCloud 时如何处理首次启动体验?