从非UI线程调用Snackbar.make()的工作原理是什么?

18

我可以在后台线程中调用Snackbar.make()而不会出现任何问题。这让我感到惊讶,因为我以为只有UI线程才能执行UI操作,但是这里显然不是这种情况。

那么Snackbar.make()究竟有什么不同之处呢?为什么在后台线程修改它不像其他UI组件那样会引发异常呢?

3个回答

26
首先: 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块内,管理器负责解除当前显示的Snackbars,更新持续时间或重新安排如果您两次show()相同的内容,当然还包括创建新的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;
        }
    }
}

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

起初这可能看起来很奇怪,但这非常有道理。 SnackbarManager 只负责跟踪 Snackbar 的状态并协调它们,它不关心 Snackbar 的外观、如何显示或甚至是什么,它只在正确的时刻调用正确的回调上的 show() 方法,告诉 Snackbar 显示自己。


让我们倒回一下,直到现在我们从未离开过后台线程。 SnackbarManagershow() 方法中的 synchronized 块确保没有其他线程会干扰我们所做的一切,但是什么会在主线程上安排显示和解除显示事件呢?然而,现在当我们查看 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来显示Snackbar,或者是MSG_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 looper(通过 Looper.getMainLooper() 指示),所以此处理程序在UI线程上运行。消息的有效载荷 - Snackbar - 被转换然后根据消息类型调用 Snackbar 上的 showView() 或者 hideView()现在,这两种方法都在UI线程上执行!

这两种方法的实现有点复杂,因此我不会详细介绍每个方法中究竟发生了什么。然而,显然这些方法负责将 View 添加到视图层次结构中,在其出现和消失时对其进行动画处理,处理 CoordinatorLayout.Behaviours 和其他与UI相关的事项。

如果您还有任何问题,请随时询问。


浏览我的答案时,我意识到这个答案变得比预计的要长得多,但是当我看到这样的源代码时,我忍不住!我希望您欣赏这个深入的解答,或者也许我只是浪费了几分钟时间!


不,你没有浪费时间,非常感谢 :) - q126y

-1

Snackbar.make 完全可以在非 UI 线程中调用,它使用其管理器内部的处理程序,在主线程上运行,从而隐藏了调用者对其底层复杂性的影响。


管理器中的“Handler”在其中没有任何作用。它只是用于通知“SnackbarRecords”超时。在“Snackbar”类中有一个单独的“Handler”,它实际上负责显示或隐藏“Snackbar”。 - Xaver Kapeller
这仍然不足以构成2个反对票。Snackbar从任何线程调用都是完全安全的。关键在于它使用主循环器上的处理程序来执行其工作。这就是原问题所有者需要理解的。我无法取消这些反对票,因此也不会与它们斗争。 - Nazgul

-2

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

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

protected void onPostExecute(Object object) { .. }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接