面试还在问 SparseArray?记住 3 句话,让你临时把佛脚抱好!

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

最基本的增删改查就这几个方法,不过 SparseArray 内部使用数组这种顺序表的结构,同样也提供了一些通过 index 的操作方式。

sparseArray.indexOfKey(int key);

sparseArray.indexOfValue(E value);

sparseArray.keyAt(int index);

sparseArray.valueAt(int index);

sparseArray.setValueAt(int index);

sparseArray.removeAt(int index);

sparseArray.removeAt(int index,int size);

SparseArray 的使用方式,符合我们的使用习惯,基本上看看方法名就大概知道它的含义。

接下来就来看看它的内部实现。

2.2 第一句话

SparseArray 内部使用双数组,分别存储 Key 和 Value,Key 是 int[],用于查找 Value 对应的 Index,来定位数据在 Value 中的位置。

在 SparseArray 内部,采用两个数组来存放数据,它们是一一对应的关系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hvi3ubHc-1570713306747)(https://upload-images.jianshu.io/upload_images/15679108-1caa8d90a43cf1f4?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

mValue 数组是为了存储数据的索引,它和 mKey 数组的关系是一一对应的,我们通过 key 的值,就可以定位出它在 mKey 数组中的 index,进而可以操作 mValue 数组中对应的位置。

因为是一一对应的关系,所有对数据的操作,都需要对这两个数据进行操作。第一句话就是为了对 SparseArray 数据是如何存储的,有一个基本的认识。

2.3 第二句话

使用二分查找来定位 Key 数组中对应值的位置,所以 Key 数组是有序的。

既然是使用数组这种顺序表,在查找的时候通常需要从前向后遍历数组,这时的时间复杂度就是 O(n),这明显不是 SparseArray 想要的。

在 SparseArray 中,采用二分查找算法,来快速通过 key 定位出它在 mKey 数组中的位置,二分查找的实现,在 ContainerHelpers.binarySearch() 中。正因为使用了二分查找, SparseArray 的查找操作,时间复杂度可以做到 O(logn)。

再看 SparseArray 的源码,所有和 key 相关的操作,第一步都是通过二分查找,定位出它的 index,再进行后续的处理,例如 get() 的实现。

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];

}

}

先忽略其中的 DELETE 标记的逻辑,后面会讲到。

binarySearch() 中会通过 key 查找对应的 index,如果查询不到,它会返回一个负数 i,注意这个 i 是有意义的,i 的相反数,就是 key 在 mKey 数组中,比较合适的位置(index)。

什么叫比较合适的位置?

就是虽然这个 key 没有在 mKey 数组中找到,但是如果把 key 插入到 mKey 数组的第 i 个位置上,mKey 数组依然是有序的。

我们知道,二分查找的前提条件,就是必须是针对有序并且支持下标随机访问的数据结构,所以它在执行插入操作的时候,必须保证 mKey 数据中的数据有序。也正因为如此,mKey 数组是一个基本类型的 int 数组,天然有序并且大小比对也简单。

我们看看 put() 方法的实现就清楚了。

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) {

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);

mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);

mSize++;

}

}

