SparseArray源碼分析

SparseArray源碼分析

  • SparseArray(稀疏數組)是什么?

    類似于map,可以儲存key-value鍵值對,與HashMap不同因為其key只能是整型int而且內部存儲結構是數組(最新HashMap存儲結構為紅黑樹+鏈表+數組);為android的工具類,用于優化HashMap<Integer, V>這種情況。由于其內部使用數組來存儲key和value,且數組內容可能大部分都并未使用,因此叫稀疏數組。

  • SparseArray的優勢劣勢?

    與HashMap相對比,SparseArray的內存效率更高,因為它避免了自動裝箱鍵,并且沒有HashMap中額外的節點(鏈表節點,紅黑樹節點)內存開銷。但是由于SparseArray將key放在一個數組結構中,且使用二分查找,因此其不適用于大數據量的儲存。例如儲存數百個數據,速度和HashMap相當,性能差異不明顯,并且兩者之間性能差異小于50%。相比于HashMap,SparseArray更節省內存,但是在大數據量的時候,插入和查找的效率都會比HashMap低。

  • SparseArray的應用場景?

    適用于儲存小容量數據,key必須為int型

下面分析源碼:

類成員變量:

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

private int[] mKeys;
private Object[] mValues;
private int mSize;
  • DELETED:用于標記已刪除的key-value
  • mGarbage:用于標記是否需要回收被刪除的數據
  • mKeys:用于儲存key
  • mValues:用于儲存values
  • mSize:當前包含非空元素(value)的條目(key-value),未調用gc前,包括value為DELETED的情況,這里的gc并非為java中的System.gc(),而是SparseArray中的gc方法。

構造函數

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;
}
  • 默認構造函數:調用帶參構造函數,指定容量為10
  • 帶參(指定數組容量)構造函數:容量為空則賦值mKeys和mValues為長度為0的數組,否則創建最小長度為initialCapacity的Unpadded Object數組,再將key數組的大小指定為value數組的大小。

關于EmptyArray:

點我查看源碼

//版本名稱:  Oreo API Level:  26
public final class EmptyArray {
    private EmptyArray() {}

    public static final boolean[] BOOLEAN = new boolean[0];
    public static final byte[] BYTE = new byte[0];
    public static final char[] CHAR = new char[0];
    public static final double[] DOUBLE = new double[0];
    public static final float[] FLOAT = new float[0];
    public static final int[] INT = new int[0];
    public static final long[] LONG = new long[0];

    public static final Class<?>[] CLASS = new Class[0];
    public static final Object[] OBJECT = new Object[0];
    public static final String[] STRING = new String[0];
    public static final Throwable[] THROWABLE = new Throwable[0];
    public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
    public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0];
    public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE =
        new java.lang.reflect.TypeVariable[0];
}

關于ArrayUtils.newUnpaddedObjectArray:

點我查看源碼

// 版本名稱:  Oreo API Level:  26
public static Object[] newUnpaddedObjectArray(int minLen) {
    return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
}

其中調用了VMRuntime.getRuntime().newUnpaddedArray來分配object數組內存,該方法字面意思為new一個un padded的數組,即new一個不填充的數組。返回一個至少為minLen長度的數組,但有可能會更大。增加的大小用于避免數組排列時填充的大小,填充量取決于組件類型和內存分配器實現。因為編譯器會根據數組對齊,在申請obj內存時,往往會分配大于該obj的實際內存而保證自然對齊使CPU讀寫更加高效,但是這往往需要填充一些沒有用到的空間,因此,為了不浪費這個空間,array變得更大了。

關于數據結構對齊可以看wiki描述:https://en.wikipedia.org/wiki/Data_structure_alignment

put

