有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

当进度对话框和后台线程处于活动状态时,安卓如何处理屏幕方向的变化?

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

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

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

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.安卓.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244): 安卓.view.WindowLeaked: Activity MyAct has leaked window com.安卓.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244):     at 安卓.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager(  244):     at 安卓.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager(  244):     at 安卓.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager(  244):     at 安卓.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager(  244):     at 安卓.app.Dialog.show(Dialog.java:212)
E/WindowManager(  244):     at 安卓.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager(  244):     at 安卓.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 安卓.view.View.performClick(View.java:2129)
E/WindowManager(  244):     at 安卓.view.View.onTouchEvent(View.java:3543)
E/WindowManager(  244):     at 安卓.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager(  244):     at 安卓.view.View.dispatchTouchEvent(View.java:3198)

我曾试图关闭onSaveInstanceState中的进度对话框,但这只会防止立即崩溃。后台线程仍在运行,UI处于部分绘制状态。需要在重新开始工作之前关闭整个应用程序


共 (6) 个答案

  1. # 1 楼答案

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

    我建议让mHandler不稳定,并在方向改变时更新它

  2. # 2 楼答案

    编辑:正如Dianne Hackborn(又名a.a.hackbod)在本StackOverflow 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)方法

  3. # 3 楼答案

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

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

  4. # 4 楼答案

    我为这些问题提出了一个坚如磐石的解决方案,它符合“安卓方式”。我所有的长期运行操作都使用IntentService模式

    也就是说,我的活动广播意图,意图服务完成工作,将数据保存在数据库中,然后广播粘性意图。粘性部分很重要,因此即使活动在用户启动工作后暂停,并且错过了来自IntentService的实时广播,我们仍然可以响应并从调用活动中获取数据ProgressDialog可以很好地与onSaveInstanceState()配合使用这个模式

    基本上,您需要保存一个标志,表明您在保存的实例包中运行了一个进度对话框不要保存进度对话框对象,因为这会泄露整个活动。为了对进度对话框有一个持久的句柄,我将其作为弱引用存储在应用程序对象中。在方向改变或任何其他导致活动暂停(电话、用户点击主页等)然后继续的情况下,我会关闭旧对话框,并在新创建的活动中重新创建一个新对话框

    对于不确定的进度对话框,这很容易。对于进度条样式,您必须将最后一次已知的进度放在捆绑包中,以及您在活动中本地使用的任何信息,以跟踪进度。在恢复进度时,您将使用此信息在与之前相同的状态下重新生成进度条,然后根据当前状态进行更新

    总之,将长期运行的任务放入IntentService,再加上明智地使用onSaveInstanceState(),可以让您高效地跟踪对话框,然后在整个活动生命周期事件中恢复。下面是活动代码的相关部分。您还需要BroadcastReceiver中的逻辑来适当地处理粘性意图,但这超出了本文的范围

    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));
        }
    }
    
  5. # 5 楼答案

    我的解决方案是扩展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);
        }
    }
    
  6. # 6 楼答案

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

    我认为,如果潜在的问题是该程序无法在onDestroy()中生存,那么公认的解决方案只是一种变通方法,会使该程序存在严重的其他问题和漏洞。请记住,Android框架明确指出,由于您无法控制的情况,您的活动几乎随时都有被破坏的风险。因此,无论出于何种原因,您的活动都必须能够在onDestroy()和随后的onCreate()中存活,而不仅仅是屏幕方向的改变

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