数组的插入,一定会伴随着数据的搬移。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z1eQoMgX-1570713306749)(https://upload-images.jianshu.io/upload_images/15679108-bf6950c0989b32b5?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

而在 put() 方法中,也会用到二分查找定位 key 的 index,我们主要关注其中的 GrowingArrayUtils.insert() 方法。

在这个 insert() 方法中,完成两个任务:

  1. 将数据插入到指定数组对应的位置上。

  2. 如果发现数组空间不够了,就生成一个更大的新数组,将数组通过复制的方法,动态扩容后搬移到新数组中,并返回新数组。

这些高级集合类,和基本数据结构有一个最显著的区别,就是它支持动态扩容,而 SparseArray 动态扩容的实现代码,在 GrowingArrayUtils 的 insert() 方法中,其原理就是一次动态复制来扩容。

扩容的逻辑也很简单,就是依据当前的 Size 动态放大,Size 在 4 以上时,每次扩容 2 倍。具体算法在 GrowingArrayUtils 的 growSize() 方法中。

public static int growSize(int currentSize) {

return currentSize <= 4 ? 8 : currentSize * 2;

}

第二句话,说明了 SparseArray 内部使用二分查找,在 mKey 数组中定位 key 的位置。又因为需要支持二分查找,所以 mKey 数组内存储的数据,必须是有序的。所以在 put() 操作的时候,就需要通过数组插入的方式,来保证数据的有序。

又因为使用了数组结构,在数据空间不够时,还需要采取动态扩容的方式,采用新旧数组复制的方式,增大数组的空间,这部分操作都封装在 GrowingArrayUtils 的 insert() 方法中。

2.3 第三句话

使用数组就要面临删除数据时数据搬移的问题,所以引入了 DELETE 标记。

SparseArray 用到了「数组」结构,在插入的时候为了给新数据腾位置,需要执行一个时间复杂度度为 O(n) 的搬移操作,这是无法避免的。

但是删除操作其实是有优化空间的。对数组的删除,如果不做数据搬移,数组中必然存在数据空洞。

SparseArray 对删除操作的优化,引入 DELETE 标记,以此来减少在删除数据时对数据的搬运次数,以此达到在删除时做到 O(1) 的时间复杂度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LN3PJ3Yw-1570713306750)(https://upload-images.jianshu.io/upload_images/15679108-a4c0f2f597f3dc7a?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

而在插入的时候,遇到 DELETE 标识,表示当前数据已经被删除掉了。

这样优化的删除的同时,对插入操作也起到了一定的优化,就像前面展示的 put()代码实现中,如果二分查找的结果,发现对应文字的 value 为 DELETE,则直接替换,减少了一次插入带来的数组的数据搬运。

注意 DELETE 标识是在 mValue 数组中存储的,mKey 中依然存储着它上一次对应数据的 key 值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OD14rlP4-1570713306751)(https://upload-images.jianshu.io/upload_images/15679108-064188dd1943ff74?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

但是引入 DELETE 标识就会有个问题,虽然数据被删除了,但是它依然在数组中占有位置空间,也就是它会影响到一些操作,例如在调用 size() 方法的时候,肯定是想知道真实数据的数量,而不应该包含 DELETE 标识的数据量。又例如在put() 操作时发现数组空间不够,但是数组内存在 DELETE 标识,此时应该做的是去清理 DELETE 标识,而不是去扩容数组。

引入 DELETE 让 delete() 操作可以做到 O(1) 的时间复杂度,但是也带来了问题,这就引入了 SparseArray 的 gc() 机制。

GC 我相信大家都比较熟悉,它在 JVM 里代表对内存的回收机制,而在 SparseArray 中,它标识了对 DELETE 标识的回收。

在一些必要的条件下,会触发 gc() 逻辑,来清理双数组中的 DELETE 标识。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tBc4FBEZ-1570713306752)(https://upload-images.jianshu.io/upload_images/15679108-48ded9a8c7c0e1a2?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

gc() 方法中,通过一次循环,就可以完成 DELETE 标识的清除,在 gc() 方法中,用了一个布尔类型的 mGarbage 属性,来记录当前 mValue 中,是否存在 DELETE 标识,这是判定是否需要 GC 的依据。。

gc() 会有一次循环,这是 O(n) 的时间复杂度,那什么时候执行 gc() 也是有讲究的。

如果你在 SparseArray 文件中搜索 gc() 方法的调用,你会发现有很多地方都用到了 gc() 方法。原则上,有两类操作可能会触发到 gc() 逻辑,所有和 size 相关的操作以及和数组扩容相关的操作。这很好理解,通过 index 获取数据,必须保证 size 的准确,所以如果有 DELETE 标识,必须执行 gc(),扩容时也是,存在 DELETE 标识说明还有剩余的空间,无需进行扩容。

到这里第三句话也理清楚了,引入 DELETE 标识是为了减少删除数据时数据的搬移次数,而这必然带来了数组的「空洞」,为了解决这个问题,又需要在适当的时候触发 GC 操作,来回收 DELETE 标识。

三. 查缺补漏


三句话理解 SparseArray 就说完了,这三句话帮助我们理解 SparseArray 使用的数据结构,实现原理,以及如何平衡遇到的问题。

理解了这些,就可以很好的区分 SparseArray 的使用场景,以及可以借鉴的地方。

当然我这样解释,一定是忽略了一些细节,但是这些都不是最重要的,最后这里再查缺补漏。

3.1 装箱和拆箱

SparseArray 的 Key 是基本数据类型的 int[],从文档和其他的文章中都会提到,自动装箱和拆箱对性能的影响,但我认为这不是最重要的。

如果你理解 SparseArray 的设计目标,就会发现用基本数据类型是一个必然的结果,SparseArray 就是为了节省内存空间而存在的。之所以用基本数据类型,首先基本数据类型本身比类更省空间,其次因为用到了二分查找,所以需要保证 mKey 数组的有序,排序就涉及到了数据大小的比对,使用基本数据类型也能很好的解决两数比对大小的效率问题。

3.2 省内存的说法

节省和浪费其实都是相对的,说到 SparseArray 更省内存,通常是与 HashMap 之间做对比。

HashMap 相较于 SparseArray 复杂很多,使用到的属性变量也多不少,而 SparseArray 从头到尾就是在操作两个数组,大小的差距可想而知。

同时 HashMap 为了应对哈希冲突,引入了「负载因子」,它的默认值是 0.75。什么意思呢?就是 table 的容量达到了指定尺寸的 0.75%,就会开始扩容,也就是必然有 25% 的空间是不存储数据而被浪费的。而 SparseArray 是可以把数组利用到最后一个空间,不会轻易扩容。

在 Stack Overflow 中就有人以 1000 条数据作为样本,计算 HashMap 与 SparseArray 对内存的占用情况,单位是 byte。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ggWUXZqW-1570713306752)(https://upload-images.jianshu.io/upload_images/15679108-798f5ca4e9d59cad?imageMogr2/auto-orient/strip)]

虽然这里的测试不是很严谨,但是依然可以看到他们在内存空间的使用上,已经不是一个数量积的了。

对计算方式有兴趣的,可以在文末找到 Stack Overflow 的地址。

3.3 对数据插入顺序的依赖

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

【Android思维脑图(技能树)】

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【Android高级架构视频学习资源】

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

维脑图(技能树)】**

[外链图片转存中…(img-k3ptVR1B-1713189591888)]

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-dAb1nhQY-1713189591888)]

【Android高级架构视频学习资源】

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-O2sFYHfP-1713189591888)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值