深入原始碼學習 Android data binding 之:回撥通知管理器 CallbackRegistry 解析

LemonYang發表於2019-03-04

在android data binding庫裡面有三個版塊我認為是掌握這個庫的核心點,分別是:

  • 註解定義和使用
  • 註解處理器的實現
  • 監聽註冊與回撥

在前面的文章當中我們已經分別分析了data binding當中的註解的使用和一個很關鍵的ViewDataBinding的類及apt編譯期生成的相應子類的解析。如果你沒看過的話可以先去看一下前面的文章。
深入原始碼學習 android data binding 之:data binding 註解

深入原始碼學習 android data binding 之:ViewDataBinding

CallbackRegistry 這個類就是掌控監聽註冊與回撥的一個核心點。在data binding的變數setter方法和rebind的過程中都是通過CallbackRegistry作為核心邏輯的部分。從類的定義註釋我們可以知道,這其實一個工具類,負責儲存和通知我們的回撥介面。並且這個類本身就包括了一個常規的邏輯——在接收到通知之後取消這個回撥的註冊。接下來,我會根據個人的理解對這個類的設計和API進行簡單的介紹。

類設計簡述

在以前我寫介面回撥的程式碼的時候通常都是進行"簡單定義回撥介面、介面依賴注入、回撥處理"這樣一個流程的,所以程式碼結構會比較零散。而 CallbackRegistry 生來就是一個管理者,管理介面的註冊和回撥。它把介面的定義和介面回撥之後的處理邏輯交給呼叫者去實現,而本身只負責管理這個介面以及完成回撥分發的邏輯。我們先節選一部分原始碼進行說明。

/**
* 一個CallbackRegistry的例項用於管理一種型別的回撥介面
* @param <C> 對應的是我們的回撥介面型別
* @param <T> 通知傳送者的型別,一般就是CallbackRegistry例項所在的類
* @param <A> 用於介面回撥時額外的引數
*/
public class CallbackRegistry<C, T, A> implements Cloneable {
    ...
    // 通過list管理我們所有註冊的回撥
    private List<C> mCallbacks = new ArrayList<C>();
    ...
    // 當我們在構造一個CallbackRegistry的例項的時候,我們需要傳入一個NotifierCallback物件
    // 這個物件就是用於掌控具體介面回撥之後的邏輯處理的
    // 這是一個抽象類,由呼叫方自主實現,類定義可以看下面
    public CallbackRegistry(NotifierCallback<C, T, A> notifier) {
        mNotifier = notifier;
    }
    ...

    // 泛型引數的定義跟上面相同
    public abstract static class NotifierCallback<C, T, A> {
        /**
         * 當我們呼叫CallbackRegistry#notifyCallbacks(Object, int, Object)的方法的時候最終會回撥這個方法
         * @param callback The callback to notify.
         * @param sender The opaque sender object.
         * @param arg The opaque notification parameter.
         * @param arg2 An opaque argument passed in
         *        {@link CallbackRegistry#notifyCallbacks}
         * @see CallbackRegistry#CallbackRegistry(CallbackRegistry.NotifierCallback)
         */
        public abstract void onNotifyCallback(C callback, T sender, int arg, A arg2);
    }
}複製程式碼

回撥介面管理

位標記回撥介面的狀態

在CallbackRegistry類當中,我們可以不斷的新增回撥介面,當我們要移除介面的時候,並不是直接把回撥從list集合中移除,而是判斷當前通知是否正在傳送。如果通知正在傳送,那麼會通過long型別裡面的每一個位標誌介面的取消狀態。這樣子可以避免併發修改list帶來的執行緒安全的問題。如下所示:

/**
* A bit flag for the first 64 listeners that are removed during notification.
* The lowest significant bit corresponds to the 0th index into mCallbacks.
* For a small number of callbacks, no additional array of objects needs to
* be allocated.
*/
private long mFirst64Removed = 0x0;

/**
* Bit flags for the remaining callbacks that are removed during notification.
* When there are more than 64 callbacks and one is marked for removal, a dynamic
* array of bits are allocated for the callbacks.
*/
private long[] mRemainderRemoved;複製程式碼

預設的情況下,CallbackRegistry類只會使用一個long型別標誌介面的狀態(是否被移除),一個long值可以標記64個介面的狀態。在介面數超出64個之後會使用一個動態的long型別的陣列mRemainderRemoved負責處理。

// 設定移除介面回撥的標誌位
// index對應的是回撥介面在list中的位置
private void setRemovalBit(int index) {
    if (index < Long.SIZE) {
        // It is in the first 64 callbacks, just check the bit.
        // 通過位移運算更新位的值
        final long bitMask = 1L << index;
        mFirst64Removed |= bitMask;
    } else {
        final int remainderIndex = (index / Long.SIZE) - 1;
        if (mRemainderRemoved == null) {
            mRemainderRemoved = new long[mCallbacks.size() / Long.SIZE];
        } else if (mRemainderRemoved.length < remainderIndex) {
            // need to make it bigger
            // 動態的調整陣列的大小
            long[] newRemainders = new long[mCallbacks.size() / Long.SIZE];
            System.arraycopy(mRemainderRemoved, 0, newRemainders, 0, mRemainderRemoved.length);
            mRemainderRemoved = newRemainders;
        }
        final long bitMask = 1L << (index % Long.SIZE);
        mRemainderRemoved[remainderIndex] |= bitMask;
    }
}複製程式碼

