SparseArray 學習

在安卓項目中, 新建一個 HashMap 對象, 會有提示: 建議使用 SparseArray 會有更好的表現(xiàn). 尤其是鍵為 Integer 類型的 maps, 更加高效. 尤其是 value 是原始數(shù)據(jù)類型時, 可以使用 SparseIntArray 等來避免裝箱的操作. (其實還有其它優(yōu)勢)

為什么叫 SparseArray?

SparseArray 稀疏數(shù)組. 為什么叫稀疏?

先來看下 Javascript 中關于稀疏數(shù)組的定義:

稀疏數(shù)組就是包含從 0 開始的不連續(xù)索引的數(shù)組. 通常, 數(shù)組的 length 屬性值代表數(shù)組中元素的個數(shù). 如果數(shù)組是稀疏的, length 屬性值大于元素的個數(shù). 可以用 Array() 構(gòu)造函數(shù)或簡單地指定數(shù)組的索引值大于當前數(shù)組長度來創(chuàng)建稀疏數(shù)組.

可能有點懵, 我先說下我寫完這篇筆記的理解. 在 SparseArray 數(shù)組中, 部分需要刪除的元素, 不會被立即從數(shù)組中刪除, 而是被標記為需要刪除. 只有在需要的時候, 才一次性去刪除和釋放空間. 這時, 數(shù)組的 length 長度不能反映有效元素的個數(shù), 因為被標記需要刪除的元素也被計算進去了. SparseArray 中, 有一個單獨的成員變量來計算實際有效元素的個數(shù). 所以 SparseArray 數(shù)組就是一大串數(shù)組中有很多標記為可刪除的元素, 所以是稀疏的, 零散的. 之后會詳細記錄對于刪除標志位的學習.

類型

Android Studio 給出的提示 - value 是原始數(shù)據(jù)類型時, 可以避免裝箱的操作. 那詳細看一下 SparseArray 給出的類型:

SparseArray          <int, Object>
SparseBooleanArray   <int, boolean>
SparseIntArray       <int, int>
SparseLongArray      <int, long>
LongSparseArray      <long, Object>

會發(fā)現(xiàn)它和 HashMap 唯一不同的就是鍵值對的類型, HashMap的鍵值都為泛型, 但是 SparseArray 的鍵值只能為 int/long. 因為 long 的范圍更大, 所以 LongSparseArray 儲存的數(shù)據(jù)比 SparseArray 多.

int 的范圍是 -2^31 到 2^31-1, 而 long 是 -2^63 到 2^63-1

使用方法

SparseArray<String> sparseArray= new SparseArray<>();
       
// 增加元素
sparseArray.append(0, "myValue"); // append 方式, 追加. 其實也包含了 put. 后面源碼分析
sparseArray.put(1, "myValue"); // put 方式, 指定 key 更新
        
//刪除元素, 二者等同
sparseArray.remove(1);
sparseArray.delete(1);

//修改元素, put/append 相同的 key 值即可
sparseArray.put(1,"newValue");
sparseArray.append(1,"newValue");

設計思想

成員變量

首先來 SparseArray 的源碼, 其中定義了一些成員變量:

private static final Object DELETED = new Object();
private boolean mGarbage = false;

// 需要說明一下
// 這里的 mKeys 數(shù)組是按照 key 值遞增存儲的
private int[] mKeys;
private Object[] mValues;
private int mSize;
  • DELETED - 標志字段, 用于判斷是否刪除.
  • mGarbage - 標志字段, 用于確定當前是否需要垃圾回收.
  • mKeys 數(shù)組 - 存儲 key. int 類型.
  • mValues 數(shù)組 - 存儲值
  • mSize 當前元素數(shù)量

append 方法

