intellij老是警告的SparseArray是什麼 - HashMap的替代者

來源:互聯網
上載者:User

標籤:android   hashmap   記憶體最佳化   稀疏數組   

序言

身為一個有代碼潔癖的程式員,在寫Android應用的時候,我總是會去注意

  • 代碼規範(Google Android Guideline)
  • 能一行搞定的代碼,絕不寫兩行
  • 決不讓編譯器(intellij, as)右邊捲軸有黃色
  • 不重複自己

當然了,實際開發中,編譯器報的warning有些不太好避免,比如有些null 指標,編譯器從android源碼來看,覺得不會出現null 指標,但是實際情況下….你懂得,部分rom手賤改壞了源碼,結果就crash了,所以我們能做的,就是盡量減少warning。

扯了這麼多,說回主題,有時候在HashMap申明那行,intellij會報warning,說用SparseArray更好,那麼SparseArray究竟是什麼東西,為什麼更好,為什麼提示說更省記憶體呢?

本文以api 21的源碼為準

SparseArray源碼分析
// E對應HashMap的Valuepublic class SparseArray<E> implements Cloneable {    // 用來最佳化刪除效能,標記已經刪除的對象    private static final Object DELETED = new Object();    // 用來最佳化刪除效能,標記是否需要記憶體回收    private boolean mGarbage = false;    // 儲存索引,整數索引從小到大被映射在該數組    private int[] mKeys;    // 儲存物件    private Object[] mValues;    // 實際大小    private int mSize;

