SparseArray 源碼解析

使用 Android Studio 做爲 IDE 的開發者可能會遇到一個現象,就是在代碼中若是聲明瞭 Map<Integer, Object> 類型的變量的話,Android Studio 會提示:Use new SparseArray<Object>(...) instead for better performance ...,意思就是用 SparseArray< Object > 性能更優,能夠用來替代 HashMapjava

這裏就來介紹下 SparseArray 的內部原理,看看它與 HashMap 有什麼差異,關於 HashMap 的源碼解析能夠看這裏:Java集合框架源碼解析之HashMapandroid

1、基本概念

先看下 SparseArray 的使用方式git

SparseArray<String> sparseArray = new SparseArray<>();
        sparseArray.put(100, "leavesC");
        sparseArray.remove(100);
        sparseArray.get(100);
        sparseArray.removeAt(29);
複製代碼

SparseArray< E > 至關於 Map< Integer,E > ,key 值固定爲 int 類型,在初始化時只須要聲明 Value 的數據類型便可,其內部用兩個數組分別來存儲 Key 列表和 Value 列表:int[] mKeys ; Object[] mValuesgithub

mKeysmValues 經過以下方式對應起來:數組

  • 假設要向 SparseArray 存入 key10value200 的鍵值對,則先將 10 存到 mKeys 中,假設 10mKeys 中對應的索引值是 index ,則將 value 存入 mValues[index]
  • mKeys 中的元素值按照遞增的形式存放,每次存放新的鍵值對時都經過二分查找方法來對 mKeys 進行排序

最重要的一點就是 SparseArray 避免了 Map 每次存取值時的裝箱拆箱操做,Key 值都是基本數據類型 int,這有利於提高性能框架

2、全局變量

布爾變量 mGarbage 也是 SparseArray 的一個優化點之一,用於標記當前是否有待垃圾回收(GC)的元素,當該值被置爲 true 時,即意味着當前狀態須要進行垃圾回收,但回收操做並不立刻進行,而是在後續操做中再統一進行ide

//數組元素在沒有外部指定值時的默認元素值
    private static final Object DELETED = new Object();
    //用於標記當前是否有待垃圾回收(GC)的元素
    private boolean mGarbage = false;
    private int[] mKeys;
    private Object[] mValues;
    //當前集合元素大小
    //該值並不必定是時時處於正確狀態,由於有可能出現只刪除 key 和 value 二者之一的狀況
    //因此在調用 size() 方法前都須要進行 GC
    private int mSize;
複製代碼

3、構造函數

key 數組和 value 數組的默認大小都是 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;
    }
複製代碼

4、添加元素

添加元素的方法有幾個,主要看 put(int key, E value) 方法,當中用到了 ContainerHelpers 類提供的二分查找方法:binarySearch,用於查找目標 key 在 mKeys 中的當前索引(已有改 key)或者是目標索引(沒有該 key)性能

binarySearch 方法的返回值分爲兩種狀況:學習

  1. 若是 mKeys 中存在對應的 key,則直接返回對應的索引值
  2. 若是 mKeys 中不存在對應的 key
    • 2.1 假設 mKeys 中存在值比 key 大且大小與 key 最接近的值的索引爲 presentIndex,則此方法的返回值爲 ~presentIndex
    • 2.2 若是 mKeys 中不存在比 key 還要大的值的話,則返回值爲 ~mKeys.length
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
    }
複製代碼

能夠看到,即便在 mKeys 中不存在目標 key,但其返回值也指向了應該讓 key 存入的位置。經過將計算出的索引值進行 ~ 運算,則返回值必定是 0 或者負數,從而與「找獲得目標key的狀況(返回值大於0)」的狀況區分開

從這個能夠看出該方法的巧妙之處,單純的一個返回值就能夠區分出多種狀況,且經過這種方式來存放數據可使得 mKeys 的內部值一直是按照值遞增的方式來排序的

