這一次,完全搞懂SparseArray實現原理

最近在整理SparseArray這一知識點的時候,發現網上大多數SparseArray原理分析的文章都存在不少問題(能夠說不少做者並無讀懂SparseArray的源碼),也正所以,纔有了這篇文章。咱們知道,SparseArray與ArrayMap是Android中高效存儲K-V的數據結構,也是是Android面試中的常客,弄懂它們的實現原理是頗有必要的,本篇文章就以SparseArray的源碼爲例進行深刻分析。java

開始以前先給你們推薦一下AndroidNote這個GitHub倉庫,這裏是個人學習筆記,同時也是我文章初稿的出處。這個倉庫中彙總了大量的java進階和Android進階知識。是一個比較系統且全面的Android知識庫。對於準備面試的同窗也是一份不可多得的面試寶典,歡迎你們到GitHub的倉庫主頁關注。git

1、SparseArray的類結構

SparseArray能夠翻譯爲稀疏數組,從字面上能夠理解爲鬆散不連續的數組。雖然叫作Array,但它倒是存儲K-V的一種數據結構。其中Key只能是int類型,而Value是Object類型。咱們來看下它的類結構:github

public class SparseArray<E> implements Cloneable {
    // 用來標記此處的值已被刪除
    private static final Object DELETED = new Object();
    // 用來標記是否有元素被移除
    private boolean mGarbage = false;
    // 用來存儲key的集合
    private int[] mKeys;
    // 用來存儲value的集合
    private Object[] mValues;
    // 存入的元素個數
    private int mSize;
    
    // 默認初始容量爲10
    public SparseArray() {
        this(10);
    }

    public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }
    
    // ...省略其餘代碼

}
複製代碼

能夠看到SparseArray僅僅實現了Cloneable接口並無實現Map接口,而且SparseArray內部維護了一個int數組和一個Object數組。在無參構造方法中調用了有參構造,並將其初始容量設置爲了10。面試

2、SparseArray的remove()方法

是否是以爲很奇怪?做爲一個容器類,不先講put方法怎麼先將remove呢?這是由於remove方法的一些操做會影響到put的操做。只有先了解了remove才能更容易理解put方法。咱們來看remove的代碼:算法

// SparseArray
public void remove(int key) {
    delete(key);
}

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

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

能夠看到remove方法直接調用了delete方法。而在delete方法中會先經過二分查找(二分查找代碼後邊分析)找到key所在的位置,而後將這一位置的value值置爲DELETE,注意,這裏還將mGarbage設置爲了true來標記集合中存在刪除元素的狀況。想象一下,在刪除多個元素後這個集合中是否是就可能會出現不連續的狀況?大概這也是SparseArray名字的由來吧。數組

3、SparseArray的put()方法

做爲一個存儲K-V類型的數據結構,put方法是key和value的入口。也是SparseArray中最重要的一個方法。先來看下put方法的代碼:markdown

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

    if (i >= 0) { // 意味着以前mKeys中已經有對應的key存在了,第i個位置對應的就是key。
        mValues[i] = value; // 直接更新value
    } else { // 返回負數說明未在mKeys中查找到key
      
        // 取反獲得待插入key的位置
        i = ~i;
      
	// 若是插入位置小於size,而且這個位置的value恰好是被刪除掉的,那麼直接將key和value分別插入mKeys和mValues的第i個位置
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
	// mGarbage爲true說明有元素被移除了,此時mKeys已經滿了,可是mKeys內部有被標記爲DELETE的元素
        if (mGarbage && mSize >= mKeys.length) {
            // 調用gc方法移動mKeys和mValues中的元素,這個方法能夠後邊分析
            gc();
					
            // 因爲gc方法移動了數組,所以插入位置可能有變化,因此須要從新計算插入位置
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        } 
	// GrowingArrayUtils的insert方法將會將插入位置以後的全部數據向後移動一位,而後將key和value分別插入到mKeys和mValue對應的第i個位置,若是數組空間不足還會開啓擴容,後邊分析這個insert方法
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}
複製代碼

雖然這個方法只有寥寥數行,可是想要徹底理解卻並不是易事,即便寫了很詳細的註釋也不容易讀懂。咱們不妨來詳細分析一下。第一行代碼經過二分查找獲得了一個index。看下二分查找的代碼:數據結構

// ContainerHelpers
static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
        final int mid = (lo + hi) >>> 1;
        final int midVal = array[mid];

        if (midVal < value) {
            lo = mid + 1;
        } else if (midVal > value) {
            hi = mid - 1;
        } else {
            return mid;  // value found
        }
    }
    return ~lo;  // value not present
}
複製代碼

關於二分查找相信你們都是比較熟悉的,這一算法用於在一組有序數組中查找某一元素所在位置的。若是數組中存在這一元素,則將這個元素對應的位置返回。若是不存在那麼此時的lo就是這個元素的最佳存儲位置。上述代碼中將lo取反做爲了返回值。由於lo必定是大於等於0的數,所以取反後的返回值一定小於等於0.明白了這一點,再來看put方法中的這個if...else是否是很容易理解了?app