新增介面

/**
* 當我們需要新增的介面已經存在的時候,不會重複新增
* 要注意的是,在CallbackRegistry裡面,開放的都是例項的synchronized方法
* 而這在Java中這相當於 synchronized(this)塊,而這是可重入的鎖。
* 而CallbackRegistry在通知回撥的時候又通過了long型別的位來處理,所以新增新的回撥並不會影響當前的通知
* @param callback The callback to add.
*/
public synchronized void add(C callback) {
    if (callback == null) {
        throw new IllegalArgumentException("callback cannot be null");
    }
    int index = mCallbacks.lastIndexOf(callback);
    if (index < 0 || isRemoved(index)) {
        mCallbacks.add(callback);
    }
}

/**
     * Returns true if the callback at index has been marked for removal.
     *
     * @param index The index into mCallbacks to check.
     * @return true if the callback at index has been marked for removal.
     */
    private boolean isRemoved(int index) {
        if (index < Long.SIZE) {
            // It is in the first 64 callbacks, just check the bit.
            final long bitMask = 1L << index;
            return (mFirst64Removed & bitMask) != 0;
        } else if (mRemainderRemoved == null) {
            // It is after the first 64 callbacks, but nothing else was marked for removal.
            return false;
        } else {
            final int maskIndex = (index / Long.SIZE) - 1;
            if (maskIndex >= mRemainderRemoved.length) {
                // There are some items in mRemainderRemoved, but nothing at the given index.
                return false;
            } else {
                // There is something marked for removal, so we have to check the bit.
                final long bits = mRemainderRemoved[maskIndex];
                final long bitMask = 1L << (index % Long.SIZE);
                return (bits & bitMask) != 0;
            }
        }
    }複製程式碼

關於可重入鎖的概念,這裡稍微提及一下:對於帶有synchronized關鍵字的例項方法,在Java中這相當於 synchronized(this)塊,因此這些方法都是在同一個管程物件(即this)上同步的。如果一個執行緒持有某個管程物件上的鎖,那麼它就有權訪問所有在該管程物件上同步的塊。這就叫可重入。若執行緒已經持有鎖,那麼它就可以重複訪問所有使用該鎖的程式碼塊。

移除介面

上面提及到了,在CallbackRegistry中,回撥的移除並不是立即從list中直接將物件刪除的,而是通過位標誌來管理狀態的。

/**
* 移除回撥
* 當通知正在傳送的時候,不會將介面移除,而只是標記移除的狀態
* 在通知傳送完畢之後再將回撥介面從list當中移除
* @param callback The callback to remove.
*/
public synchronized void remove(C callback) {
    //mNotificationLevel是一個成員變數,這個變數會在每次通知傳送前+1,通知傳送完畢之後又-1
    //所以當mNotificationLevel不為0的時候,表明通知正在傳送中
    if (mNotificationLevel == 0) {
        mCallbacks.remove(callback);
    } else {
        int index = mCallbacks.lastIndexOf(callback);
        if (index >= 0) {
            setRemovalBit(index);
        }
    }
}複製程式碼

清空容器

/**
* Removes all callbacks from the list.
*/
public synchronized void clear() {
    if (mNotificationLevel == 0) {
        mCallbacks.clear();
    } else if (!mCallbacks.isEmpty()) {
        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
            setRemovalBit(i);
        }
    }
}複製程式碼

通過上面的幾處分析,我們會發現,CallbackRegistry這個類靈活的運用了位狀態和synchronized關鍵字來處理了併發狀態下的list容器的管理。這時得CallbackRegistry有一個很關鍵的特點—— 它是支援在通知傳送過程中不打斷通知流程的可重入的修改我們的回撥介面集 。關於這一點,大家可以在細細的去看一下這個類的原始碼,慢慢體會。

回撥通知管理

CallbackRegistry的回撥通知有一個很顯著的特點,那就是使用遞迴演算法分發通知。

