【问题标题】:How to handle Handler messages when activity/fragment is paused活动/片段暂停时如何处理处理程序消息
【发布时间】:2011-12-23 19:50:36
【问题描述】:

我的other posting略有不同

基本上,我的Fragment 中有一条消息Handler,它会收到一堆消息,这些消息可能会导致对话框被关闭或显示。

当应用程序进入后台时,我会收到一个onPause,但我的消息仍然会像预期的那样通过。但是,因为我使用的是片段,所以我不能只是关闭并显示对话框,因为这将导致 IllegalStateException

我不能只是关闭或取消允许状态丢失。

鉴于我有一个 Handler 我想知道是否有推荐的方法 在暂停状态下我应该如何处理消息。

我正在考虑的一个可能的解决方案是在暂停时记录通过的消息并在onResume 上播放它们。这有点令人不满意,我认为框架中必须有一些东西可以更优雅地处理。

【问题讨论】:

  • 您可以在片段的 onPause() 方法中删除处理程序中的所有消息,但存在恢复消息的问题,我认为这是不可能的。

标签: android android-fragments


【解决方案1】:

虽然 Android 操作系统似乎没有充分解决您的问题的机制,但我相信这种模式确实提供了一种相对简单的解决方法。

以下类是android.os.Handler 的包装器,它在活动暂停时缓冲消息并在恢复时播放它们。

确保您拥有的任何异步更改片段状态(例如提交、关闭)的代码仅从处理程序中的消息调用。

PauseHandler 类派生您的处理程序。

每当您的活动收到onPause() 调用PauseHandler.pause()onResume() 调用PauseHandler.resume()

将处理程序handleMessage() 的实现替换为processMessage()

提供storeMessage() 的简单实现,它总是返回true

/**
 * 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);
        }
    }
}

下面是如何使用PausedHandler 类的简单示例。

单击按钮时,会向处理程序发送延迟消息。

当处理程序接收到消息(在 UI 线程上)时,它会显示 DialogFragment

如果没有使用 PausedHandler 类,并且在按下测试按钮启动对话框后按下主页按钮,则会显示 IllegalStateException。

public class FragmentTestActivity extends Activity {

    /**
     * Used for "what" parameter to handler messages
     */
    final static int MSG_WHAT = ('F' << 16) + ('T' << 8) + 'A';
    final static int MSG_SHOW_DIALOG = 1;

    int value = 1;

    final static class State extends Fragment {

        static final String TAG = "State";
        /**
         * Handler for this activity
         */
        public ConcreteTestHandler handler = new ConcreteTestHandler();

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);            
        }

        @Override
        public void onResume() {
            super.onResume();

            handler.setActivity(getActivity());
            handler.resume();
        }

        @Override
        public void onPause() {
            super.onPause();

            handler.pause();
        }

        public void onDestroy() {
            super.onDestroy();
            handler.setActivity(null);
        }
    }

    /**
     * 2 second delay
     */
    final static int DELAY = 2000;

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

        if (savedInstanceState == null) {
            final Fragment state = new State();
            final FragmentManager fm = getFragmentManager();
            final FragmentTransaction ft = fm.beginTransaction();
            ft.add(state, State.TAG);
            ft.commit();
        }

        final Button button = (Button) findViewById(R.id.popup);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final FragmentManager fm = getFragmentManager();
                State fragment = (State) fm.findFragmentByTag(State.TAG);
                if (fragment != null) {
                    // Send a message with a delay onto the message looper
                    fragment.handler.sendMessageDelayed(
                            fragment.handler.obtainMessage(MSG_WHAT, MSG_SHOW_DIALOG, value++),
                            DELAY);
                }
            }
        });
    }

    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
    }

    /**
     * Simple test dialog fragment
     */
    public static class TestDialog extends DialogFragment {

        int value;

        /**
         * Fragment Tag
         */
        final static String TAG = "TestDialog";

        public TestDialog() {
        }

        public TestDialog(int value) {
            this.value = value;
        }

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

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            final View inflatedView = inflater.inflate(R.layout.dialog, container, false);
            TextView text = (TextView) inflatedView.findViewById(R.id.count);
            text.setText(getString(R.string.count, value));
            return inflatedView;
        }
    }

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

        /**
         * Activity instance
         */
        protected Activity activity;

        /**
         * Set the activity associated with the handler
         * 
         * @param activity
         *            the activity to set
         */
        final void setActivity(Activity activity) {
            this.activity = activity;
        }

        @Override
        final protected boolean storeMessage(Message message) {
            // All messages are stored by default
            return true;
        };

        @Override
        final protected void processMessage(Message msg) {

            final Activity activity = this.activity;
            if (activity != null) {
                switch (msg.what) {

                case MSG_WHAT:
                    switch (msg.arg1) {
                    case MSG_SHOW_DIALOG:
                        final FragmentManager fm = activity.getFragmentManager();
                        final TestDialog dialog = new TestDialog(msg.arg2);

                        // We are on the UI thread so display the dialog
                        // fragment
                        dialog.show(fm, TestDialog.TAG);
                        break;
                    }
                    break;
                }
            }
        }
    }
}

