SparseArray源碼分析

引子

SparseArray是google官方提供的一種int到Object的map,文檔見(jiàn):SparseArray。
大家都知道Java里已經(jīng)有了非常通用且功能強(qiáng)大的HashMap,那么Google為什么要再發(fā)明個(gè)輪子呢?這個(gè)新輪子相比有什么優(yōu)勢(shì)呢?下面我們先從整體上看下區(qū)別。

區(qū)別何在

從官方文檔描述我們知道此類的引入主要是基于節(jié)省內(nèi)存考慮,因?yàn)楫吘惯\(yùn)行時(shí)是在終端設(shè)備上。說(shuō)它更省內(nèi)存,主要是因?yàn)橄啾菻ashMap,它在實(shí)現(xiàn)上避免了int到Integer的autobox操作,還有每次put操作需要的額外Entry對(duì)象;它的key和value分別存儲(chǔ)在2個(gè)數(shù)組中,get時(shí)使用二分查找來(lái)定位某個(gè)key,不適合大數(shù)據(jù)量的場(chǎng)景比如上千,幾百的量比較合適,刪除操作也比較特殊,僅僅是個(gè)標(biāo)記操作,內(nèi)部也有自己的gc()方法用來(lái)清理壓縮內(nèi)存。接下來(lái)讓我們進(jìn)入正題。

源碼分析

和以往一樣,首先我們來(lái)看看其關(guān)鍵字段和ctor,如下:

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

    private int[] mKeys; // 存放int key的數(shù)組,元素升序排列
    private Object[] mValues;
    private int mSize;

    /**
     * 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 {
            // 分配至少initialCapacity這么大的數(shù)組
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }

常量DELETED對(duì)象用來(lái)標(biāo)記刪除,如果某個(gè)位置的值是DELETED則表示這個(gè)位置沒(méi)對(duì)應(yīng)的元素(被刪除了);

mGarbage在刪除操作中會(huì)被設(shè)置(置為true),表示接下來(lái)需要清理壓縮了(compacting);

mkeys,mValues分別表示SparseArray的內(nèi)部存儲(chǔ),即分別是key、value的存儲(chǔ)數(shù)組,mkeys在put操作中會(huì)被保證以升序的方式排列(以便二分查找能夠work);

mSize表示有效的key-value對(duì)的數(shù)目;

兩個(gè)構(gòu)造函數(shù)比較簡(jiǎn)單明了,無(wú)參版本會(huì)調(diào)用帶參數(shù)的版本并且將初始容量設(shè)為10,我們可以看到如果initialCapacity=0的話,mkeys、mValues會(huì)分別被初始化為EmptyArray里的常量數(shù)組,實(shí)際是長(zhǎng)度為0的數(shù)組,不會(huì)產(chǎn)生內(nèi)存分配;在Android 9.0的設(shè)備上跑無(wú)參的版本,實(shí)際mValues為有11個(gè)元素的數(shù)組(這點(diǎn)細(xì)節(jié)可能跟大多數(shù)人想的不一樣,因?yàn)槠鋵?shí)newUnpaddedObjectArray實(shí)際上會(huì)分配至少initialCapacity個(gè)元素,并不一定剛好等于它)。

最后都設(shè)置了mSize為0,表明當(dāng)前還只是個(gè)剛被創(chuàng)建的空的SparseArray。

接下來(lái)看2個(gè)依據(jù)key來(lái)得到value的方法,即get方法:

  /**
     * Gets the Object mapped from the specified key, or <code>null</code>
     * if no such mapping has been made.
     */
    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];
        }
    }

2個(gè)版本的實(shí)現(xiàn)都很簡(jiǎn)單明了,唯一有趣的點(diǎn)是內(nèi)部的ContainerHelpers.binarySearch(mKeys, mSize, key);這行代碼調(diào)用;
SparseArray內(nèi)部就是通過(guò)此二分查找算法來(lái)search key的,一并看下其實(shí)現(xiàn):

// This is Arrays.binarySearch(), but doesn't do any argument validation.
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
}

