Java&Android 基礎知識梳理(10) - SparseArray 源碼解析

1、基本概念

SparseArray的用法和keyint類型,valueObject類型的HashMap相同,和HashMap相比,先簡要介紹一下它的兩點優點。java

內存佔用

Java&Android 基礎知識梳理(8) - 容器類 咱們已經學習過HashMap的內部實現,它內部是採用數組的形式保存每一個Entry,並採用鏈地址法來解決Hash衝突的問題。可是採用數組會遇到擴容的問題,默認狀況下當數組內的元素達到loadFactor的時候,會將其擴大爲目前大小的兩倍,那麼就有可能形成空間的浪費。數組

SparseArray雖然也是採用數組的方式來保存Key/Value學習

private int[] mKeys;
private Object[] mValues;
複製代碼

可是與HashMap使用普通數組不一樣,它對存放ValuemValues數組進行了優化,其建立方式爲:優化

public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            //默認狀況下,建立的 initialCapacity 大小爲 10。
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }
複製代碼

其中ArrayUtils.newUnpaddedObjectArray(initialCapacity)用於建立優化後的數組,該方法其實是一個Native方法,它解決了當數組中的元素沒有填滿時形成的空間浪費。spa

SparseArray 淺析 一文中介紹了SparseArray對於數組的優化方式,假設有一個9 x 7的數組,在通常狀況下它的存儲模型能夠表示以下:.net

數組存儲的通常模型

能夠看到這種模型下的數組當中存在大量無用的0值,內存利用率很低。而優化後的方案用兩個部分來表示數組:指針

  • 第一部分:存放的是數組的行數、列數、當前數組中有效元素的個數
  • 第二部分:存放的是全部有效元素的行、列數、元素的值

數組存儲的優化模型
mKeys則是用普通數組實現的,經過查找 Key值所在的位置,再根據 mValues數組的屬性找到對應元素的行、列值,從而獲得對應的元素值。

避免自動裝箱

對於HashMap來講,當咱們採用put(1, Object)這樣的形式來放入一個元素時,會進行自動裝箱,即建立一個Integer對象放入到Entry當中。code

SparseArray則不會存在這一問題,由於咱們聲明的就是int[]類型的mKeys數組。cdn

2、源碼解析

2.1 存放過程

public void put(int key, E value) {
        //經過二分查找法進行查找插入元素所在位置。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //若是大於0,那麼直接插入。
        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);
            }
            //從新分配數組,並插入新的 Key,Value。
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
複製代碼

2.2 讀取過程

public E get(int key, E valueIfKeyNotFound) {
        //經過二分查找,在 Key 數組中獲得對應 Value 的下標。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //取出下標對應的元素。
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }
複製代碼

2.3 刪除過程

public void delete(int key) {
        //二分查找所在位置。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //將該位置的元素置爲 DELETED,它是內部預先定義好的一個對象。
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
複製代碼

能夠看到,在刪除元素的時候,它是用一個空的Object來標記該位置。在合適的時候(例如上面的put方法),才經過下面的gc()方法對mKeysmValues數組 從新排列對象

private void gc() {
        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;
    }
複製代碼

2.4 二分查找

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>=0,因此當沒法查找到對應元素的時候,返回值~lo必定<0。(~lo=-(lo+1)

這也是咱們在2.1中看到,爲何在i>=0時就能夠直接替換的緣由,由於只要i>=0,就說明以前已經存在一個Key相同的元素了。

而在返回值小於0時,對它再一次取~,就恰好能夠獲得 要插入的位置

3、SparseArray 的效率問題

瞭解了SparseArray的原理以後,咱們能夠分析出有如下幾方面有可能會影響SparseArray插入的效率:

  • 插入的效率。插入的效率其實主要跟Key值插入的前後順序有關,假如Key值是按 遞減順序 插入的,那麼每次咱們都是在mValues[0]位置插入元素,這就要求把原來ValuesmKeys數組中[0, xxx]位置元素複製到[1, xxx+1]的位置,而若是是 遞增插入 的則不會存在該問題,直接擴大數組數組的範圍以後再插入便可。
  • 查找的效率。這點很明顯,由於採用了二分查找,若是查找的Key值位於折半處,那麼將會更快地找到對應的元素。

也就是說SparseArray在插入和查找上,相對於HashMap並不存在明顯的優點,甚至在某些狀況下,效率還要更差一些。

Google之因此推薦咱們使用SparseArray來替換HashMap,是由於在移動端咱們的數據集每每都是比較小的,而在這種狀況下,這二者效率的差異幾乎能夠忽略。可是在內存利用率上,因爲採用了優化的數組結構,而且避免了自動裝箱,SparseArray明顯更高,所以更推薦咱們使用SparseArray

4、SparseArray 的衍生

SparseArray還有幾個衍生的類,它們的基本思想都是同樣的,即:

  • 用兩個數組分別存儲keyvalue,經過下標管理映射關係。
  • 採用二分查找法查找如今mKeys數組中對應找到所在元素的下標,再去mValues數組中取出元素。

咱們在平時使用的時候,能夠根據實際的應用場景選取相應的集合類型。

Key 類型不一樣

假如keylong型:

  • LongSparseArraykeylongvalueObject

Value 類型不一樣

假如keyint,而value爲下面三種基本數據類型之一,那麼能夠採用如下三種集合來避免value的自動裝箱來進一步優化。

  • SparseLongArraykeyintvaluelong
  • SparseBooleanArraykeyintvalueboolean
  • SparseIntArraykeyintvalueint

Key 和 Value 類型都不一樣

假如keyvalue都不爲基本數據類型,那麼能夠採用:

  • ArrayMapkeyObjectvalueObject
相關文章
相關標籤/搜索