我在PausedHandler 类中添加了一个storeMessage() 方法,以防即使活动暂停也应立即处理任何消息。如果处理了一条消息,则应返回 false 并丢弃该消息。

【讨论】:

  • 很好的解决方案,很好用。不禁想到框架应该处理这个问题。
  • 如何将回调传递给DialogFragment?
  • 我不确定我是否理解 Malachiasz 的问题,请您详细说明。
  • 这是一个非常优雅的解决方案!除非我错了,因为resume 方法在技术上使用sendMessage(msg) 可能在之前(或在循环的迭代之间)有其他线程排队消息,这意味着存储的消息可能与到达的新消息交错。不确定这是否是一个大问题。也许使用sendMessageAtFrontOfQueue(当然还有向后迭代)可以解决这个问题?
  • 我认为这种方法可能并不总是有效 - 如果活动被操作系统破坏,则待处理的消息列表在恢复后将为空。
【解决方案2】:

quickdraw 优秀的 PauseHandler 的一个稍微简单的版本是

/**
 * 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
     */
    private final List<Message> messageQueueBuffer = Collections.synchronizedList(new ArrayList<Message>());

    /**
     * Flag indicating the pause state
     */
    private Activity activity;

    /**
     * Resume the handler.
     */
    public final synchronized void resume(Activity activity) {
        this.activity = activity;

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

    /**
     * Pause the handler.
     */
    public final synchronized void pause() {
        activity = null;
    }

    /**
     * Store the message if we have been paused, otherwise handle it now.
     *
     * @param msg   Message to handle.
     */
    @Override
    public final synchronized void handleMessage(Message msg) {
        if (activity == null) {
            final Message msgCopy = new Message();
            msgCopy.copyFrom(msg);
            messageQueueBuffer.add(msgCopy);
        } else {
            processMessage(activity, msg);
        }
    }

    /**
     * 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 activity  Activity owning this Handler that isn't currently paused.
     * @param message   Message to be handled
     */
    protected abstract void processMessage(Activity activity, Message message);

}

它确实假设您总是希望存储离线消息以进行重播。并将 Activity 作为输入提供给#processMessages,因此您无需在子类中管理它。

【讨论】:

  • 为什么你的 resume()pause()handleMessage synchronized
  • 因为您不希望在#handleMessage 期间调用#pause,而在#handleMessage 中使用它时突然发现该活动为空。这是跨共享状态的同步。
  • @William 您能否详细解释一下为什么需要在 PauseHandler 类中进行同步?似乎这个类只在一个线程中工作,即 UI 线程。我猜想在#handleMessage 期间无法调用#pause,因为它们都在UI 线程中工作。
  • @William 你确定吗? HandlerThread handlerThread = new HandlerThread("mHandlerNonMainThread"); handlerThread.start(); Looper looperNonMainThread = handlerThread.getLooper();处理程序 handlerNonMainThread = new Handler(looperNonMainThread, new Callback() { public boolean handleMessage(Message msg) { return false; } });
  • 对不起@swooby,我不关注。我确定什么?你贴的代码 sn-p 的目的是什么?
【解决方案3】:

在我的项目中,我使用观察者设计模式来解决这个问题。在 Android 中,广播接收器和意图是这种模式的实现。

我要做的是创建一个 BroadcastReceiver,我在片段/活动的 onResume 中注册并在片段/活动的 onPause 中取消注册。 在 BroadcastReceiver 的方法 onReceive 中,我将所有需要运行的代码放入 - BroadcastReceiver - 接收通常发送到您的应用程序的 Intent(消息)。要提高片段可以接收的意图类型的选择性,您可以使用意图过滤器,如下例所示。

这种方法的一个优点是 Intent(消息)可以从您的应用程序的任何地方发送(在您的片段顶部打开的对话框、异步任务、另一个片段等) .参数甚至可以作为 Intent Extras 传递。

