Android之SparseArray 源碼解析

前言

SparseArray是安卓特有的一種數(shù)據(jù)結(jié)構(gòu)哆姻,跟HashMap相似,都是存儲<Key,Value>的實體。但是SparseArray的Key只能是Int類型的待侵。在存儲的時候Key按照順序進(jìn)行了排序出刷,當(dāng)查詢的時候采用了二分查找法來定位位置璧疗。這種方式相對來說更加迅速

變量

private boolean mGarbage = false;//是否可以進(jìn)行回收,也就是進(jìn)行key馁龟,value的整理

private int[] mKeys;//存儲的key值

private Object[] mValues;//存儲的value值

private int mSize; //數(shù)組的大小

可以看到崩侠,對于key和value的存儲,是分別存儲在兩個不同的數(shù)組中的屁柏。而且key的類型是int啦膜,而不是封裝后的Integer。

這里面有個 mGarbage 變量淌喻,它標(biāo)志著我們當(dāng)前的數(shù)據(jù)是否可以進(jìn)行數(shù)據(jù)的整理工作僧家。比如說,當(dāng)我們移除某個key以后裸删,會將這個標(biāo)志位設(shè)置為true八拱,在需要的時候(比如說我們要進(jìn)行數(shù)據(jù)的存儲),會根據(jù)這個標(biāo)志位進(jìn)行一次數(shù)組的整理工作。

構(gòu)造函數(shù)

public SparseArray() {
    //默認(rèn)數(shù)組的大小是10
    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;
}

從構(gòu)造函數(shù)可以看出來肌稻,SparseArray的數(shù)組 默認(rèn)大小是10 清蚀,如果我們在實際的使用過程中能夠確定要保存的數(shù)據(jù)量的大小,最好直接初始化爹谭,這樣就不會出現(xiàn)擴容的問題枷邪。

源碼分析

添加元素