如源碼所說(shuō),這個(gè)版本和java.util.Arrays.java里的實(shí)現(xiàn)一樣,只是省略了參數(shù)檢查。二分查找大家都接觸過(guò),看下實(shí)現(xiàn)是不是很眼熟,沒(méi)什么特別的,這里著重說(shuō)下最后沒(méi)找到時(shí)的返回值~lo。如方法的doc所說(shuō),沒(méi)找到的情況下會(huì)返回一個(gè)負(fù)值,那到底返回哪個(gè)負(fù)值呢,-1行不行?其實(shí)這里的~lo就相當(dāng)于-(lo+1)(參看Arrays.binarySearch的實(shí)現(xiàn))。為什么要這樣做,因?yàn)槲覀儾粌H想表示沒(méi)找到,還想返回更多信息,即這個(gè)key如果要插進(jìn)來(lái)應(yīng)該在的位置(外面的代碼只需要再次~即取反就可以得到這個(gè)信息)。

接下來(lái)回到剛才的get方法,明白了這里使用的二分查找這個(gè)方法就非常簡(jiǎn)單明了了。get內(nèi)部通過(guò)binarySearch的返回值來(lái)做判斷,如果是負(fù)的或i>=0但是位置i已經(jīng)被標(biāo)記為刪除了則返回valueIfKeyNotFound,否則直接返回(E) mValues[i]。

緊接著來(lái)看一組delete/remove方法:

  /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) { // 元素存在的前提下
            // 標(biāo)記刪除法!??!
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED; 
                mGarbage = true;
            }
        }
    }

    /**
     * Alias for {@link #delete(int)}.
     */
    public void remove(int key) {
        delete(key);
    }

    /**
     * Removes the mapping at the specified index.
     */
    public void removeAt(int index) {
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }

    /**
     * Remove a range of mappings as a batch.
     *
     * @param index Index to begin at
     * @param size Number of mappings to remove
     */
    public void removeAtRange(int index, int size) {
        final int end = Math.min(mSize, index + size);
        for (int i = index; i < end; i++) {
            removeAt(i);
        }
    }

delete(key)和remove(key)一樣,都是先通過(guò)二分查找來(lái)找這個(gè)key,如果不存在則do nothing,否則如果這個(gè)位置還沒(méi)被標(biāo)記為刪除則標(biāo)記之,順便設(shè)置mGarbage(之前提到過(guò)刪除之類的操作都會(huì)設(shè)置這個(gè)值,表示接下來(lái)需要執(zhí)行清理壓縮操作了)。注意這里數(shù)組的不同處理,沒(méi)有直接移動(dòng)數(shù)組元素(壓縮處理)來(lái)刪除這個(gè)key,而僅僅是標(biāo)記操作(另外留意下這些操作在執(zhí)行的時(shí)候,mKeys數(shù)組并沒(méi)有經(jīng)過(guò)任何修改)。這2個(gè)方法都是通過(guò)key來(lái)進(jìn)行操作,有時(shí)你可能想基于index來(lái)執(zhí)行某些操作,上面的removeAt(index)和removeAtRange(index, size)就是這樣的方法,處理也都是如果位置i沒(méi)標(biāo)記刪除則標(biāo)記之,并設(shè)置mGarbage的值。只是在removeAtRange中對(duì)上限做了保險(xiǎn)處理即end = Math.min(mSize, index+size),以防數(shù)組越界。

接下來(lái)該打起來(lái)精神睜大眼睛了,讓我們一起來(lái)看看SparseArray的關(guān)鍵,姑且稱之為清理壓縮,上代碼:

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

        int n = mSize;
        // 數(shù)組的2個(gè)下標(biāo),一前一后,接下來(lái)移動(dòng)數(shù)組元素需要用到 
        int reusedIndex = 0;
        int[] keys = mKeys;
        Object[] values = mValues;

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

            if (val != DELETED) {
                if (i != reusedIndex) {
                    keys[reusedIndex] = keys[i];
                    values[reusedIndex] = val;
                    values[i] = null; // 往前提后,釋放此位置上的元素
                }

               // 如果遇到了DELETED值,那么此下標(biāo)不動(dòng),i還是正常增加
               // 所以下次循環(huán)執(zhí)行的時(shí)候,就會(huì)發(fā)生后面的有效值覆蓋掉前面DELETED的位置
               // 如果遇到的是有效值,那么這2個(gè)下標(biāo)是同步移動(dòng)的
                reusedIndex++;
            }
        }

        mGarbage = false;
        mSize = reusedIndex;

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

