【问题标题】:How does calling Snackbar.make() from non-UI thread work?从非 UI 线程调用 Snackbar.make() 是如何工作的?
【发布时间】:2016-09-27 09:24:49
【问题描述】:

我可以毫无问题地从后台线程调用Snackbar.make()。这让我感到惊讶,因为我认为 UI 操作只允许来自 UI 线程。但这绝对不是这里的情况。

究竟是什么让Snackbar.make() 与众不同?为什么当您从后台线程修改它时,它不会像任何其他 UI 组件一样导致异常?

【问题讨论】:

    标签: java android multithreading user-interface android-snackbar


    【解决方案1】:

    首先:make() 不执行任何 UI 相关操作,它只是创建一个新的Snackbar 实例。是对show() 的调用实际上将Snackbar 添加到视图层次结构中并执行其他危险的UI 相关任务。但是,您可以从任何线程安全地执行此操作,因为它被实现为在 UI 线程上安排任何显示或隐藏操作,无论哪个线程调用 show()

    更详细的答案让我们仔细看看Snackbar的源代码中的行为:


    让我们从你的电话show()开始吧:

    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }
    

    如您所见,对show() 的调用获取SnackbarManager 的实例,然后将持续时间和回调传递给它。 SnackbarManager 是一个单例。它负责显示、调度和管理Snackbar 的类。现在让我们继续在SnackbarManager 上实现show()

    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                // Means that the callback is already in the queue. We'll just update the duration
                mCurrentSnackbar.duration = duration;
    
                // If this is the Snackbar currently being shown, call re-schedule it's
                // timeout
                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
                scheduleTimeoutLocked(mCurrentSnackbar);
                return;
            } else if (isNextSnackbarLocked(callback)) {
                // We'll just update the duration
                mNextSnackbar.duration = duration;
            } else {
                // Else, we need to create a new record and queue it
                mNextSnackbar = new SnackbarRecord(duration, callback);
            }
    
            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
                // If we currently have a Snackbar, try and cancel it and wait in line
                return;
            } else {
                // Clear out the current snackbar
                mCurrentSnackbar = null;
                // Otherwise, just show it now
                showNextSnackbarLocked();
            }
        }
    }
    

    现在这个方法调用有点复杂。我不打算详细解释这里发生了什么,但总的来说,围绕它的synchronized 块确保了对show() 的调用的线程安全。

    synchronized 块内,如果您两次show() 相同的时间,经理会负责关闭当前显示的Snackbars 更新持续时间或重新安排时间,当然还会创建新的Snackbars。对于每个Snackbar,都会创建一个SnackbarRecord,其中包含最初传递给SnackbarManager的两个参数,持续时间和回调:

    mNextSnackbar = new SnackbarRecord(duration, callback);
    

    在上面的方法调用中,这发生在中间,在第一个 if 的 else 语句中。

    然而,唯一真正重要的部分——至少对于这个答案来说——就在底部,即对showNextSnackbarLocked()的调用。这就是魔法发生的地方,下一个 Snackbar 排队 - 至少有点。

    这是showNextSnackbarLocked()的源码:

    private void showNextSnackbarLocked() {
        if (mNextSnackbar != null) {
            mCurrentSnackbar = mNextSnackbar;
            mNextSnackbar = null;
    
            final Callback callback = mCurrentSnackbar.callback.get();
            if (callback != null) {
                callback.show();
            } else {
                // The callback doesn't exist any more, clear out the Snackbar
                mCurrentSnackbar = null;
            }
        }
    }
    

    正如您首先看到的,我们通过检查 mNextSnackbar 是否不为空来检查 Snackbar 是否排队。如果不是,我们将SnackbarRecord 设置为当前Snackbar 并从记录中检索回调。现在发生了一些事情,在进行一次简单的 null 检查以查看回调是否有效之后,我们在回调上调用 show(),这是在 Snackbar 类中实现的 - 而不是在 SnackbarManager 中实现的 - 实际显示Snackbar 在屏幕上。

    起初这可能看起来很奇怪,但它很有意义。 SnackbarManager 只负责跟踪Snackbars 的状态并协调它们,它并不关心Snackbar 的外观、显示方式甚至是什么,它只是调用show() 方法正确的回调在正确的时间告诉Snackbar展示自己。


    让我们倒带一下,到目前为止,我们从未离开过后台线程。 SnackbarManagershow() 方法中的 synchronized 块确保没有其他 Thread 可以干扰我们所做的一切,但是在主 Thread 上安排显示和关闭事件的内容仍然缺失。然而,当我们查看 Snackbar 类中回调的实现时,这种情况现在会改变:

    private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
        @Override
        public void show() {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
        }
    
        @Override
        public void dismiss(int event) {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
        }
    };
    

    因此,在回调中,一条消息被发送到静态处理程序,MSG_SHOW 显示SnackbarMSG_DISMISS 再次隐藏它。 Snackbar 本身作为有效负载附加到消息中。现在,只要我们查看该静态处理程序的声明,我们就差不多完成了:

    private static final Handler sHandler;
    private static final int MSG_SHOW = 0;
    private static final int MSG_DISMISS = 1;
    
    static {
        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message message) {
                switch (message.what) {
                    case MSG_SHOW:
                        ((Snackbar) message.obj).showView();
                        return true;
                    case MSG_DISMISS:
                        ((Snackbar) message.obj).hideView(message.arg1);
                        return true;
                }
                return false;
            }
        });
    }
    

    所以这个处理程序在 UI 线程上运行,因为它是使用 UI 循环器创建的(如 Looper.getMainLooper() 所示)。消息的有效负载 - Snackbar - 被强制转换,然后根据消息的类型在 Snackbar 上调用 showView()hideView()这两种方法现在都在 UI 线程上执行!

    这两种方法的实现都有些复杂,所以我不会详细介绍它们中究竟发生了什么。但是很明显,这些方法负责将View 添加到视图层次结构中,在它出现和消失时对其进行动画处理,处理CoordinatorLayout.Behaviours 和其他有关UI 的内容。

    如果您有任何其他问题,请随时提出。


    滚动浏览我的答案,我意识到这比预期的要长方式,但是当我看到这样的源代码时,我情不自禁!我希望你能欣赏一个长而深入的回答,或者我可能只是浪费了几分钟的时间!

    【讨论】:

    • 不,您没有浪费时间,非常感谢 :)
    【解决方案2】:

    Snackbar.make 完全不会被非 ui 线程调用。它在其管理器中使用了一个处理程序,该处理程序在主循环器线程上运行,从而将调用者隐藏在它的底层复杂性中。

    【讨论】:

    • 经理中的Handler 与此无关。它仅用于通知SnackbarRecords 超时。 Snackbar 类中有一个单独的 Handler,它实际上负责显示或隐藏 Snackbar
    • 这仍然不等于 2 票。 Snackbar 是从任何线程调用的完全安全的形式。关键是它使用主循环器上的处理程序来完成它的工作。这就是原始问题所有者需要了解的内容。我不能驳回这些反对票,所以我也不打算与他们作斗争。时间
    【解决方案3】:

    只有创建视图层次结构的原始线程才能接触其视图。

    如果您使用 onPostExecute,您将能够访问视图

    protected void onPostExecute(Object object) { .. }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-11-24
      • 1970-01-01
      • 1970-01-01
      • 2012-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多