SparseArray 稀疏數組源碼分析

SparseArray sparse 稀疏

介紹

SparseArray 用來實現 int 類型與 Object 類型的映射,跟普通的 Map 不同,普通 Map 中有更多的空索引,對比 HashMap 來說,稀疏數組實現了更高效的內存使用,因為稀疏數組避免了 int 類型 key 的自動裝箱,且稀疏數組每個 value 都不需要使用 Entry 對象來包裝。所以在 Android 開發中,我們可以使用 SparseArray 來實現更高效的實現 Map

SparseArray 實現了 Cloneable 接口,說明時支持克隆操作的,下面慢慢分析增刪改查以及克隆等操作

一、成員變量

/**
 * 刪除操作時替換對應位置 value 的默認值
 */
private static final Object DELETED = new Object();
/**
 * 是否需要回收
 */
private boolean mGarbage = false;
/**
 * 存儲 key 的數組
 */
private int[] mKeys;
/**
 * 存儲 value 的數組
 */
private Object[] mValues;
/**
 * 當前存儲的鍵值對數量
 */
private int mSize;

SparseArray 中聲明了一個 int 類型的數組和一個 Object 類型的數組

二、構造函數

/**
 * 創建一個空 map 初始容量為 10
 */
public SparseArray() { this(10); }

/**
 * 根據指定初始容量創建鍵值對為空的稀疏數組,并且不會申請額外內存;指定初始容量為 0 時會創建一個輕量級的不需要任何內存分配的稀疏數組
 * capacity 容量
 */
public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        mKeys = EmptyArray.INT; // 長度為 0 的 int 類型數組
        mValues = EmptyArray.OBJECT; // 長度為 0 的 Object 類型數組
    } else {
        mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
        mKeys = new int[mValues.length];
    }
    mSize = 0;
}

SparseArray 有兩個構造函數,默認時創建初始容量為 10 數組,另外一個時可以使用者指定出事容量的數量

三、添加/修改 操作

public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 使用二分法查找對應的 key 在數組中的下標

    if (i >= 0) { // 索引大于等于 0 說明原數組中有對應 key
        mValues[i] = value; // 則直接 Value 數組中的 value 值為最新的 value
    } else { // 索引小于 0 說明原數組中不存在對應的 key
        i = ~i; // 取反后得到當前 key 應該在的位置

        if (i < mSize && mValues[i] == DELETED) { // 如果數組長度夠,并且當前位置已被回收則直接對該位置賦值
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        if (mGarbage && mSize >= mKeys.length) { // 回收狀態為 true 并且內容長度大于等于 key 數組長度
            gc(); // 回收,整理數組

            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); // 再次使用二分法查找位置
        }

        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 執行 key 插入到 key 數組對應位置
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 執行 value 插入到 value 數組對應位置
        mSize++; // 鍵值對數量加 1
    }
}

上面的 put 方法中用到了一個 ContainerHelpers 的 binarySearch 函數,我們先來看一下這個函數的操作,主要是使用二分法查找對應的位置

// 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; // 帶符號右移,也就是做除以 2,這里是找到中間位置索引的操作
            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 的時候,這里是將如果數組中存在 value 時應該在的位置取反后返回
    }

接著我們看一下 gc() 方法的操作

// 
private void gc() {
    int n = mSize; // 鍵值對數量
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) { // 通過循環將 value 數組中的 DELETED 值移除,并且 DELETED 以后的鍵跟值都往前補
        Object val = values[i];

        if (val != DELETED) {
            if (i != o) { // 循環第一次執行時 i 和 o 都是 0 ,這種情況不需要處理
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null; // 原位置置空
            }
            o++;
        }
    }

    mGarbage = false; // 回收狀態置為 false
    mSize = o; // 將鍵值對的值更新為實際的鍵值對數量
}

/**
 * GrowingArrayUtils 中定義了 泛型/int/long/boolean 等類型數組在指定位置插入數據的方法
 */
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;
}

/**
 * GrowingArrayUtils 中定義了 泛型/int/long/boolean 等類型數組在指定位置插入數據的方法,這個方法的作用為,在位置超出數組大小時,計算擴容后數組的新長度
 * 舊數組長度小于 4 則設置為 8,否則都是在當前長度基礎上擴容一被
 */
public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
}

小結一下,插入操作的工作是,首先在原 key 數組中查找是否有對應的 key,如果找到則直接替換 value 數組中對應下標的值;如果 key 不存在之前的 key 數組,則需要根據是否回收狀態進行無用數據回收,然后執行插入,插入過程中如果數組需要擴容還需要執行擴容操作。

由插入操作可以看出,keys 數組中的值為從小到大排列,是一個有序數組

上面分析了插入方法的主要邏輯,接下來繼續看 查找/刪除 等操作,如果明白了插入操作,下面的就都簡單了

四、查找方法 get(int key)

public E get(int key) {
    return get(key, null);
}

