SparseArray 那些事兒(帶給你更細緻的分析)

前言

說到Android 經常使用的數據結構,那不得不提一下SparseArray(稀疏數組),咱們在不少業務以及Android源碼中能見到java

基本介紹 (Whate)

簡單來說就是一個使用int做爲Key的 Map ,官網的介紹就是: SparseArrays map integers to Objects數組

繼承關係:

它繼承自Object,實現了Cloneable:性能優化

image

public class SparseArray<E> implements Cloneable {}
複製代碼

其中E就是咱們的泛型參數,即咱們要存入數據的類型markdown

構造方法:

public SparseArray() {
        this(10);
    }
 
    public SparseArray(int initialCapacity) {
    }
複製代碼

咱們能夠看到,它有兩個構造方法,一個參數爲容量大小,另外一個無參構造方法,最終調用的是容量爲10的的構造方法。數據結構

增刪改查:

既然是一個數據結構,固然要從增刪改查來介紹它的基本用法:app

使用方法(How)

增:

提供了put和append方法讓使用能夠放以int 做爲key,任何類型做爲值的數據。oop

public void put(int key, E value) public void append(int key, E value) 複製代碼
刪:

固然也就是指定某個key去刪除源碼分析

public void delete(int key) public void remove(int key) 複製代碼

也能夠刪除某個key以後返回刪除那個key的值:性能

public E removeReturnOld(int key) 複製代碼

由於SparseArray 內部存儲是用數組實現的,因此提供了按照數組下標來移除元素的功能(使用的時候要注意數組越界的問題):優化

public void removeAt(int index) 複製代碼

還提供了基於數組下標的範圍移除的功能(好比從數組的第1個開始日後移除大小3個的):

public void removeAtRange(int index, int size) 複製代碼
改:

還提供了能夠修改某個下標對應值的方法

public void setValueAt(int index, E value) 複製代碼
查:

根據咱們存入的key找到咱們的值

public E get(int key) public E get(int key, E valueIfKeyNotFound) 複製代碼

還能夠根據數組下標獲取值

public E valueAt(int index) 複製代碼

一樣能夠根據下標獲取key:

public int keyAt(int index) 複製代碼

也能夠根據咱們的key或者value反查出下標:

public int indexOfKey(int key) public int indexOfValue(E value) public int indexOfValueByValue(E value) 複製代碼
  • 特別說明:

indexOfValue方法經過value值查下標的話,若是多個key都使用了相同的value,只會返回升序查找的第一個符合要求的下標,

其餘功能
//大小
public int size() //清空 public void clear() 複製代碼

看完這裏基本用法已是都介紹完了,固然瞭解事物三部曲: 是什麼、怎麼用都講了,最後一步爲何固然也不能少,下面就來細講講它都實現原理:

主要源碼分析 (Why)

爲了方便理解,咱們先從get方法開始分析:

public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }
複製代碼

get方法最終調用的是這個方法,方法第一行首先調用了ContainerHelpers.binarySearch(mKeys, mSize, key)方法,這個方法接受三個參數,第三個是咱們即將存數據所指定的key,第一個和第二個是咱們的全局變量,分別是存咱們全部key的數組和 當前存入數據的大小:

//存放咱們的key
private int[] mKeys;
//存放咱們的值
private Object[] mValues;
//表示存入數據量的大小
private int mSize;
複製代碼

從這裏咱們也能看到,咱們的存入的鍵值對分別被存到量兩個數組中,而後用一個全局變量表示當前存入數據量的大小,那爲什麼要單獨用一個變量來表示它的大小而不是這兩個數組的長度呢?這個咱們後面講。

如今參數以及含義都知道了,咱們來看看這個方法,這個方法其實就是SparseArray核心的二分查找法,後面存取等操做都會有它到身影,咱們來分析一下:

核心的二分查找法:

/** * 找到目標key的 位置 * * @param keysArray 存key的數組 * @param size 數組的大小 * @param targetKey 要找的key * @return 若是找到了返回相應的key , * 若未找到,則返回這個key應該被存放的位置的取反 ~location */
    static int binarySearch(int[] keysArray, int size, int targetKey) {
        //位置(初始值爲0)
        int location = 0;
        //查找上限
        int ceiling = size - 1;
        //二分法查找
        while (location <= ceiling) {
            //除以二取先中間的key
            final int mid = (location + ceiling) >>> 1;
            final int midKey = keysArray[mid];
            if (midKey < targetKey) {
                location = mid + 1;
            } else if (midKey > targetKey) {
                ceiling = mid - 1;
            } else {
                return mid;  // key found
            }
        }
        //此時location的值就是它應該被存儲的到數組的位置(0或者length+1)
        return ~location;  // key not present
    }
