fred-ye / summary

my blog
43 stars 9 forks source link

[Android] Activity处于不可交互状态或不可见状态时消息的处理 #27

Open fred-ye opened 10 years ago

fred-ye commented 10 years ago

在开发过程中经常会碰到这种情况,某一个Activity已经处于不可交互状态或者不可见状态时,这个Activity中定义的Handler却还没有处理完相关的任务,或者是启动的一个线程,线程尚未执行完毕。在通常这种情况下,由于有尚未处理的任务存在,该Handler不会被销毁,任务会执行。但如果这个Handler中涉及到刷新UI的相关操作,那么可能就有点麻烦了,特别是如果此时Activity已经是不可见状态,或已经被回收了,刷新UI变会抛出异常,导致程序崩溃。需要注意的是,当activity处于不可见状态,或者已销毁,对其刷UI的操作时并一定会出现异常,得看具体是做什么样的刷UI操作,Android真是神奇

下面给出一个例子,以下代码中Activity的布局非常简单,里面就只有一个按钮,当点击这个按钮后会有一个刷UI的操作,用来显示一个DialogFragment,只是这个操作会在6秒钟之后执行。代码如下:

public class TestPauseHandler extends Activity {
    private static final String TAG = "TestPauseHandler";
    private Button btnTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_pause_handler);
        btnTest = (Button)findViewById(R.id.btn_test);
        btnTest.setOnClickListener(listener);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.i(TAG, "-----------onSaveInstanceState---------");
    }

    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "-----------onDestory---------");
    }

    private View.OnClickListener listener = new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            new Handler().postDelayed(new Runnable() {

                @Override
                public void run() {
                    DialogFragment dialogFragment = new DialogFragment();
                    dialogFragment.show(getFragmentManager(),"dialog"); //show DialogFragment的源码里其实会执行Transaction的commit
                }
            }, 6000);
        }
    };
}

如果我们这样玩它: 1.点击这个按钮后,接着点HOME按钮,或者切到系统的Setting屏,我们会发现该activity的onSaveInstanceState方法执行了,然后过了一会,程序就Crash了。程序报的异常是

05-20 10:31:27.598    2445-2445/com.fred.testactivity E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.fred.testactivity, PID: 2445
    java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
            at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1328)
            at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1346)
            at android.app.BackStackRecord.commitInternal(BackStackRecord.java:728)
            at android.app.BackStackRecord.commit(BackStackRecord.java:704)

如果在点了按钮后, 按Back键,结束这个activity, 回到桌面, 会发现onDestroy方法调用了,过了一会,程序抛出异常。

    Process: com.fred.testactivity, PID: 2482
    java.lang.IllegalStateException: Activity has been destroyed
            at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1350)
            at android.app.BackStackRecord.commitInternal(BackStackRecord.java:728)
            at android.app.BackStackRecord.commit(BackStackRecord.java:704)
            at android.app.DialogFragment.show(DialogFragment.java:230)
            at com.fred.testactivity.TestPauseHandler$1$1.run(TestPauseHandler.java:49)

2.我们将刷UI操作的run方法中代码换成

Dialog dialog = new Dialog(TestPauseHandler.this);
dialog.show();

点击按钮后,接着点HOME按钮,或者切到系统的Setting屏,我们会发现该activity的onSaveInstanceState方法执行了,点Recent apps键,重新回到这个app, 发现dialog弹出来了,程序没有crash。

但是,如果在点了按钮后, 按Back键,结束这个activity, 回到桌面, 会发现onDestroy方法调用了,过了一会,程序抛出异常

Process: com.fred.testactivity, PID: 2331
    android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@15b4466a is not valid; is your activity running?
            at android.view.ViewRootImpl.setView(ViewRootImpl.java:562)
            at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
            at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
            at android.app.Dialog.show(Dialog.java:298)

3.将run方法中代码换成

btnTest.setText("Click again");

只是修改了Button上面的文字,接着做实验。 点击按钮后,接着点HOME按钮,或者切到系统的Setting屏,我们会发现该activity的onSaveInstanceState方法执行了,点Recent apps键,重新回到这个app, 发现dialog弹出来了,程序没有crash。

点击按钮后, 按Back键,结束这个activity, 回到桌面, 会发现onDestroy方法调用了,程序依旧没有Crash。

Android真是神奇,当activity处于不可见状态,或者已销毁,对其刷UI的操作时并一定会出现异常,得看具体是做什么样的刷UI操作。 想知道具体原因就得去看源码了。

在Android中刷UI的操作其实也是通过发送消息进行的。于是我们希望的结果是,在当前Activity处于不可交互状态时,若有消息没有处理完,先将消息缓存着,等到以后当此Activity恢复可交互状态时,再处理消息。 在Stackoverflow上面有这么一个问答,有高手给出了这么一个解决方案,非常优雅。在我们的项目中也用得到了采用。核心代码如下:

/**
 * Message Handler class that supports buffering up of messages when the
 * activity is paused i.e. in the background.
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    final Vector<Message> messageQueueBuffer = new Vector<Message>();

    /**
     * Flag indicating the pause state
     */
    private boolean paused;

    /**
     * Resume the handler
     */
    final public void resume() {
        paused = false;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.elementAt(0);
            messageQueueBuffer.removeElementAt(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler
     */
    final public void pause() {
        paused = true;
    }

    /**
     * Notification that the message is about to be stored as the activity is
     * paused. If not handled the message will be saved and replayed when the
     * activity resumes.
     * 
     * @param message
     *            the message which optional can be handled
     * @return true if the message is to be stored
     */
    protected abstract boolean storeMessage(Message message);

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     * 
     * @param message
     *            the message to be handled
     */
    protected abstract void processMessage(Message message);

    /** {@inheritDoc} */
    @Override
    final public void handleMessage(Message msg) {
        if (paused) {
            if (storeMessage(msg)) {
                Message msgCopy = new Message();
                msgCopy.copyFrom(msg);
                messageQueueBuffer.add(msgCopy);
            }
        } else {
            processMessage(msg);
        }
    }
}

其思路是:

一个采用PauseHandler的实现如下:

public class TestPauseHandler extends Activity {
    private static final String TAG = "TestPauseHandler";
    private Button btnTest;
    private ConcreteTestHandler handler = new ConcreteTestHandler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_pause_handler);
        btnTest = (Button) findViewById(R.id.btn_test);
        btnTest.setOnClickListener(listener);

        handler.setActivity(this);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.i(TAG, "-----------onSaveInstanceState---------");
    }

    protected void onDestroy() {
        super.onDestroy();
        //清除所有的callback和message.
        handler.removeCallbacksAndMessages(null);
        Log.i(TAG, "-----------onDestory---------");
    }

    private View.OnClickListener listener = new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            Message message = Message.obtain();
            handler.sendMessageDelayed(message, 6000);
        }
    };

    @Override
    protected void onResume() {
        super.onResume();
        handler.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        handler.pause();
    }

    static class ConcreteTestHandler extends PauseHandler {
        protected Activity activity;

        public void setActivity(Activity activity) {
            this.activity = activity;
        }

        @Override
        protected boolean storeMessage(Message message) {
            return true;
        }

        @Override
        protected void processMessage(Message message) {
            final Activity activity = this.activity;
            if (activity != null) {
                DialogFragment dialogFragment = new DialogFragment();
                dialogFragment.show(activity.getFragmentManager(), "dialog"); //show DialogFragment的源码里其实会执行Transaction的commit
                Log.i(TAG, "PauseHandler processMessage execute -->");
            }
        }
    }
}

采用这种方式,问题便可以完美的解决