既然和HashMap相似,那么肯定是有數(shù)據(jù)的增刪改的

    public void put(int key, E value) {
        //通過ContainerHelpers進(jìn)行二分查找诺凡。如果存在則返回key的位置
        // 如果不存在則返回key在數(shù)組中可以存儲的位置i的負(fù)值东揣。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果key已經(jīng)存在,則直接賦值
        if (i >= 0) {
            mValues[i] = value;
        } else {
            //binarySearch 方法的返回值分為兩種情況:
            //1腹泌、如果存在對應(yīng)的 key嘶卧,則直接返回對應(yīng)的索引值
            //2、如果不存在對應(yīng)的 key
            //  2.1凉袱、假設(shè) mKeys 中存在"值比 key 大且大小與 key 最接近的值的索引"為 presentIndex芥吟,則此方法的返回值為 ~presentIndex
            //  2.2、如果 mKeys 中不存在比 key 還要大的值的話专甩,則返回值為 ~mKeys.length
            //可以看到钟鸵,即使在 mKeys 中不存在目標(biāo) key,但其返回值也指向了應(yīng)該讓 key 存入的位置
            //通過將計算出的索引值進(jìn)行 ~ 運算配深,則返回值一定是 0 或者負(fù)數(shù)携添,從而與“找得到目標(biāo)key的情況(返回值大于0)”的情況區(qū)分開
            //且通過這種方式來存放數(shù)據(jù),可以使得 mKeys 的內(nèi)部值一直是按照值遞增的方式來排序的
            i = ~i;
            //如果搜索到的i位置可以使用篓叶,并且沒有數(shù)據(jù)烈掠,則將對應(yīng)的key,value存入到i位置
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            //如果可以進(jìn)行數(shù)組的整理缸托,并且當(dāng)前的數(shù)組大小能夠進(jìn)行存儲左敌,則進(jìn)行數(shù)據(jù)的整理,然后再進(jìn)行位置的查找
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                // Search again because indices may have changed.
                //GC 后再次進(jìn)行查找俐镐,因為值可能已經(jīng)發(fā)生變化了
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            //通過復(fù)制或者擴容數(shù)組矫限,將數(shù)據(jù)存放到數(shù)組中
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

這里有個輔助類 ContainerHelpers ,它的 binarySearch 方法會根據(jù)實際情況返回key所對應(yīng)的位置值

  • 如果mKeys數(shù)組中存在key佩抹,那么直接返回key所對應(yīng)的索引值

  • 如果mKeys中不存在key

    • key比mKeys中的所有的數(shù)據(jù)都大叼风,則返回~mKeys.length

    • key處于mKeys中的某個中間位置,則返回那個值比 key 大且大小與 key 最接近的值的索引棍苹。

可以看到无宿,哪怕key在數(shù)組中不存在, binarySearch 枢里,也會將key保存的最佳位置給返回回來孽鸡。

當(dāng)key的位置確定以后蹂午,會根據(jù)情況進(jìn)行數(shù)組的重新編排,重新編排的話彬碱,當(dāng)前的key和value在數(shù)組中的位置就會發(fā)生變化豆胸,所以會調(diào)用 binarySearch 重新獲取適合的插入位置。

最后調(diào)用 GrowingArrayUtils.insert 方法進(jìn)行數(shù)據(jù)的插入巷疼。

這個方法會判斷當(dāng)前的數(shù)組大小是否能夠繼續(xù)插入key晚胡,如果不可以的話,會進(jìn)行擴容皮迟。如果可以搬泥,會將i位置以后的數(shù)據(jù)往后移動一位,然后將i位置插入我們的key值伏尼。

移除元素

移除元素的函數(shù)有多個,我們一個個來看尉尾。

    public void remove(int key) {
        delete(key);
    }
    public void delete(int key) {
        //通過二分查找爆阶,獲取key所在位置
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        if (i >= 0) {
            //將所在位置的value設(shè)置為DELETED,然后標(biāo)記需要進(jìn)行整理。
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
    public E removeReturnOld(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        if (i >= 0) {
            //如果key所在的index的值不為DELETED沙咏,則返回數(shù)據(jù)辨图,并標(biāo)記對應(yīng)index的值為DELETED,
            if (mValues[i] != DELETED) {
                final E old = (E) mValues[i];
                mValues[i] = DELETED;
                mGarbage = true;
                return old;
            }
        }
        return null;
    }
    //刪除指定索引對應(yīng)的元素值
    public void removeAt(int index) {
        if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
            // The array might be slightly bigger than mSize, in which case, indexing won't fail.
            // Check if exception should be thrown outside of the critical path.
            throw new ArrayIndexOutOfBoundsException(index);
        }
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }

可以看到肢藐,不管哪種remove方法故河。實際的移除操作,只是把key所在的位置的value值設(shè)置為了 DELETED 吆豹,然后設(shè)置了 mGrabage 標(biāo)志位鱼的。并沒有進(jìn)行key數(shù)組和value數(shù)組的移動操作。

查找元素

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

    //獲取指定key的值痘煤,如果獲取不到凑阶,則返回指定對象。
    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
        //獲取key對應(yīng)的index位置
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果不存在衷快,或者i位置的數(shù)據(jù)已經(jīng)回收了宙橱,則直接返回
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }
    public E valueAt(int index) {
        if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        if (mGarbage) {
            gc();
        }
        return (E) mValues[index];
    }
    //根據(jù)value值,獲取其對應(yīng)的index信息
    public int indexOfValue(E value) {
        if (mGarbage) {
            gc();
        }
        for (int i = 0; i < mSize; i++) {
            if (mValues[i] == value) {
                return i;
            }
        }
        return -1;
    }

查找方法有的返回的是key所對應(yīng)的的信息蘸拔,有的是獲取index信息师郑。這里面進(jìn)行了區(qū)分操作。因為如果只是根據(jù)key值獲取value值的話调窍,不需要進(jìn)行數(shù)組的整理工作宝冕。而一旦涉及到了index的查找工作,那么就需要根據(jù) mGrabage 先進(jìn)行一次整理工作陨晶,然后才能進(jìn)行index的相關(guān)處理猬仁。

垃圾回收

SparseArray的垃圾回收并不是我們平時所理解的JVM的垃圾回收帝璧,只是因為當(dāng)我們進(jìn)行移除value的情況下,并沒有進(jìn)行數(shù)據(jù)的移除湿刽,只是設(shè)置了 mGrabage 的烁,而且將對應(yīng)位置的value設(shè)置為了 DELETED 來表示當(dāng)前位置是可以回收的。所以當(dāng)我們需要適應(yīng)索引時诈闺,就會出現(xiàn)索引無效的問題渴庆。所以需要通過垃圾回收來進(jìn)行數(shù)組的整理,將數(shù)組整理為連續(xù)的數(shù)據(jù)雅镊。

    //用于移除無用的引用襟雷,通過移動,將現(xiàn)在所有的key和value連續(xù)保存到數(shù)組中仁烹,而不會使某個位置的value為空
    //而且會將mSize賦值為實際使用的大小
    private void gc() {
        int n = mSize;
        //o 值用于表示 GC 后的元素個數(shù)
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            //如果value不是DELETED耸弄,證明當(dāng)前的位置數(shù)據(jù)是可用的
            if (val != DELETED) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
    }

優(yōu)劣

優(yōu)勢

  • 使用int類型作為key,避免了裝箱拆箱操作
  • 延遲了垃圾回收時機卓缰,只在需要的時候才進(jìn)行一次回收
  • 和Map每個存儲節(jié)點都是一個類對象不同计呈,SparseArray不需要用于包裝的結(jié)構(gòu)體,單個元素存儲成本更低廉
  • 在小數(shù)據(jù)量下征唬,二分查找效率更高一些捌显。

劣勢

  • 插入新元素會導(dǎo)致大量的數(shù)組移動
  • 數(shù)據(jù)量較大時,二分查找效率會變低

本文由 開了肯 發(fā)布总寒!

同步公眾號[開了肯]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扶歪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子摄闸,更是在濱河造成了極大的恐慌善镰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贪薪,死亡現(xiàn)場離奇詭異媳禁,居然都是意外死亡,警方通過查閱死者的電腦和手機画切,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門竣稽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霍弹,你說我怎么就攤上這事毫别。” “怎么了典格?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵岛宦,是天一觀的道長。 經(jīng)常有香客問我耍缴,道長砾肺,這世上最難降的妖魔是什么挽霉? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮变汪,結(jié)果婚禮上侠坎,老公的妹妹穿的比我還像新娘。我一直安慰自己裙盾,他們只是感情好实胸,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著番官,像睡著了一般庐完。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徘熔,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天门躯,我揣著相機與錄音,去河邊找鬼近顷。 笑死生音,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窒升。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼慕匠,長吁一口氣:“原來是場噩夢啊……” “哼饱须!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起台谊,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蓉媳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后锅铅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酪呻,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年盐须,在試婚紗的時候發(fā)現(xiàn)自己被綠了玩荠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡贼邓,死狀恐怖阶冈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塑径,我是刑警寧澤女坑,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布换棚,位于F島的核電站卢未,受9級特大地震影響臂港,放射性物質(zhì)發(fā)生泄漏椒涯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一碉就、第九天 我趴在偏房一處隱蔽的房頂上張望盟广。 院中可真熱鬧,春花似錦铝噩、人聲如沸衡蚂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毛甲。三九已至,卻和暖如春具被,著一層夾襖步出監(jiān)牢的瞬間玻募,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工一姿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留七咧,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓叮叹,卻偏偏與公主長得像艾栋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛉顽,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355