複製代碼

我這邊把源碼中的命名稍微改了一下,讓理解起來更容易一點,那麼總體看下來,就是從咱們存key的數組去找咱們的targetKey,若是找到了,則直接返回,沒找到返回一個賦值

分析:

一開始定義初始位置爲0,上限即整個大小,而後當咱們當位置小於或者等於上限時候開始循環查找,第9行,對初始位置和上限的和作一個無符號右移,也就是除以2,而後取到位於中間的key,經過比較目標key和中間key的大小去肯定肯定下次查找的範圍,若是中間的值小,說明在中間範圍以上,因此下次開始查找的範圍的起始位置就是中間位置+1,再次執行循環體的內容,若是找到了則直接返回,若是始終沒找到,返回location的取反,其中取反和無符號涉及到位運算,若是還不是特別瞭解能夠參考這裏,不管最終location = 0或者length+1 ,它的取反都是一個負數。

細節:

  • 若是沒找到key 此時到location其實就是該值應該被放置到數組中到位置,此時返回到location到取反~ ,是一個負值,再次取反即可還原該值,在put方法中就有還原這個值對操做。
  • 根據二分查找方法可知,咱們key的存儲順序是按key大小升序排列的。

再次回到咱們的get方法,如今看下來就簡單了,i就是咱們要找key的位置下標,若是小於0,就表示未找到該key,直接返回未找到,不然返回Value中的第i個元素。

if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
複製代碼

再看另外一個條件:mValues[i] == DELETED他其實表示的是存值數組中的第i個元素被刪除了,進一步探究,咱們再來看看它的刪除方法 (delete方法和Remove其實都是一個方法):

刪除
public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage= true;
            }
        }
    }
複製代碼

仍是咱們熟悉的二分查找法,首先找到這個key對應的數組下標,若是大於或者等於0,表示存在,此時若是尚未被刪除,將該位置負值爲DELETE,而後將mGarbage標記爲置爲ture,表示要進行垃圾回收,而DELETED就是一個Object,用於表示該位置的元素被刪除了。

private static final Object DELETED = new Object();
複製代碼

那麼再回到剛剛的get方法的第二個條件,當存值的數組中被標記爲刪除以後,即便數組給該下標分配了空間,也會認爲key對應的值不存在,而咱們的delete操做只是給值中的元素作了標記操做,並無對數組對象作一些操做,不會像ArrayList 會對數組作移位操做。

思考

到這裏再回想一下,一開始爲什麼要設置全局標記位mSize而不是數組的長度來表示size了吧?由於即便我一個原來有值的某一個元素被刪除了,而數組大小並無隨之變小,而實際上這個size確定要減小一個,帶着思考,咱們來看看size()方法:

public int size() {
        if (mGarbage) {
            gc();
        }
        return mSize;
    }
複製代碼

首先第一個判斷條件就是咱們刪除操做中的標識位,當執行了刪除操做之後,執行gc方法,執行完成後返回全局的mSize,那麼咱們跟進這個gc方法,看看到底作了什麼:

private void gc() {
        //原始大小
        int originSize = mSize;
        //回收以後的大小
        int afterGcSize = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        for (int i = 0; i < originSize; i++) {
            Object val = values[i];
            //若是該位置的元素沒有被刪除
            if (val != DELETED) {
                //一旦這兩個值不相等(只要一個元素髮生了刪除,該元素之後這兩個值始終不相等,而且afterGcSize始終小於 i)
                if (i != afterGcSize) {
                    //元素移位操做
                    //將第i個的元素移到 上一個位置
                    keys[afterGcSize] = keys[i];
                    values[afterGcSize] = val;
                    values[i] = null;
                }
                //沒有刪除元素就自增
                afterGcSize++;
            }
        }
        mGarbage = false;
        mSize = afterGcSize;
    }
複製代碼

這邊把方法命名從新修改了一下,方便閱讀,那麼整個方法下來,其實就是數組元素移位,將標記爲刪除元素以後的元素往前移動到該位置,mSize被從新被賦值爲爲afterGcSize的大小即真正未刪除元素的大小,而後將mGarbage重置爲false。

例子:

若是爲有一個keys爲[-1,2,4]對應值爲[A,B,C]的SparseArray,爲如今將Key爲2的刪除,下面用動圖模擬一下當調用size時候執行gc的過程:

image

其中 i 表示循環執行的次數,注意看afterGcSize 變化的時機,而後最後gc後的狀態,請你們記住,後面還能用到。 在這裏插入圖片描述

gc以後 size = 2, 可是values數組長度仍是3

小結:

gc過程就是把元素前移去填補刪除到元素,而後返回真正存在元素到大小做爲size,這也再次解釋了爲何全局會有一個mSize而不是使用數組長度做爲size了。

咱們再來繼續看看改的方法,第一個方法仍是咱們剛剛看過的對檢查並gc的方法,gc事後而後和對相應下標作賦值操做:

public void setValueAt(int index, E value) {
        if (mGarbage) {
            gc();
        }
        mValues[index] = value;
    }
複製代碼

看完這些,咱們再回過頭來分析咱們「最複雜」的方法-增:

增:

先來看看put:

public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;

            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

            if (mGarbage && mSize >= mKeys.length) {
                gc();

                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }

            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
複製代碼
  1. 第一行先根據以前分析的方法得到下標,若是下標大於0表示該key已經存在,咱們直接將存值數組對應的下標進行賦值。
  2. 重點看else中的邏輯,以前咱們分析過了,若是 i小於0 則表示目前數組中不存在該key,而且對i作~(取反)操做即能獲得他在數組中應該被存放對位置,再看到第8行,其實這個邏輯是一個重用操做,若是i應該被存放的位置的元素被標記爲刪除了,很好,直接把對應下標的key和value替換。
  3. 此時再回想一下此時的狀態: 即便刪除標記的位置也沒匹配上個人下標,so,須要把垃圾清理一下(14行gc),再從新計算一下咱們的下標(由於gc以後,刪除標記位以後的元素會移動位置,再次計算可能位置就變了)。
  4. gc以後,咱們便看到了相似於insert的方法,分別對keys和values的數組作了處理,最好mSize增長一位,看上去像是數組擴容?咱們來一探究竟:
public static int[] insert(int[] array, int currentSize, int index, int element) {
        assert currentSize <= array.length;

        if (currentSize + 1 <= array.length) {
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            array[index] = element;
            return array;
        }

        int[] newArray = new int[growSize(currentSize)];
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }
複製代碼

5.首先咱們以Keys的調用來分析入參:

mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
複製代碼

分別傳入的是存Key的數組,當前大小,即將存放key的下標、即將被存放的key。

方法中第一個if體:咱們的mSize小於或者等於數組的長度執行一部分邏輯,注意看System.arraycopy方法,這個方法入參的src 和 des 其實就是他自己,那麼整段邏輯下來就是必要的時候對數組進行移位操做,移位操做完成後,對index進行賦值操做,因此雖然是insert方法,這裏其實沒有對數組進行擴容,而是重複利用了空間。那麼爲何走到這個條件體呢?回顧以前咱們對gc流程,一旦對元素進行刪除而且調用了gc以後,存key的數組長度確定是大於mSize的。 6. 再往下就是咱們真正的擴容操做了:

int[] newArray = new int[growSize(currentSize)];
    
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}

複製代碼

這裏的擴容和Arraylist的自增當前容量一半的擴容方式不一樣的是,當小於4直接是擴容到8,不然直接翻倍。

7.這裏咱們inser方法分析完成,最終經過這個方法完成了對存鍵值對數組的互用或者擴,至此put方法分析基本完成。

總結:

  1. SparseArray 因內部使用了int作爲key避免了自動裝箱操做,相比HashMap是更省內存的,可是另外一方面由於內部是二分查找法,在存儲大量數據的狀況下,性能是比HashMap差的,可是Android中通常沒有特別大量數據的場景,因此Android中儘量更推薦使用SparseArray。
  2. SpareArray雖然內部是數組實現的,可是它是按照Key的大小升序的,因此存數據前後並不能決定它在數組下標中的順序。
  3. 它除了增長元素可能會有數組擴容操做,其餘都是經過標記位,數組元素移位來完成,性能優化更好。
  4. 適用場景:數據量不大,空間比時間重要,key爲int的狀況,對於咱們客戶端來講通常頁面數據不會過千,那麼SparseArray相對於HashMap在查詢上不會有太大的區別,可是在內存上有很大的優點。
相關文章
相關標籤/搜索