另一个优点是这种方法与任何 Android API 版本兼容,因为在 API 级别 1 上引入了 BroadcastReceivers 和 Intents。

您不需要对应用的清单文件设置任何特殊权限,除非您打算使用 sendStickyBroadcast(您需要在其中添加 BROADCAST_STICKY)。

public class MyFragment extends Fragment { 

    public static final String INTENT_FILTER = "gr.tasos.myfragment.refresh";

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {

        // this always runs in UI Thread 
        @Override
        public void onReceive(Context context, Intent intent) {
            // your UI related code here

            // you can receiver data login with the intent as below
            boolean parameter = intent.getExtras().getBoolean("parameter");
        }
    };

    public void onResume() {
        super.onResume();
        getActivity().registerReceiver(mReceiver, new IntentFilter(INTENT_FILTER));

    };

    @Override
    public void onPause() {
        getActivity().unregisterReceiver(mReceiver);
        super.onPause();
    }

    // send a broadcast that will be "caught" once the receiver is up
    protected void notifyFragment() {
        Intent intent = new Intent(SelectCategoryFragment.INTENT_FILTER);
        // you can send data to receiver as intent extras
        intent.putExtra("parameter", true);
        getActivity().sendBroadcast(intent);
    }

}

【讨论】:

  • 如果在暂停状态期间调用 notifyFragment() 中的 sendBroadcast(),则 unregisterReceiver() 将已被调用,因此不会有接收器来捕捉该意图。如果没有代码可以立即处理,Android系统不会丢弃intent吗?
  • 我觉得green robots eventbus置顶帖是这样的,酷。
【解决方案4】:

这里有一种稍微不同的方法来解决在回调函数中进行片段提交并避免 IllegalStateException 问题的问题。

首先创建一个自定义的可运行接口。

public interface MyRunnable {
    void run(AppCompatActivity context);
}

接下来,创建一个用于处理 MyRunnable 对象的片段。如果 MyRunnable 对象是在 Activity 暂停后创建的,例如如果屏幕旋转,或者用户按下主页按钮,则将其放入队列中以供以后使用新上下文进行处理。由于 setRetain 实例设置为 true,因此队列在任何配置更改后都可以保留。方法 runProtected 在 UI 线程上运行以避免带有 isPaused 标志的竞争条件。

public class PauseHandlerFragment extends Fragment {

    private AppCompatActivity context;
    private boolean isPaused = true;
    private Vector<MyRunnable> buffer = new Vector<>();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = (AppCompatActivity)context;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public void onPause() {
        isPaused = true;
        super.onPause();
    }

    @Override
    public void onResume() {
        isPaused = false;
        playback();
        super.onResume();
    }

    private void playback() {
        while (buffer.size() > 0) {
            final MyRunnable runnable = buffer.elementAt(0);
            buffer.removeElementAt(0);
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    //execute run block, providing new context, incase 
                    //Android re-creates the parent activity
                    runnable.run(context);
                }
            });
        }
    }
    public final void runProtected(final MyRunnable runnable) {
        context.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if(isPaused) {
                    buffer.add(runnable);
                } else {
                    runnable.run(context);
                }
            }
        });
    }
}

最后,片段可以在主应用程序中使用如下:

public class SomeActivity extends AppCompatActivity implements SomeListener {
    PauseHandlerFragment mPauseHandlerFragment;

    static class Storyboard {
        public static String PAUSE_HANDLER_FRAGMENT_TAG = "phft";
    }

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        ...

        //register pause handler 
        FragmentManager fm = getSupportFragmentManager();
        mPauseHandlerFragment = (PauseHandlerFragment) fm.
            findFragmentByTag(Storyboard.PAUSE_HANDLER_FRAGMENT_TAG);
        if(mPauseHandlerFragment == null) {
            mPauseHandlerFragment = new PauseHandlerFragment();
            fm.beginTransaction()
                .add(mPauseHandlerFragment, Storyboard.PAUSE_HANDLER_FRAGMENT_TAG)
                .commit();
        }

    }

    // part of SomeListener interface
    public void OnCallback(final String data) {
        mPauseHandlerFragment.runProtected(new MyRunnable() {
            @Override
            public void run(AppCompatActivity context) {
                //this block of code should be protected from IllegalStateException
                FragmentManager fm = context.getSupportFragmentManager();
                ...
            }
         });
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-13
    • 2012-03-25
    • 2014-06-20
    • 2018-03-26
    • 2016-05-12
    • 2012-01-30
    • 2021-12-05
    • 2020-03-10
    相关资源
    最近更新 更多