public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key); //二叉查找,可以看后面的方法解釋

    if (i >= 0) {
        mValues[i] = value; //數組中存在該key,直接更新值
    } else {
        i = ~i;  //取到要插入的數組下標
    
         //如果要插入的下標小于mSize且被標志為已刪除的話
         //重新將key賦值,value賦值
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
         
         //如果標記為mBarbage且mSize大于或者等于key數組大小
        if (mGarbage && mSize >= mKeys.length) {
             //gc方法回收已刪除的數據
            gc();

            //重新進行二叉搜索,因為索引可能已經發生變化了
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        
         //插入key
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
         //插入value
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
         //mSize加1
        mSize++;
    }
}
  • 首先通過二分查找是否在數組中存在該key,若找到,則返回對應的數組下標,否則,返回要插入的數組下標的負數
  • 如果存在的話,直接將對應的value數組下標賦值
  • 如果不存在的話,返回要插入的數組下標的負數,因此這里重新取反,拿到要插入數組下標的整數。再根據條件判斷key是否為已刪除數據,是否要進行數組壓縮,最后插入key和value到各自數組中。

關于ContainerHelpers.binarySearch:

//該方法要求array必須為有序數組
//array:要查找的數組
//size:數組中有效大小,從下表0開始,到size-1為數組的有效范圍
//value:要查找的值
static int binarySearch(int[] array, int size, int value) {
    //定義low低數組下標,用于數組中往上查找,最小為0
    int lo = 0; 
    
    //定義high高數組下標,用于數組中往下查找,最大為size-1
    int hi = size - 1; 

    //如果lo <= hi,則循環
    while (lo <= hi) {
         //取數組中間下標
         //右移1位即等同于除以2的1次方即除2,無符號右移,空位都以0補齊,與運算比除預算效率高
        final int mid = (lo + hi) >>> 1;  
        
         //取數組中間下標的值
        final int midVal = array[mid];

         //若中間值小于value,則將low賦值為mid+1,繼續往上查找
        if (midVal < value) {
            lo = mid + 1;
            
         //若中間值大于value,則將high賦值為mid-1,繼續往下查找
        } else if (midVal > value) {
            hi = mid - 1;
            
         //已找到,返回數組下標
        } else {
            return mid;  // value found
        }
    }
    //否則返回一個小于0的值,且該值還有另外一個含義:
    //要插入的位置下標,SparseArray拿到這個返回值后,便可知道要插入的下標
    //該值還有另外一個作用就是可以保證數組的有序性
    return ~lo;  // value not present
}

關于gc

該方法為SparseArray的回收方法,用于將刪除的數據從mKeys數組和mValues數組中刪除,并將剩下的數據往前移動

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

    int n = mSize; //當前數組有效大小,因為mkey大小和mValues大小相同,因此這里mSize是對兩者而言的
    int o = 0; //數組中非null的數量,即有效數據數量,起始值為0
    
    int[] keys = mKeys;
    Object[] values = mValues;

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

         //不是被刪除的元素
        if (val != DELETED) { 
             //若i 不等于 o的話
             //證明o為被刪除的下標,因此這里需要將下標為o的key數組的值
             //賦值為下標為i的值,且將下標為o的value數組的值賦值為當前val
             //最后將已經移走的當前下標i對應的value數組的值置為null
             //但是這里可以看到,key[i]并沒有置為0,因為不設置為0的話
             //不會影響二叉查找,因為后面mSize為非null數據數量
            if (i != o) {
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null;
            }
            
             //有效數據+1
            o++;
        }
    }

    mGarbage = false; //gc后將回收標志置為false
    mSize = o; //將mSize賦值為數組中非null的數量

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

關于GrowingArrayUtils.insert

點我查看源碼

//array: 要插入的數組
//currentSize: 數組中的元素數量,小于或等于array.length
//index: 要插入的下標
//element: 要插入的元素
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    //如果數組中的元素數量大于數組長度的話,拋出異常
    assert currentSize <= array.length;
    
    //如果在當前數組再插入一個元素,并且不超過數組的大小的話
    if (currentSize + 1 <= array.length) {
         //將原本數據從index開始,往后移動currentSize - index個數據
        System.arraycopy(array, index, array, index + 1, currentSize - index);
         //將index對應的值賦值為要插入的元素
        array[index] = element;
        return array;
    }

    @SuppressWarnings("unchecked")
    //否則需要重新創建數組,且將原本數組的內容賦值到新數組中
    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;
}