/**
* Notify all callbacks.
* 通知所有的回撥,這個是通知回撥的入口,最終會通過呼叫NotifierCallback#onNotifyCallback()方法呼叫自己實現的具體邏輯
* @param sender The originator. This is an opaque parameter passed to
* {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, Object)}
* @param arg An opaque parameter passed to
* {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, Object)}
* @param arg2 An opaque parameter passed to
* {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, Object)}
*/
public synchronized void notifyCallbacks(T sender, int arg, A arg2) {
    // 通過一個int值標誌通知傳送的層級,每次傳送通知之前都會加1
    mNotificationLevel++;
    // 這個方法通過遞迴演算法去完成回撥通知
    notifyRecurse(sender, arg, arg2);
    mNotificationLevel--;
    // 當所有的通知分發完畢之後,將之前標記的需要移除的介面從容器中移除
    if (mNotificationLevel == 0) {
        if (mRemainderRemoved != null) {
            for (int i = mRemainderRemoved.length - 1; i >= 0; i--) {
                final long removedBits = mRemainderRemoved[i];
                if (removedBits != 0) {
                    removeRemovedCallbacks((i + 1) * Long.SIZE, removedBits);
                    mRemainderRemoved[i] = 0;
                }
            }
        }
        if (mFirst64Removed != 0) {
            removeRemovedCallbacks(0, mFirst64Removed);
            mFirst64Removed = 0;
        }
    }
}

/**
* 下面主要就是兩個方法的呼叫,並且兩個方法最終都會發起通知回撥,這裡需要解釋一下為什麼會這樣子做
* 在類裡面我們是通過long型別的每一個bit標誌對應的介面的移除狀態的,當我們的介面超過64個之後就會通過繼續增加long陣列來繼續標誌
* 但是有可能我們的介面數是超過64的,但是並沒有做過移除的操作,所以並不會新建long陣列去記錄標誌位資訊
* 也就是我們的介面跟標誌位結合來看會存在兩種情況,一種是最大標記位之前的介面,一種是最大標誌位之後沒有被標記的介面.
* 所以notifyRemainder()方法通知的是那些從開始到存在的最大標識位之前的介面
* notifyCallbacks()方法通知的是最大標誌位之後到介面總數之間的介面
* 如果上面我的表述看不明白的話可以看下面的圖片
*/
private void notifyRecurse(T sender, int arg, A arg2) {
    final int callbackCount = mCallbacks.size();
    final int remainderIndex = mRemainderRemoved == null ? -1 : mRemainderRemoved.length - 1;

    // Now we've got all callbakcs that have no mRemainderRemoved value, so notify the others.
    notifyRemainder(sender, arg, arg2, remainderIndex);

    // notifyRemainder notifies all at maxIndex, so we'd normally start at maxIndex + 1
    // However, we must also keep track of those in mFirst64Removed, so we add 2 instead:
    final int startCallbackIndex = (remainderIndex + 2) * Long.SIZE;

    // The remaining have no bit set
    notifyCallbacks(sender, arg, arg2, startCallbackIndex, callbackCount, 0);
}

// 下面這裡就是我們非常熟悉的遞迴演算法了
private void notifyRemainder(T sender, int arg, A arg2, int remainderIndex) {
    if (remainderIndex < 0) {
        notifyFirst64(sender, arg, arg2);
    } else {
        final long bits = mRemainderRemoved[remainderIndex];
        final int startIndex = (remainderIndex + 1) * Long.SIZE;
        final int endIndex = Math.min(mCallbacks.size(), startIndex + Long.SIZE);
        notifyRemainder(sender, arg, arg2, remainderIndex - 1);
        notifyCallbacks(sender, arg, arg2, startIndex, endIndex, bits);
    }
}

/**
* 從startIndex到endIndex迴圈發起通知回撥
* bits用以標記每一個位對應的介面是否已經被移除。當bits是0的時候表示所有的通知都需要通知
*/
private void notifyCallbacks(T sender, int arg, A arg2, final int startIndex,final int endIndex, final long bits) {
    // 從到一個bit開始,通過位與操作判斷bits對應的位是0還是1(1表示移除標誌)
    // 每一輪之後bitMask左移一位,用此判斷每個位對應的狀態
    long bitMask = 1;
    for (int i = startIndex; i < endIndex; i++) {
        if ((bits & bitMask) == 0) {
            mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2);
        }
        bitMask <<= 1;
    }
}

private void notifyFirst64(T sender, int arg, A arg2) {
    final int maxNotified = Math.min(Long.SIZE, mCallbacks.size());
    notifyCallbacks(sender, arg, arg2, 0, maxNotified, mFirst64Removed);
}複製程式碼

深入原始碼學習 Android data binding 之:回撥通知管理器 CallbackRegistry 解析

如果仔細看了上面的原始碼以及註釋的話應該能夠明白整個回撥通知流程是怎麼走的了,最終都會呼叫 NotifierCallback#onNotifyCallback() 方法,這就是由我們在使用CallbackRegistry的時候必須建立並傳入的NotifierCallback物件。

關於CallbackRegistry的實際使用,我們在前面的ViewDataBinding的分析文章裡面已經提及到,這裡不再過多陳述。

感謝你寶貴的時間閱讀這篇文章,如果你喜歡的話可以點贊收藏,也可以關注我的賬號。我的個人主頁淺唱android也會更新我的文章。接下來我還會繼續分析android data binding這個庫,並在最後進行總結和簡單的實踐分析。

相關文章