建構函式:

    public SparseArray(int initialCapacity) {        if (initialCapacity == 0) {            // EmptyArray是一個不需要數組分配的輕量級表示。            mKeys = EmptyArray.INT;            mValues = EmptyArray.OBJECT;        } else {            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);            mKeys = new int[mValues.length];        }        mSize = 0;    }

這裡也充分節約了記憶體。newUnpaddedObjectArray最後指向了VMRuntime的一個native方法

/** * 返回一個至少長minLength的數組,但可能更大。增長的大小來自於避免數組後的任何padding。padding的大小依賴於componentType和記憶體 Clerk的實現 */public native Object newUnpaddedArray(Class<?> componentType, int minLength);

Get方法使用了二分尋找

/** * 獲得指定key的映射對象,或者null如果沒有該映射。 */public E get(int key) {    return get(key, null);}@SuppressWarnings("unchecked")public E get(int key, E valueIfKeyNotFound) {    // 二分尋找    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);    // 如果沒找到或者該value已經被標記刪除    if (i < 0 || mValues[i] == DELETED) {        return valueIfKeyNotFound;    } else {        return (E) mValues[i];    }}

對應的二分尋找實現這裡就不贅述了,大致就是一個對有序數組一分為二比較中間值和目標值的迴圈尋找。

其中比較巧妙的一點是在沒有找到的時候會返回一個~low的index,其有兩個作用:

  • 告訴調用者沒有找到
  • 調用者可以直接用~result獲得該元素應該插入的位置(-(insertion point) - 1)。

對應的put方法

/** * 添加一個指定key到指定object的映射,如果之前有一個指定key的映射則直接替換掉原映射object。 */public void put(int key, E value) {    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);    if (i >= 0) {        // 該key原來有了,替換掉        mValues[i] = value;    } else {        // 做一個負運算,獲得應該插入的index        i = ~i;        // size足夠且原value已經被標記為刪除        if (i < mSize && mValues[i] == DELETED) {            mKeys[i] = key;            mValues[i] = value;            return;        }        // 走到這裡就說麼i超出了size,或者對應元素是有效        // 被標記為需要記憶體回收且SparseArray大小不小於keys數組長度        if (mGarbage && mSize >= mKeys.length) {            // 壓縮空間(這裡源碼有點逗,竟然還有Log.e的注釋留在那裡,看來Android源碼工程師也是要調試的),會壓縮數組,把無效的值都去掉,保證連續有效值            gc();            // 再次尋找因為索引可能改變            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);        }        // 插入,如果size不夠則會重新分配數組        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);        // 實際大小加1        mSize++;    }}

在看看remove(del)方法

/** * 如果有的話,刪除對應key的映射 */public void delete(int key) {    // 又是二分尋找    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);    // 存在則標記對應value為DELETED,且置位mGarbage    if (i >= 0) {        if (mValues[i] != DELETED) {            mValues[i] = DELETED;            mGarbage = true;        }    }}/** * {@link #delete(int)}的別名. */public void remove(int key) {    delete(key);}/** * 刪除指定索引的映射(這個有點暴力啊,用的應該比較少吧,直接指定位置了) */public void removeAt(int index) {    // 就是delete裡面那段方法,如此說來為什麼delete不調用removeAt而要重複這段代碼呢    if (mValues[index] != DELETED) {        mValues[index] = DELETED;        mGarbage = true;    }}

大致看了crud的這幾個方法,應該有個一個初步瞭解了吧,SparseArray因為key是一個純粹的整數數組,避免了auto-boxing key和額外的資料結構去映射K/V關係,從而節省了記憶體。

當然了,這裡也有一個tradeoff,由於key數組需要有序,所以每次都會相對簡單的寫操作更花費時間,要去二分尋找,要在數組刪除/插入元素。所以對應地為了最佳化,因為了mGarbage和DELETED,將可能的多次gc合并為一次,延遲到必須的時候執行。

源碼注釋裡也提到了,該類不適合用於大資料量,上百個entry的時候差別在50%以內,尚可以接受,畢竟在移動端我們缺的,往往是記憶體,而不是CPU。

HashMap源碼分析

相較SparseArray,HashMap的實現就複雜一點了,因為其支援多種key(甚至null),還要實現Iterator。這裡我們主要看看基本操作實現。

/** * transient關鍵字表示不需要序列化。 * Hash表,null key的在下面。 * HashMapEntry定義了K/V映射,hash值,以及next元素 */transient HashMapEntry<K, V>[] table;/** * 該entry代表null key, 或者不存在該映射. */transient HashMapEntry<K, V> entryForNullKey;/** * hash map的映射數量. */transient int size;/** * 結構修改的時候自增,來做(最大努力的)並發修改檢測 */transient int modCount;/** * hash表會在大小超過該闕值的時候做rehash。通常該值為0.75 * capacity, 除非容量為0,即上面的EMPTY_TABLE聲明。 */private transient int threshold;public HashMap(int capacity) {    if (capacity < 0) {        throw new IllegalArgumentException("Capacity: " + capacity);    }    if (capacity == 0) {        // 和SparseArray類似,所有空的執行個體共用同一個EMPTY表示        HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;        table = tab;        // 強制先put()來替換EMPTY_TABLE        threshold = -1;        return;    }    if (capacity < MINIMUM_CAPACITY) {        capacity = MINIMUM_CAPACITY;    } else if (capacity > MAXIMUM_CAPACITY) {        capacity = MAXIMUM_CAPACITY;    } else {        // 難道是為了記憶體padding        capacity = Collections.roundUpToPowerOfTwo(capacity);    }    makeTable(capacity);}/** * 根據給定容量分配一個hash表,並設定對應闕值。 * @param newCapacity 必須是2的次數 */private HashMapEntry<K, V>[] makeTable(int newCapacity) {// 這麼做難道是為了同步性考慮HashMapEntry<K, V>[] newTable            = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];    table = newTable;    threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity    return newTable;}

然後是插入的代碼

/** * 映射指定key到指定value,如果有原對應mapping返回其value,否則返回null。 */@Override public V put(K key, V value) {    if (key == null) {        // key為空白直接去putValueForNullKey方法        return putValueForNullKey(value);    }    // 計算該key的hash值,根據key本身的hashCode做二次hash    int hash = Collections.secondaryHash(key);    HashMapEntry<K, V>[] tab = table;    int index = hash & (tab.length - 1);    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {        // 原來有對應entry了,直接修改value,然後返回oldValue        if (e.hash == hash && key.equals(e.key)) {            preModify(e);            V oldValue = e.value;            e.value = value;            return oldValue;        }    }    // 沒有現存的entry,建立一個    modCount++;    // size超出闕值,double容量,重新計算index    if (size++ > threshold) {        tab = doubleCapacity();        index = hash & (tab.length - 1);    }    // 在table的index位置插入一個新的HashMapEntry,其next是自己    addNewEntry(key, value, hash, index);    return null;}private V putValueForNullKey(V value) {    HashMapEntry<K, V> entry = entryForNullKey;    // 和上面類似的邏輯,如果存在則替換,否則建立該HashMapEntry    if (entry == null) {        addNewEntryForNullKey(value);        size++;        modCount++;        return null;    } else {        preModify(entry);        V oldValue = entry.value;        entry.value = value;        return oldValue;    }}

姑且說到這裡,大致可以清楚,HashMap是一個以hash為中心的實現,在size上,也只有double的邏輯,而沒有remove後是否縮小capacity的邏輯。時間複雜度O(1)的代價就是耗費大量記憶體來儲存資料。

比較

HashMap -> 快,浪費記憶體
SparseArray -> 會有效能損耗, 節約記憶體

我們做一個簡單的效能測試,不帶參數初始化HashMap和SparseArray,同樣儲存Integer->String的映射

用:符號來分割HashMap和SparseArray的耗時(毫秒)

實驗 第一次 第二次 第三次 第四次
依次put 10000個數 13:7 12:6 76:4 14:6
隨機put 10000個數 16:83 18:85 15:76 17:78

4次測試結果相近(有一個噪音資料76/4有待研究),HashMap在有序的key時候,更耗時,而在無需key的時候則是SparseArray更耗時,而HashMap則沒有太大的效能差別。

再對隨機put後的結果做了10000次get後,獲得了7:3,7:3,8:3的結果,可見get操作上,SparseArray效能更好,但即便在10000個entry的時候,差別其實也並不大。

在記憶體上,看到SparseArray在Allocation Tracker中為32,而HashMap則達到了69632這個可怕的數字。。。。。。

結論

在key為整數的情況下,考慮到移動端往往K/V對不會太大,所以用SparseArray能更節省記憶體,且效能損耗在可接受的範圍。

從實驗結果看,SparseArray相較於HashMap,大大節省了記憶體,對移動端實在是不二的選擇。

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

intellij老是警告的SparseArray是什麼 - HashMap的替代者

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.