蘋果核 - Pairing Function —— vlayout 中使用數學的小場景

Longerian: 『關於vlayout,有人在 Github 上諮詢DelegateAdapter 的構造方法裏關於 hasConsistItemType 參數的含義。我稍微做了解釋,但爲了更好的介紹這一塊知識點,我想起了之前團隊裏的同學(@Villadora)在設計這一塊時的一個巧妙的處理,特此將其中的奧祕分享出來。本文原作者是Villadora,我轉載並做了少許修改。』

遇到的問題

在設計DelegateAdapter的時候,需要一個設計讓開發更簡潔,希望融合多個Adapters到一個Adapter,這樣開發者不需要寫if/else來出了各種類型和佈局。
類似下面的代碼,通過position經過offset的處理,傳遞給被delegated的adapter,這樣很簡單,也沒有問題。

class DelegateAdapter extends Adapter {
    public void onBindViewHolder(ViewHolder holder, int position) {
        Adapter subAdapter = findSubAdapter(position);
        subAdapter.onBindViewHolder(holder, position - subAdapter.getStartPosition());
    }
}

這樣做的目的是爲了讓subAdapter想正常的Adapter一樣編寫,但是可以多個混合在一起傳遞給RecyclerView。那麼除了onBindViewHolder之外,還有別的也要處理,比如itemId、itemType。

這裏以itemType爲例:

public int getItemViewType(int position) {
  Adapter subAdapter = findSubAdapter(position);

  int itemType = subAdapter.getItemViewType(position - subAdapter.getStartPosition());
}

這裏就存在了一個問題: 由於subAdapter會有多個,而每個subAdapter都應該互相獨立而不影響的。也就是說他們的itemType不一定會統一,可能兩個subAdapter的itemType都返回0,但在內部實現上,實際對應的View卻是不一樣的。如果 DelegateAdapter 不做處理就返回,那麼RecyclerView中的Recycler就會緩存錯誤的View類型,並最終導致出錯crash。

所以一定要區分不同的subAdapter, 這裏我給每個subAdapter加上了一個unique id。這樣一組(uid, itemType)在 DelegateAdapter 中肯定是唯一的了。

信息的數學抽象

現在問題來了,getItemViewType() 要求返回的是一個int,而目前能標示唯一性的是一組pair (uid, itemType)。是不能返回Pair的。需要把這兩個數字合併成一個int,並且有:

newItemType = f(uid, itemType)
When u1 != u2 || itemType1 != itemType2; f(u1, itemType1) != f(u2, itemType2)

也就是說需要這樣一個函數將pair轉化爲int,並且pair不相等時,函數結果肯定不相等。

由於每個subAdapter的itemType的總數是未知的,理論上是可能很大的數,所以預分段的辦法是行不通的。

並且在

public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   findSubAdapter?
}

方法中時拿不到position的,而只有viewType,意味着如果想找到對應的subAdapter,需要subAdapter的unique id。也就是說要從生成的viewType的信息中提取unique id。

這意味着之前的映射函數 f(uid, itemType) 必須是可逆的。

{uid, itemType} = f -1(newItemType)

第一感覺由於是可逆查詢,兩方面的信息都需要保留,所以想到的是內建Map去保留Pair到一個分配的newItemType的映射。這樣實現時可以,但是存在着當subAdapter改變之後,需要去從這個Map中刪除掉過期的信息,否則雖然數據量不大,但是儲存單調增長,始終會留下隱患。而subAdapter的改變實際上是會在很低地方都有可能發生,甚至可能內部就改變itemType而不通知 DelegateAdapter。即使實現監聽器,整個冗餘代碼也會很多。

而另一種方案是將兩個數字中可能的較小值做爲prefix,把int值的最高位幾位做mask然後來存放這個prefix。但這樣做存在着如果itemType或者unique id增長到很大,超過mask的範圍的時候,會發生prefix溢出或者被複寫;雖然這個值可能大到 2^28;但在理論上還是有這樣的可能性。

有沒有什麼辦法能夠完美的解決這個問題呢? 回頭梳理了下需求,實際上是要尋找一個function,能夠將包含兩個數字的pair轉化爲一個值,並且是可逆的。這不就是找一個將二維向量轉化爲一維向量的可逆函數嗎?有了這個思路,我想自己可以按某個順序給二維平面中的點做標記,那麼(x, y) => z 就完成了,但是需要可逆,這個步驟就比較麻煩了。牛頓教育我們,一定要站在巨人的肩膀上,既然這樣一個需求被轉化爲了一個數學問題,而數學研究一向是超前的,那麼這個問題肯定已經有前輩先賢們研究過了。決定去搜算法去了。

結果沒用多久就通過pair function找到了想要的 Pairing Function 裏面有列舉 N x N => N的映射,並給出了一個實現 Cantor pairing function。這個映射最開始用來證明二維空間和一維具有相同的基數(cardinal number), 面對康託這類神人唯有長跪不起。

當然除了Cantor Pairing Function,Pairing Function 這裏還有其他的pairing function。

Cantor Pairing Function相比之前的方案具備簡明易懂,密佈不易超界,計算簡單等諸多優點,就選它了。這樣最終實現是:

public int getItemViewType(int position) {
  Adapter subAdapter = findSubAdapter(position);
  int itemType = subAdapter.getItemViewType(position - subAdapter.getStartPosition());
  
  ...
  
  int index = p.first.mIndex;
  return (int) getCantor(subItemType, index);
}

private static long getCantor(long k1, long k2) {
	return (k1 + k2) * (k1 + k2 + 1) / 2 + k2;
}

...
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
	  ...
	  //reverse Cantor Function
	  int w = (int) (Math.floor(Math.sqrt(8 * viewType + 1) - 1) / 2);
	  int t = (w * w + w) / 2;
	  int index = viewType - t;
	  int subItemType = w - index;
	  int idx = findAdapterPositionByIndex(index);
	  if (idx < 0) {
	  		return null;
	  }
	  Pair<AdapterDataObserver, Adapter> p = mAdapters.get(idx);
	  return p.second.onCreateViewHolder(parent, subItemType);
}

最終沒有額外的儲存空間和冗餘信息,也不用擔itemType/uid中某個單一極值出現就導致最終結果越界,效果好極了。

有時候數學上的幫助能讓代碼優雅並健壯很多,但是一定要先分析問題建立簡單的模型。選擇哪種算法有時候不是那麼容易找到的,而真正實現起來往往也沒有最初想象的難。
這樣對非負整數的多維向量(對的 不止二維)到一維實際上在不少地方都可能會用到,之前沒有特別注意,而之後瞭解了就可以採用這樣的辦法來解決。

花上些許時間,能夠把可能的隱患消除掉,而不用寫上註釋文檔提醒使用者諸如請不要設置超過2^4個subAdapters之類,導致隱性依賴;解決掉未來可能埋的坑同時學上點數學知識,何樂而不爲呢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章