gc方法只有在mGarbage設(shè)置的時(shí)候才有可能調(diào)用,其總體思想是把靠后的有效元素(即沒(méi)被刪除的)往前(reusedIndex的位置)提,從而達(dá)到清理壓縮的目的。我們仔細(xì)分析下,首先初始化一些接下來(lái)要用到的值,這里特別留意下reusedIndex(源碼中叫o,實(shí)在不好理解,我按自己的理解重新命名了),初始化為0,指向第一個(gè)元素。接下來(lái)是遍歷mValues數(shù)組的for循環(huán),其內(nèi)部是如果當(dāng)前元素val是DELETED則啥也不做,接著看下一個(gè)元素(注意此時(shí)reusedIndex還是保持不動(dòng));否則當(dāng)前是個(gè)有效的元素,執(zhí)行步驟1、2:

  1. reusedIndex不是當(dāng)前的index則應(yīng)該把當(dāng)前元素拷貝到reusedIndex的位置(key和value都復(fù)制,即往前提),當(dāng)前位置的value清空(置null);

  2. reusedIndex往前+1(每遇到一個(gè)有效元素),準(zhǔn)備下次循環(huán)。

結(jié)束遍歷之后reset mGarbage(因?yàn)閯倓傋鲞^(guò)了,暫時(shí)不需要了),更新mSize為reusedIndex(因?yàn)槊坑龅揭粋€(gè)有效元素,reusedIndex就+1,所以它的值就是新的mSize)。
整個(gè)gc結(jié)束后,mValues數(shù)組里的所有DELETED元素就不存在了,故mSize也會(huì)相應(yīng)減小。