/**
 * 根據 key 查找 value ,如果 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];
    }
}

可以看到 get() 方法比較簡單,首先通過 二分法 找到當前 key 在 key 數組中的位置,如果位置不小于 0 且 value 數組中對應位置的值部位 DELETED,說明找到對應值,直接返回,否則就返回 null。get() 操作是有一個重載方法的,調用者可以傳入一個默認值,在查不到對應 key 時則返回默認值。

五、刪除方法 delete(int 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;
        }
    }
}

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;
}

刪除操作就更簡單了,首先通過二分法查找 key 所在位置,找到就將 value 中對應位置的值設置為 DELETED,在其他操作時通過 gc() 操作執行該位置的回收。removeReturnOld 方法則是會返回刪除的 value 值。

同時,SparseArray 也提供了移除指定位置的鍵值對的方法

/**
 * 刪除指定位置的值
 */
public void removeAt(int index) {
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

/**
 * 以 index 開始,刪除之后 size 個值,包含 index 位置,不包含 index + size
 */
public void removeAtRange(int index, int size) {
    final int end = Math.min(mSize, index + size);
    for (int i = index; i < end; i++) {
        removeAt(i);
    }
}

六、其他操作

克隆

SparseArray 重寫了 clone 方法,科隆時其 keys,values 數組都會克隆成新的數組

@Override
@SuppressWarnings("unchecked")
public SparseArray<E> clone() {
    SparseArray<E> clone = null;
    try {
        clone = (SparseArray<E>) super.clone();
        clone.mKeys = mKeys.clone();
        clone.mValues = mValues.clone();
    } catch (CloneNotSupportedException cnse) {
        /* ignore */
    }
    return clone;
}

size() 返回鍵值對的數量

首先執行 gc() 操作,然后返回正確的數量

public int size() {
    if (mGarbage) {
        gc();
    }

    return mSize;
}

keyAt() valueAt() setValueAt() indexOfKey() indexOfValue()

/**
 * 返回指定位置的 key
 */
public int keyAt(int index) {
    if (mGarbage) {
        gc();
    }
    return mKeys[index];
}

/**
 * 返回指定位置的 value
 */
public E valueAt(int index) {
    if (mGarbage) {
        gc();
    }

    return (E) mValues[index];
}

/**
 * 將對應位置的值設置為指定 value
 */
public void setValueAt(int index, E value) {
    if (mGarbage) {
        gc();
    }

    mValues[index] = value;
}

/**
 * 返回指定位置的 key
 */
public int indexOfKey(int key) {
    if (mGarbage) {
        gc();
    }

    return ContainerHelpers.binarySearch(mKeys, mSize, key);
}

/**
 * 返回指定位置的 value
 */
public int indexOfValue(E value) {
    if (mGarbage) {
        gc();
    }

    for (int i = 0; i < mSize; i++) {
        if (mValues[i] == value) {
            return i;
        }
    }

    return -1;
}

/**
 * 返回指定 value 所在位置,只不過 value 相等的判斷使用 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;
}

/**
 * 移除所有鍵值對
 */
public void clear() {
    int n = mSize;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
        values[i] = null;
    }

    mSize = 0;
    mGarbage = false;
}

/**
 * 插入鍵值對,優化插入的 key 大于所有現在已有 key 的情況,由于 key 數組是從大到小的有序數組,所以這種情況下不需要二分法查找位置,優化了性能
 */
public void append(int key, E value) {
    if (mSize != 0 && key <= mKeys[mSize - 1]) { // 如果不是大于現在已有的 key ,則按照正常方式插入
        put(key, value);
        return;
    }

    if (mGarbage && mSize >= mKeys.length) { // 執行回收 DELETED 的 value
        gc();
    }

    mKeys = GrowingArrayUtils.append(mKeys, mSize, key); // 直接向后插入
    mValues = GrowingArrayUtils.append(mValues, mSize, value); // 直接向后插入
    mSize++;
}

/**
 * 打印所有的 key value
 */
public String toString() {
    if (size() <= 0) {
        return "{}";
    }

    StringBuilder buffer = new StringBuilder(mSize * 28);
    buffer.append('{');
    for (int i=0; i<mSize; i++) {
        if (i > 0) {
            buffer.append(", ");
        }
        int key = keyAt(i);
        buffer.append(key);
        buffer.append('=');
        Object value = valueAt(i);
        if (value != this) {
            buffer.append(value);
        } else {
            buffer.append("(this Map)");
        }
    }
    buffer.append('}');
    return buffer.toString();
}

七、總結

SparseArray 的代碼非常少,只有 450 行左右,并且特別易于理解。但 SparseArray 要比 HashMap 更加高效,在 Android 手機中,如果 key 為 int 類型的 Map 數據,最好使用 SparseArray 來實現。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 一、基本數據類型 注釋 單行注釋:// 區域注釋:/* */ 文檔注釋:/** */ 數值 對于byte類型而言...
    龍貓小爺閱讀 4,282評論 0 16
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 內存優化前我們先了解一些和內存相關的概念: 垃圾回收 內存抖動 四種引用 內存泄露 下面我們回到正題, 講一下如何...
    MZzF2HC閱讀 1,712評論 0 6
  • 也有日子沒動手撕烤人參了,前天聽一哥嘆道每一兩天就要抓一個節點向前推事兒,今天突然悟到:一哥別無選擇,惟有把個人乃...
    冰小寒閱讀 546評論 0 3
  • 001 增加自由支配的時間。 很多時候我們覺得時間不夠用,還有許多事情沒辦。如果每天早起哪怕一個小時,一年就多留出...
    我心已許閱讀 212評論 1 1