public void append(int key, E value) {
    
    // 當 mSize 不為 0 并且不大于 mKeys 數(shù)組中的最大值時
    // 因為 mKeys 是一個升序數(shù)組, 最大值即為 mKeys[mSize-1]
    if (mSize != 0 && key <= mKeys[mSize - 1]) {
        // 執(zhí)行 put 方法
        put(key, value);
        return;
    }
    
    // 不是 put 則證明需要新增
    
    // 當垃圾回收標志 mGarbage 為 true 并且當前元素已經(jīng)占滿整個數(shù)組
    if (mGarbage && mSize >= mKeys.length) {
        gc(); // 執(zhí)行垃圾回收進行空間壓縮
    }
        
    // 當 mSize 為 0, 或者 key 值大于當前 mKeys 數(shù)組最大值的時候
    // 在數(shù)組最后一個位置插入元素.
    mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
    mValues = GrowingArrayUtils.append(mValues, mSize, value);
    
    // 元素加一
    mSize++;
}

會發(fā)現(xiàn)處理以下兩種情況:

  1. 如果是更新對應 key 的值, append 方法調(diào)用 put 方法. 此時 key 值不等于 0 且小于等于最大值. 因為 key 值之前有提到是遞增存儲.
  2. 如果是新增. 即 key 等于 0, 數(shù)組為空, 或者 key 大于當前最大值, 在最后插入.

put 方法

public void put(int key, E value) {

    // 二分查找
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
    // 查找到
    if (i >= 0) {
        mValues[i] = value; // 更新值
    } else { // 沒有找到
        i = ~i; // 獲得二分查找結(jié)束后, lo 的值
        
        // 元素要添加的位置正好被標記為 DELETED, 直接覆蓋它的值即可.
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        
        // 垃圾回收, 但是空間壓縮后,mValues 數(shù)組和 mKeys 數(shù)組元素有變化, 需要重新計算插入的位置
        if (mGarbage && mSize >= mKeys.length) {
           gc();

            // 重新二分查找插入的位置
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        
        // 在指定位置 i 處, 插入元素
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        
        // 元素加一
        mSize++;
    }
}

方法里的二分查找取反操作很奇怪, 我們先看下二分查找的代碼:

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;  // 找到了
        }
    }
    return ~lo;  // 和普通二分查找不一樣的地方. 返回了負的 lo.
}

正常沒找到, 我們可能會返回 -1, 表示查找失敗就夠了. 那為什么返回一個 lo 的取反?

  1. 首先需要和 -1, 一樣的功能, 需要一個負數(shù)表示沒有找到.
  2. 這個 lo 值其實就是新元素的插入位置.

好了, 回到 put 方法, 在拿到待插入元素應該插入的位置之后, 它首先判斷需要插入的位置對應 mValues 數(shù)組的值是不是 DELETED, 如果是的話,直接覆蓋, 那為什么這樣做?

先看下 delete 方法源碼

public void delete(int key) {
    // 查找要刪除的元素下標
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

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

SparseArray 也有 remove 方法, 它其實是直接調(diào)用 delete, 所以二者是一樣的效果.

看到這個 delete 方法, 并沒有直接去刪除, 而是將 value 值設為 DELETED 標志位. 然后設置 mGarbage 為 true, 也就是垃圾回收標志位. 再來看下之前緊跟著的 gc() 方法:

private void gc() {
  
    int n = mSize; // 壓縮前數(shù)組大小
    int o = 0;     // 壓縮后數(shù)組大小, 初始為0
    int[] keys = mKeys; // 保存新的 key 值的數(shù)組
    Object[] values = mValues; // 保存新的 value 值的數(shù)組

    for (int i = 0; i < n; i++) { 
        Object val = values[i];
        
        // 如果沒有 DELETED 標志位
        if (val != DELETED) {
            
            // i != o, 說明前面已經(jīng)有元素被打上 DELETED 標簽
            if (i != o) {
                // 將位置 i 的元素移動到 o 處. 其實 o 處就是之前 DELETED 標簽的位置. 被覆蓋
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null; // 釋放空間
            }

            // 新數(shù)組元素加一
            o++;
        }
    }
    
    // 回收完畢, 還原標志位
    mGarbage = false;
    
    // 回收之后數(shù)組的大小
    mSize = o;
}

所以是通過 gc() 方法, 根據(jù)標志位用 DELETED 之后的元素覆蓋該標志位的元素, 達到壓縮和空間釋放的目的.

為什么不直接刪除

回頭來看 put 方法:

// 添加元素時直接覆蓋 DELETED 標志位的元素
if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
}