看完了上面的方法,我們來(lái)看下put相關(guān)的方法,代碼如下:

  /**
     * 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) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            // 已存在,則直接用新值替換
            mValues[i] = value;
        } else {
            i = ~i; // 再次取反,得到要插入的位置

            if (i < mSize && mValues[i] == DELETED) {
                // 標(biāo)記為刪除的位置的再次復(fù)用
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

            // 到了臨界點(diǎn)且需要觸發(fā)gc,則執(zhí)行一次gc方法
            if (mGarbage && mSize >= mKeys.length) {
                gc();

                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }

            // 將key、value在i的位置插入,并返回新的數(shù)組
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++; // key-value對(duì)+1
        }
    }

    // 上面用到的GrowingArrayUtils.insert實(shí)現(xiàn),代碼如下:
    public static int[] insert(int[] array, int currentSize, int index, int element) {
        assert currentSize <= array.length;

        if (currentSize + 1 <= array.length) {
            // 大小夠用,index處的元素往后順延1個(gè)位置,給新的index騰出位置
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            array[index] = element;
            return array;
        }
       // 大小不夠用,申請(qǐng)新的數(shù)組,拷貝,index處賦值
        int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }

put實(shí)現(xiàn)關(guān)鍵的幾個(gè)點(diǎn),也已經(jīng)寫(xiě)在上面的代碼注釋里了,可以重點(diǎn)關(guān)注下。

看完了上面put的實(shí)現(xiàn)后,最后來(lái)看看幾個(gè)非常簡(jiǎn)單的方法,

  /**
     * Returns the number of key-value mappings that this SparseArray
     * currently stores.
     */
    public int size() {
        if (mGarbage) {
            gc();
        }

        return mSize;
    }

    /**
     * Given an index in the range <code>0...size()-1</code>, returns
     * the key from the <code>index</code>th key-value mapping that this
     * SparseArray stores.
     *
     * <p>The keys corresponding to indices in ascending order are guaranteed to
     * be in ascending order, e.g., <code>keyAt(0)</code> will return the
     * smallest key and <code>keyAt(size()-1)</code> will return the largest
     * key.</p>
     */
    public int keyAt(int index) {
        if (mGarbage) {
            gc();
        }

        return mKeys[index];
    }

    /**
     * Given an index in the range <code>0...size()-1</code>, returns
     * the value from the <code>index</code>th key-value mapping that this
     * SparseArray stores.
     *
     * <p>The values corresponding to indices in ascending order are guaranteed
     * to be associated with keys in ascending order, e.g.,
     * <code>valueAt(0)</code> will return the value associated with the
     * smallest key and <code>valueAt(size()-1)</code> will return the value
     * associated with the largest key.</p>
     */
    @SuppressWarnings("unchecked")
    public E valueAt(int index) {
        if (mGarbage) {
            gc();
        }

        return (E) mValues[index];
    }

    /**
     * Given an index in the range <code>0...size()-1</code>, sets a new
     * value for the <code>index</code>th key-value mapping that this
     * SparseArray stores.
     */
    public void setValueAt(int index, E value) {
        if (mGarbage) {
            gc();
        }

        mValues[index] = value;
    }

    /**
     * Returns the index for which {@link #keyAt} would return the
     * specified key, or a negative number if the specified
     * key is not mapped.
     */
    public int indexOfKey(int key) {
        if (mGarbage) {
            gc();
        }

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

    /**
     * Returns an index for which {@link #valueAt} would return the
     * specified key, or a negative number if no keys map to the
     * specified value.
     * <p>Beware that this is a linear search, unlike lookups by key,
     * and that multiple keys can map to the same value and this will
     * find only one of them.
     * <p>Note also that unlike most collections' {@code indexOf} methods,
     * this method compares values using {@code ==} rather than {@code equals}.
     */
    public int indexOfValue(E value) {
        if (mGarbage) {
            gc();
        }

        for (int i = 0; i < mSize; i++)
            if (mValues[i] == value) // 唯一需要留意的是這里比較用的==,而非equals
                return i;

        return -1;
    }

    /**
     * Removes all key-value mappings from this SparseArray.
     */
    public void clear() {
        int n = mSize;
        Object[] values = mValues;

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

        mSize = 0;
        mGarbage = false;
    }

size()方法返回當(dāng)前有效的key-value對(duì)的數(shù)目,值得注意的是在其內(nèi)部都會(huì)有條件的執(zhí)行g(shù)c方法,因?yàn)橹暗牟僮骺赡軐?dǎo)致mGarbage被設(shè)置了,所以必須執(zhí)行下清理壓縮才能返回最新的值;
keyAt(index)和valueAt(index)提供了遍歷SparseArray的方式,如下:

  for (int i = 0; i < sparseArray.size(); i++) {
        System.out.println("key = " + sparseArray.keyAt(i) + ", value = " + sparseArray.valueAt(i));
    }

同樣和size()方法一樣其內(nèi)部也都會(huì)有條件的執(zhí)行g(shù)c方法,注意你傳遞的index必須在[0, size()-1]之間,否則會(huì)數(shù)組越界。

setValueAt讓你像使用數(shù)組一樣設(shè)置某個(gè)位置處的值,同樣會(huì)有條件的執(zhí)行g(shù)c方法。

indexOfKey通過(guò)二分查找來(lái)search key的位置;indexOfValue在整個(gè)mValues數(shù)組中遍歷查找value,有則返回對(duì)應(yīng)的index否則返回-1。

clear()方法清空了mValues數(shù)組,重置了mSize,mGarbage,但請(qǐng)注意并沒(méi)有動(dòng)mKeys數(shù)組;

目前為止,SparseArray類的所有關(guān)鍵代碼都已經(jīng)分析完畢,希望對(duì)各位平時(shí)的開(kāi)發(fā)有所幫助,祝好。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容