容器類原始碼解析系列(四)---SparseArray分析(最新版)

MRYangY發表於2019-04-21

容器類原始碼解析系列(四)---SparseArray分析(最新版)

引言

容器類原始碼解析系列已經更新到了第四篇,前三篇已經分別對ArrayList、LinkedList、HashMap進行原始碼分析。

  1. 容器類原始碼解析系列(一)—— ArrayList 原始碼分析(最新版)

  2. 容器類原始碼解析系列(二)—— LinkedList 集合原始碼分析(最新版)

  3. 容器類原始碼解析系列(三)—— HashMap 原始碼分析(最新版)

SparseArray 是用來儲存Key-Value這種對映關係的容器,它要求Key的型別必須是int型

要點

  • Key的型別必須是int型;

  • SparseArray底層通過雙陣列的結構實現資料儲存,一個陣列用來儲存key值,一個陣列用來儲存value;

    SparseArray

  • 相較於HashMap,在儲存key(int型)-value資料時,SparseArray會更省記憶體,但是在資料量大的情況下,查詢效率沒有HashMap好。

  • Sparse可以儲存NULL值,沒有fail-fast機制;

關於fail-fast機制,容器類原始碼解析系列(一)—— ArrayList 原始碼分析(最新版) 這篇文章有詳細介紹。

構造方法

在看構造方法之前先看一下幾個重要成員變數。

  private static final Object DELETED = new Object();
  private boolean mGarbage = false;
  
  private int[] mKeys;
  private Object[] mValues;
  private int mSize;

複製程式碼

mKeys和mValues是我上面提到的那兩個陣列,分別用來儲存key和value的。mSize表示容器中key-value鍵值對的數量。 DELETED是什麼呢?還有mGarbage? 上面的要點中,我們提到,SparseArray在儲存資料時比HashMap更省記憶體,但是效率沒有HashMap高,SparseArray使用了二分查詢,這個我們在後面的原始碼分析中能夠看到。 所以SparseArray想了一個方法來提高效率,就用到了DELETED和mGarbage這兩個變數。這個方法是,在刪除資料時,沒有立馬把資料置空回收,重組陣列結構,而是先把要刪除的value先置為DELETED狀態,在後面合適的時機,mGarbage會被置為true,然後呼叫gc方法,統一清除DELETED狀態的資料,重新調整容器結構。而在這個過程中,如果有新新增的資料,是可以複用DELETED狀態對應的index的,這樣DELETED資料又會變成正常資料,不會被回收了。 這樣就避免了頻繁的回收調整次數。

	/**
     * Creates a new SparseArray containing no mappings.
     */
    public SparseArray() {
        this(10);
    }

    /**
     * Creates a new SparseArray containing no mappings that will not
     * require any additional memory allocation to store the specified
     * number of mappings.  If you supply an initial capacity of 0, the
     * sparse array will be initialized with a light-weight representation
     * not requiring any additional array allocations.
     */
    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;
    }
	
複製程式碼

構造方法很簡單,就兩個構造方法,預設的不傳capacity引數的情況下建立的陣列長度是10。

常規操作

新增資料

/**
     * Adds a mapping from the specified key to the specified value,
     * replacing the previous mapping from the specified key if there
     * was one.
     */
    public void put(int key, E value) {
      	//通過二分查詢來找到mKeys陣列中對應key的index索引。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) { //如果找到了,表示之前存過這個key,則覆蓋舊的value。
            mValues[i] = value;
        } else {
            i = ~i;//取反,把負數變成正數。(註釋一)

            if (i < mSize && mValues[i] == DELETED) {//如果這個key對應的value之前被刪除了,但是還沒有被執行gc操作,目前還是DELETED狀態,那麼就複用此index。(註釋二)
                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);
            }

            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);//插入新key,如果需要擴容,就像ArrayList那樣,通過copy操作來完成。
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);//插入新value
            mSize++;//表示新新增了一對key-value鍵值對。
        }
    }
複製程式碼

上面在查詢key對應的索引時,使用了二分查詢二分搜尋演算法 。我們看下程式碼:

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值為3;int是四個位元組,其二進位制表示為:00000000 00000000 00000000 00000011,那麼~3 就等於:

11111111 11111111 11111111 11111100 等於-4。

所以註釋一處的操作就好理解了。註釋二 的行為表現就是我們上面說到的DELETED狀態的妙用,用來提高效率的。

set 操作

public void setValueAt(int index, E value) {
        if (mGarbage) {
            gc();
        }

        mValues[index] = value;
    }
複製程式碼

這裡要注意index的範圍要在0~size()-1之間。

刪除操作

public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
複製程式碼

**可以看到,它在執行刪除操作時並沒有立馬把對應的value置為null。而是先設定為DELETED狀態,然後後面找到合適時機一致回收,這個期間該key是可以被複用的,如果被複用那麼DELETED狀態可以重新變成NORMAL狀態。**我們同時也注意到mGarbage這個標誌位在此刻被置為了true。

/**
     * Removes the mapping at the specified index.
     *
     * <p>For indices outside of the range <code>0...size()-1</code>,
     * the behavior is undefined.</p>
     * 主要index的範圍問題
     */
    public void removeAt(int index) {
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }
複製程式碼

get操作

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

    /**
     * Gets the Object mapped from the specified key, or the specified Object
     * if no such mapping has been made.
     */
    @SuppressWarnings("unchecked")
    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];
        }
    }
複製程式碼

gc

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

        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;

        // Log.e("SparseArray", "gc end with " + mSize);
    }
複製程式碼

在呼叫gc操作後,會對那些個DELETED狀態的value統一置為null ,方便回收。同時會對index進行一次重新排序。

我們看看有哪些操作可能會觸發SparseArray的gc方法 注意哦,我這篇文章裡說的gc操作,指的都是SparseArray內部的gc方法。

put(int key, E value) size() keyAt(int index)
valueAt(int index) setValueAt(int index, E value) indexOfKey(int key)
indexOfValue(E value) indexOfValueByValue(E value) append(int key, E value)

總結

android開發用如果儲存key-value下的key是int型的話,建議使用SparseArray容器來操作,可以減少記憶體消耗。

個人站

rainyang.me


掃碼加入我的個人微信公眾號:Android開發圈 ,一起學習Android知識!!

在這裡插入圖片描述

相關文章