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阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容