// SparseArray
public void put(int key, E value) {
 
    if (i >= 0) { 
        mValues[i] = value; // 直接更新value
    } else { 
        i = ~i;
        // ... 省略其它代碼
    }
}
複製代碼

若是i>=0,意味着當前的這個key已經存在於mKeys中了,那麼此時put只須要將最新的value更新到mValues中便可。而若是i<=0就意味着mKeys中以前沒有對應的key。所以就須要將key和value分別插入到mKeys和mValues中。而插入的最佳位置就是對i取反。svn

獲得插入位置以後,若是這個位置是被標記爲刪除的元素,那麼久能夠直接將其覆蓋掉了,所以有如下代碼:

public void put(int key, E value) {
    // ...
    if (i >= 0) {
        // ...
    } else {    
        // 若是i對應的位置是被刪除掉的,能夠直接將其覆蓋
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        // ...
    }
    
}
複製代碼

若是上邊條件不知足,那麼繼續往下看:

public void put(int key, E value) {
    // ...
    if (i >= 0) {
        // ...
    } else {    
	// mGarbage爲true說明有元素被移除了,此時mKeys已經滿了,可是mKeys內部有被標記爲DELETE的元素
        if (mGarbage && mSize >= mKeys.length) {
            // 調用gc方法移動mKeys和mValues中的元素,這個方法能夠後邊分析
            gc();
					
            // 因爲gc方法移動了數組,所以插入位置可能有變化,因此須要從新計算插入位置
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        } 
        // ...
    }
    
}
複製代碼

上邊咱們已經知道,在remove元素的時候mGarbage會被置爲true,這段代碼意味着有被移除的元素,被移除的位置並非要插入的位置,而且若是mKeys已經滿了,那麼就調用gc方法來移動元素填充被移除的位置。因爲mKeys中元素位置發生了變化,所以key插入的位置也可能改變,所以須要再次調用二分法來查找key的插入位置。

以上代碼最終會肯定key被插入的位置,接下來調用GrowingArrayUtils的insert方法來進行key的插入操做:

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

    if (i >= 0) { 
        // ...
    } else { 
         // ...

	// GrowingArrayUtils的insert方法將會將插入位置以後的全部數據向後移動一位,而後將key和value分別插入到mKeys和mValue對應的第i個位置,若是數組空間不足還會開啓擴容,後邊分析這個insert方法
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}
複製代碼

GrowingArrayUtils的insert方法代碼以下:

// GrowingArrayUtils
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    assert currentSize <= array.length;
    // 若是插入後數組size小於數組長度,能進行插入操做
    if (currentSize + 1 <= array.length) {
        // 將index以後的全部元素向後移動一位
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        // 將key插入到index的位置
        array[index] = element;
        return array;
    }

    // 來到這裏說明數組已滿,需須要進行擴容操做。newArray即爲擴容後的數組
    T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
            growSize(currentSize));
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
}

// 返回擴容後的size
public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
}
複製代碼

insert方法的代碼比較容易理解,若是數組容量足夠,那麼就將index以後的元素向後移動一位,而後將key插入index的位置。若是數組容量不足,那麼則須要進行擴容,而後再進行插入操做。

4、SparseArray的gc()方法

這個方法其實很容易理解,咱們知道Java虛擬機在內存不足時會進行GC操做,標記清除法在回收垃圾對象後爲了不內存碎片化,會將存活的對象向內存的一端移動。而SparseArray中的這個gc方法其實就是借鑑了垃圾收集整理碎片空間的思想。

關於mGarbage這個參數上邊已經有提到過了,這個變量會在刪除元素的時候被置爲true。以下:

// SparseArray中全部移除元素的方法中都將mGarbage置爲true

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

    if (i >= 0) {
        if (mValues[i] != DELETED) {
            final E old = (E) mValues[i];
            mValues[i] = DELETED;
            mGarbage = true;
            return old;
        }
    }
    return null;
}

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

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


public void removeAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}


複製代碼

而SparseArray中全部插入和查找元素的方法中都會判斷若是mGarbage爲true,而且mSize >= mKeys.length時調用gc,以append方法爲例,代碼以下:

public void append(int key, E value) {

    if (mGarbage && mSize >= mKeys.length) {
        gc();
    }
  
   // ... 省略無關代碼
}
複製代碼

源碼中調用gc方法的地方多達8處,都是與添加和查找元素相關的方法。例如put()、keyAt()、setValueAt()等方法中。gc的實現其實比較簡單,就是將刪除位置後的全部數據向前移動一下,代碼以下:

private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
        Object val = values[i];

        if (val != DELETED) {
            if (i != o) {
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null;
            }

            o++;
        }
    }

    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
}
複製代碼

5、SparseArray的get()方法

這個方法就比較簡單了,由於put的時候是維持了一個有序數組,所以經過二分查找能夠直接肯定key在數組中的位置。

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];
    }
}
複製代碼

6、總結

可見SparseArray是一個使用起來很簡單的數據結構,可是它的原理理解起來彷佛卻沒那麼容易。這也是網上大部分文章對應SparseArray的解析都是含糊不清的緣由。相信經過本篇文章的學習必定對SparseArray的實現有了新的認識!

相關文章
相關標籤/搜索