public void put(int key, E value) {
        //用二分查找法查找指定 key 在 mKeys 中的索引值
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //找獲得則直接賦值
        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;
            //若是目標位置還未賦值,則直接存入數據便可,對應的狀況是 2.1
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            //如下操做對應 2.一、2.2 兩種狀況:
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                //GC 後再次進行查找,由於值可能已經發生變化了
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            //經過複製或者擴容數組,將數據存放到數組中
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
複製代碼

5、移除元素

上文說了,布爾變量 mGarbage 用於標記當前是否有待垃圾回收(GC)的元素,當該值被置爲 true 時,即意味着當前狀態須要進行垃圾回收,但回收操做並不立刻進行,而是在後續操做中再完成

如下幾個方法在移除元素時,都是隻切斷了 mValues 中的引用,而 mKeys 並無進行回收,這個操做會留到 gc() 進行處理

//若是存在 key 對應的元素值,則將其移除
    public void delete(int key) {
        //用二分查找法查找指定 key 在 mKeys 中的索引值
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                //標記當前須要進行垃圾回收
                mGarbage = true;
            }
        }
    }

    //和 delete 方法基本相同,差異在於會返回 key 對應的元素值
    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;
    }

    //省略其它幾個比較簡單的移除元素的方法
    
複製代碼

6、查找元素

查找元素的方法較多,但邏輯都是挺簡單的

//根據 key 查找相應的元素值,查找不到則返回默認值
    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
        //用二分查找法查找指定 key 在 mKeys 中的索引值
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //若是找不到該 key 或者該 key 還沒有賦值,則返回默認值
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

    //根據 value 查找對應的索引值
    public int indexOfValue(E value) {
        if (mGarbage) {
            gc();
        }
        for (int i = 0; i < mSize; i++) {
            if (mValues[i] == value) {
                return i;
            }
        }
        return -1;
    }

    //與 indexOfValue 方法相似,但 indexOfValue 方法是經過比較 == 來判斷是否同個對象
    //而此方法是經過 equals 方法來判斷是否同個對象
    public int indexOfValueByValue(E value) {
        if (mGarbage) {
            gc();
        }
        for (int i = 0; i < mSize; i++) {
            if (value == null) {
                if (mValues[i] == null) {
                    return i;
                }
            } else {
                if (value.equals(mValues[i])) {
                    return i;
                }
            }
        }
        return -1;
    }
    
    //省略其它幾個方法
複製代碼

7、垃圾回收

由於 SparseArray 中可能會出現只移除 value 和 value 二者之一的狀況,致使數組中存在無效引用,所以 gc() 方法就用於移除無效引用,並將有效的元素值位置合併在一塊兒

private void gc() {
        int n = mSize;
        //o 值用於表示 GC 後的元素個數
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            //元素值非默認值 DELETED ,說明該位置可能須要移動數據
            if (val != DELETED) {
                //如下代碼片斷用於將索引 i 處的值賦值到索引 o 處
                //因此若是 i == o ,則不須要執行代碼了
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
    }
複製代碼

8、優劣勢

從上文的解讀來看,SparseArray 的主要優點有如下幾點:

  • 避免了基本數據類型的裝箱拆箱操做
  • 和 Map 每一個存儲結點都是一個類對象不一樣,SparseArray 不須要用於包裝的的結構體,單個元素的存儲成本更加低廉
  • 在數據量不大的狀況下,查找效率較高(二分查找法)
  • 延遲了垃圾回收的時機,只在須要的時候才一次進進行

劣勢有如下幾點:

  • 插入新元素可能會致使移動大量的數組元素
  • 數據量較大時,查找效率(二分查找法)會明顯下降

篇幅所限,這裏就不粘貼處 SparseArray.java 的完整詳細源碼註解了,能夠點擊這裏查看:SparseArray.java

更多的學習筆記能夠看這裏:JavaKotlinAndroidGuide

相關文章
相關標籤/搜索