append

直接put一個key-value到數組中,針對key大于mKeys數組所有現有鍵的情況進行優化。

public void append(int key, E value) {
     //若當前mSize不為0且key小于mKeys數組中的最大值
    if (mSize != 0 && key <= mKeys[mSize - 1]) {
         //直接put
        put(key, value);
        return;
    }
    
     //mGarbage回收標志為true且mSize大于或等于mKeys數組長度
    if (mGarbage && mSize >= mKeys.length) {
         //調用gc方法
        gc();
    }
    
    
    //插入key
    mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
    //插入value
    mValues = GrowingArrayUtils.append(mValues, mSize, value);
    
    mSize++;
}

setValueAt

按照給定的index替換value


public void setValueAt(int index, E value) {
    //mGarbage回收標志為true的話調用gc方法
    if (mGarbage) {
        gc();
    }
    
    //將對應index下表的值賦值為value
    mValues[index] = value;
}

get

//根據key獲取value
public E get(int key) {
    return get(key, null);
}

//根據key獲取value,若該key不存在或者已刪除,返回valueIfKeyNotFound
public E get(int key, E valueIfKeyNotFound) {
     //首先進行二分查找 找到key的數組下表
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
     //i小于0代表mKeys數組中不包含該Key,或者該key-value已被刪除
    if (i < 0 || mValues[i] == DELETED) {
        return valueIfKeyNotFound;
    } else {
        return (E) mValues[i];
    }
}

remove

//刪除key對應的數據
public void remove(int key) {
    delete(key);
}

//刪除key對應的數據
public void delete(int key) {
    //先通過二叉查找是否存在該key
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
    //i >= 0代表存在該key且i為其在mKeys數組中的下標
    if (i >= 0) {
         //且該key對應的value還沒被刪除
        if (mValues[i] != DELETED) {
             //將對應mValues數組下表的值置位DELETED已刪除
            mValues[i] = DELETED;
             //將回收標志置為true,在調用size(), put(), append()等方法時需要
             //調用gc方法將被標記為已刪除的數據,即對應的value值為DELETED
             //從數組移除,且將后面的沒被刪除的數據往前移動
            mGarbage = true;
        }
    }
}

//刪除對應下表的數據
public void removeAt(int index) {
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
         //將回收標志置為true
        mGarbage = true;
    }
}

//刪除從對應下表開始,大小為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);
    }
}

查詢

indexOfKey

根據key獲取其在數組中的下表

public int indexOfKey(int key) {
     //mGarbage回收標志為true的話調用gc方法
    if (mGarbage) {
        gc();
    }
    
    //若數組中存在該key,則返回對應下表
    //否則返回一個負數
    return ContainerHelpers.binarySearch(mKeys, mSize, key);
}

indexOfValue

根據value獲取其在數組中的下表

public int indexOfValue(E value) {
    //mGarbage回收標志為true的話調用gc方法
    if (mGarbage) {
        gc();
    }
    
    //遍歷mValues數組,判斷是否相等
    for (int i = 0; i < mSize; i++) {
        if (mValues[i] == value) { 
             //若相等,則返回i
            return i;
        }
    }
    
    //否則返回-1
    return -1;
}

keyAt

獲取下表為index的key

public int keyAt(int index) {
     //mGarbage回收標志為true的話調用gc方法
    if (mGarbage) {
        gc();
    }

     //返回mKeys數組數據
    return mKeys[index];
}

valueAt

獲取下表為index的value

public E valueAt(int index) {
     //mGarbage回收標志為true的話調用gc方法
    if (mGarbage) {
        gc();
    }

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

推薦閱讀更多精彩內容