還記得這一段嗎? 在添加元素的時候, 如果對應的元素正好被標記了 DELETE, 那么直接覆蓋它即可! 這樣做, 和每次都刪除元素相比, 可以直接省去刪除元素的操作, 大大提高了效率. 只有空間不足的時候, 才調(diào)用 gc() 方法一次性壓縮空間, 效率進一步.

SparseArray 與 HashMap萄唇、ArrayMap 比較

SparseArray 的限制在于鍵必須是 int/long 類型, 值必須是Object類型. 這樣可以避免key自動裝箱產(chǎn)生過多的 Object. 但是這樣的話, 如果 key 值相同, 那么數(shù)據(jù)就會被直接覆蓋.

  • SparseArray 不能保證保留插入順序, 在迭代的時候應該注意. SparseArray 中沒有 Iterator, 只實現(xiàn)了 Cloneable 接口, 沒有繼承 Collection享郊、List 或者 Map 接口.

  • 查找數(shù)據(jù)使用的是二分法, 明顯比通過 hashcode 慢, 所以數(shù)據(jù)越大, 查找速度慢的劣勢越明顯.

優(yōu)點:

  • 避免了基本數(shù)據(jù)類型的裝箱操作
  • 不需要額外的結(jié)構(gòu)體, 單個元素的存儲成本更低
  • 數(shù)據(jù)量小的情況下, 隨機訪問的效率更高

缺點:

  • 插入操作需要復制數(shù)組, 增刪效率降低
  • 數(shù)據(jù)量巨大時, 復制數(shù)組成本巨大普筹,gc() 成本也大, 查詢效率也會明顯下降

總結(jié)

  1. 通過標志位, 延遲刪除機制的機制大大提高效率. 也是稀疏數(shù)組的意義所在.
  2. 鍵為 Integer 類型的 maps, 更加高效. 尤其是 value 是原始數(shù)據(jù)類型時, 可以使用 SparseIntArray 等來避免裝箱.
  3. 二分法相比于 hashmap, 在大體量數(shù)據(jù)時, 效率降低.
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痹届,一起剝皮案震驚了整個濱河市抡驼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌燃逻,老刑警劉巖序目,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異伯襟,居然都是意外死亡猿涨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門姆怪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叛赚,“玉大人,你說我怎么就攤上這事稽揭“掣剑” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵溪掀,是天一觀的道長昙读。 經(jīng)常有香客問我,道長膨桥,這世上最難降的妖魔是什么蛮浑? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮只嚣,結(jié)果婚禮上沮稚,老公的妹妹穿的比我還像新娘。我一直安慰自己册舞,他們只是感情好蕴掏,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著调鲸,像睡著了一般盛杰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上藐石,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天即供,我揣著相機與錄音,去河邊找鬼于微。 笑死逗嫡,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的株依。 我是一名探鬼主播驱证,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恋腕!你這毒婦竟也來了抹锄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伙单,沒想到半個月后呆万,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡车份,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年谋减,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扫沼。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡出爹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缎除,到底是詐尸還是另有隱情严就,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布器罐,位于F島的核電站梢为,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏轰坊。R本人自食惡果不足惜铸董,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肴沫。 院中可真熱鬧粟害,春花似錦、人聲如沸颤芬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽站蝠。三九已至汰具,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間菱魔,已是汗流浹背留荔。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留豌习,地道東北人存谎。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓拔疚,卻偏偏與公主長得像肥隆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子稚失,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容