如何应对Android面试官 -> 常用数据结构如何进行优化

前言


本章我们开始讲解性能优化相关的话题,首先我们来看下数据结构如何优化:

image.png

性能优化


性能优化的本质:线上 APM 的性能监控,而性能监控通常是以下技术点

ByteCode、Hook(PLT Hook)、JS注入(采集 Web 性能)、Gradle、ASM、javapoet

Java 层需要实现的性能监控能力

  • CPU 指标
  • 内存指标
  • FPS 指标
  • ANR
  • 卡顿
  • GC/OOM
  • 网络(http hook)
  • 功耗
  • 日志回捞

APM 框架的能力

  • 配置(注解 + json)
  • 数据链的保存
  • CPU、GPU、GC、电量
  • ANR FPS
  • Crash

数据结构


常用数据结构性能优化

ArrayList

内部是一个数组,又叫顺序表;

add

性能分析我们主要从 add、get、remove 等操作数据的接口角度来分析;我们来看下 add 方法

private void add(E e, Object[] elementData, int s) {if (s == elementData.length)elementData = grow();elementData[s] = e;size = s + 1;
}

我们来看下 grow 方法

private Object[] grow(int minCapacity) {return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}

使用了 Arrays.copyOf 方法,当前要添加数据的位置如果有值,就将当前位置开始的所有数据都向后移动一位,将要插入的数据放到当前位置;

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {@SuppressWarnings("unchecked")T[] copy = ((Object)newType == (Object)Object[].class)? (T[]) new Object[newLength]: (T[]) Array.newInstance(newType.getComponentType(), newLength);System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;
}

remove

同样的 remove 方法也是使用了 copy 的操作来移动数据;

private void fastRemove(Object[] es, int i) {modCount++;final int newSize;if ((newSize = size - 1) > i)System.arraycopy(es, i + 1, es, i, newSize - i);es[size = newSize] = null;
}

所以,不管是添加还是删除,实际上发生的都是元素位移,那么就比较耗费性能;

get

而 ArrayList 中效率较高的读取、设置数据是 get 和 set,

public E get(int index) {Objects.checkIndex(index, size);return elementData(index);
}

为什么 get 的效率高呢?

因为数组内存是连续的,数组存储数据是通过 数组的地址 + i * 「存入数据的字节」例如:elementData 对应的地址是:0x123ff,数组中存放的是 Object,那么第 i 个对应的就是 0x123ff + i * 4,然后就可以快速定位到这个 i 对应的地址在哪里;

所以 ArrayList 的查找快;但是,我们在 Android 开发中,并不能一股脑的上来就选择 ArrayList,因为它的添加和删除还是比较耗费性能的;

LinkedList

那么,针对删除和添加比较耗费性能的情况,我们应该如何进行优化呢? LinkedList 来了;

LinkedList 它是一个双向链表形式的数据结构,每一个元素都是 Node 节点,

private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}

根据源码可以知道,每一个节点都有指向下一个节点的元素,以及指向上一个节点的元素;

add

我们来看下 LinkedList 中是如何 add 的;

/*** Links e as last element.*/
void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;
}

可以看到,add 元素的时候,直接一个赋值搞定的;add 方法是移动指针,将要插入的节点的上一个节点的下一个指针指向要插入的节点,将要插入的节点的下一个节点的上一个指针指向要插入的节点,并将要插入的节点的上一个指针指向上一个,节点的下一个指针指向下一个。这样就插入了一个新的数据,而不需要移动元素;

remove

/*** Unlinks non-null first node f.*/
private E unlinkFirst(Node<E> f) {// assert f == first && f != null;final E element = f.item;final Node<E> next = f.next;f.item = null;f.next = null; // help GCfirst = next;if (next == null)last = null;elsenext.prev = null;size--;modCount++;return element;
}

可以看到,删除的时候,直接移除对应的指针即可;

所以插入删除效率高,因为是直接移动的指针;但是 LinkedList 在查找方面效率就会变得低下;

get

/*** Returns the (non-null) Node at the specified element index.*/
Node<E> node(int index) {// assert isElementIndex(index);if (index < (size >> 1)) {Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else {Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}

可以看到,这里使用了轮询的操作,因为每个节点的创建所在 class 可能不用,甚至都没在一个内存地址,也就无法像 ArrayList 那样在一个连续的空间通过计算找到,只能通过轮询的方式;

所以,也就造成了 LinkedList 的查找效率比较低下;

所以,如何选择数据结构,需要我们根据实际的业务场景来选择,需要高效查找的时候选择 ArrayList,需要高效添加删除的时候选择 LinkedList;

HashMap

那么,问题来了,有没有一种数据结构,上面两种的优点都包含了呢?既能查找快,也能添加快呢?答案是有的;它就是 HashMap

为什么它会快呢?因为 HashMap 中既有数据,又有链表;

HashMap 有两种形式的数据结构,分别是 1.7 之前和 1.7 之后;

1.7 之前 也就是 Android24 之前 用的都是 1.7 以 数组+链表 的形式;(一个数组,数组中的每一个节点都是一个链表)

在这里插入图片描述

1.7 之后,以 数组 + 链表 + 红黑树的形式;

put

我们来看下 HashMap 是如何 put 数据的;
在这里插入图片描述
如何保证一个 key 对应一个 value,通过 key 拿到 hash 值,通过 hash 值拿到 index,拿到 index 下标在数组中对应的链表,循环这个链表,找到对应的 key,找到了就替换;

key
int hash = hash(key.hashCode());
// 求模运算
int index = (n - 1) & hash

key 是 Object,Object 转 int 完成了装箱操作;

通过 indexFor 获取 index 对应的数组中的下标 i;这样我们就完成了 put 方法需要的 key 操作; 接下来是是 Value,我们来看下 Value 是怎么操作的;

Value

获取 key 的 hashcode 之后,添加 value,调用 addEntry 方法;
在这里插入图片描述
添加或者创建;这里一共执行了三个逻辑

  • 根据下标 bucketIndex 获取的值赋值给 HashMapEntry<K,V> e;
  • 然后 new HashMapEntry(hash, key, value, e); 将新加入的节点的 next 指向 e;
  • 再把新的节点赋值给 table[bucketIndex]
hash 碰撞

我们在使用求模运算获取下标 index 的过程,其实是一个多对一的过程,这个过程带来的问题就是 hashCode1 和 hashCode2 对应了同一个 index,这就产生了 hash 碰撞;

那么怎么解决 hash 碰撞呢?HashMap 提供了链表法来解决 hash 碰撞;

链表法

那么 HashMap 是如何保证一个 Key 对应一个 Value 的呢?
在这里插入图片描述

不关心链表内容空与否,都把这个当前节点作为新加入的节点的 next 节点,这样无论怎么添加,当前节点都是新加入的节点的 next 节点;这就是所谓的链表法,那么链表法到底是如何解决 hash 碰撞的冲突的呢?
在这里插入图片描述

数据 put 的过程中,key 为 KING 和 key 为 BLAKE 的 key 通过 取模 运算之后产生的 indexe 都是 4,那么这个时候就发生了 hash 碰撞,解决方案是,把 BLAKE 放到 KING 的下一个节点上;

get

在这里插入图片描述
在这里插入图片描述

在 getEntry 方法中,通过 key 获取 hash 值,然后获取对应的 index,然后轮询这个 table 获取对应的元素;

这就是通过『链表法』来解决 hash 碰撞的问题;

我们接着拐回去看 put 方法,看下是如何 put 同一个 key 的时候,value 是如何进行替换的;
在这里插入图片描述
在这个 for 循环中,put 的时候,要看下这个节点有没有链表,有的话就轮询这个链表,看下是否有和这个 key 一致的节点,key 相同,则对值进行覆盖;

通过 key 拿到 hash 值,通过 hash 值拿到 index,拿到 index 下标在数组中对应的链表,循环这个链表,找到对应的 key,找到了就替换;

扩容

put 的时候,随之而来的问题就是『扩容』的问题;什么是扩容?扩容的评价标准是怎样的?

加载因子

DEFAULT_LOAD_FACTOR = 0.75f,这个加载因子为什么是 0.75?

这个是经过大量的测算得来的;

阈值

0.75f * 16 = 12;超过这个阈值,就进行扩容,也就是说,HashMap 不会在到达 16 的时候才进行扩容,而是提前就进行了扩容;

默认的 HashMap 有多大?

这里说的其实是 HashMap 中的 table 大小,默认是 16,且必须是 2 的多少次幂;
在这里插入图片描述
DEFAULT_INITIAL_CAPACITY = 1 << 4

扩容的意义

避免 hash 冲突;假设 table 长度是 16,Hash1 = 17,Hash2 = 1,取模之后的 index 都是 1,如果扩容成 32,那么 Hash1 = 17 取模之后是 17,Hash2 取模之后是 2,就降低了 hash 冲突的可能;

hashmap 在哪种情况下效率最低?

所有 hash 全部碰撞,变成一个单链表的时候效率最低;

如何扩容的?

在这里插入图片描述

每次扩容 2 的 N 次倍,数组长度就会改变,hash 运算的结果就会跟着改变;
在这里插入图片描述

每次扩容之后,因为 table 表的长度改变了,依据 length 进行的 hash 运算就会全部失效,就需要将所有的节点都重新 hash 运算一下,获取新的 index;这个 transfer 就是 rehash 的过程;

所以 hashMap 耗性能的地方就在『扩容』,我们要尽可能的避免扩容操作;

如何尽可能的避免扩容?

new HashMap() 的时候计算下阈值,假设是 100 个节点,那么就是 100 / 0.75 + 1,HashMap 会把这个值再次转化为距离这个值最近的 2 的 N 次幂的一个数;
在这里插入图片描述
所以说:HashMap 是一个拿空间换时间的数据结构,当如果只需要扩容一个节点的时候,HashMap 也会扩容至 2 的N 次幂,导致一半的空间被浪费掉了;

而在 Android 中,空间对于手机来说还是比较宝贵的,那么在 Android 上如何应对这种 case 呢?

SparseArray

这个是 Android 量身定制的,是为了避免空间浪费而产生的数据结构;

在这里插入图片描述
采用双数组形式,key 为 int 型数组,value 为 Object 型数组;
在这里插入图片描述
key 的下标和 value 的下标是一样的,这样保证 key-value 能一一对应上;

put

在这里插入图片描述

可以看到 key 的 index 查找是通过 二分查找 的算法来查找的;
在这里插入图片描述

以及 key 和 value 的插入都是采用 System.arraycopy 来完成的;
在这里插入图片描述
这样一设计,不仅能解决 HashMap 带来的问题,而且还能越用越快;
在这里插入图片描述
越用越快的原因在这里,将需要移除的节点标记为 DELETE,并需要 arraycopy 进行移动数组,那么下一次有新的数据添加进来时,只需要将这个 DELETE 替换为新的数据即可,也就是说 put 的时候也就不需要进行 arraycopy 了;

SparseArray 的缺点就是:它的 key 只能是 int 类型;

那么,为了优化这种 case,应该怎么办呢? ArrayMap 来了;

ArrayMap

ArrayMap 是 HashMap + SparseArray 的思想结合体;我们主要看下 put 的时候,key 是怎么转换的;

public V put(K key, V value) {final int osize = mSize;final int hash;int index;if (key == null) {hash = 0;index = indexOfNull();} else {hash = key.hashCode();index = indexOf(key, hash);}if (index >= 0) {index = (index<<1) + 1;final V old = (V)mArray[index];mArray[index] = value;return old;}index = ~index;if (osize >= mHashes.length) {final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1)): (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);if (DEBUG) System.out.println(TAG + " put: grow from " + mHashes.length + " to " + n);final int[] ohashes = mHashes;final Object[] oarray = mArray;allocArrays(n);if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {throw new ConcurrentModificationException();}if (mHashes.length > 0) {if (DEBUG) System.out.println(TAG + " put: copy 0-" + osize + " to 0");System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);System.arraycopy(oarray, 0, mArray, 0, oarray.length);}freeArrays(ohashes, oarray, osize);}if (index < osize) {if (DEBUG) System.out.println(TAG + " put: move " + index + "-" + (osize-index)+ " to " + (index+1));System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);}if (CONCURRENT_MODIFICATION_EXCEPTIONS) {if (osize != mSize || index >= mHashes.length) {throw new ConcurrentModificationException();}}mHashes[index] = hash;mArray[index<<1] = key;mArray[(index<<1)+1] = value;mSize++;return null;
}
int indexOf(Object key, int hash) {final int N = mSize;// Important fast case: if nothing is in here, nothing to look for.if (N == 0) {return ~0;}int index = binarySearchHashes(mHashes, N, hash);// If the hash code wasn't found, then we have no entry for this key.if (index < 0) {return index;}// If the key at the returned index matches, that's what we want.if (key.equals(mArray[index<<1])) {return index;}// Search for a matching key after the index.int end;for (end = index + 1; end < N && mHashes[end] == hash; end++) {if (key.equals(mArray[end << 1])) return end;}// Search for a matching key before the index.for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {if (key.equals(mArray[i << 1])) return i;}// Key not found -- return negative value indicating where a// new entry for this key should go.  We use the end of the// hash chain to reduce the number of array entries that will// need to be copied when inserting.return ~end;
}

通过源码中

hash = key.hashCode();
index = indexOf(key, hash);

也是通过 二分查找 + 追加 的方式解决 hash 冲突的问题;

好了,常用数据结构的分析优化就到这里吧~

下一章预告


内存优化

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/47945.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tslib 库-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

tslib 库 tslib 简介 tslib 库&#xff0c;这是 Linux 系统下&#xff0c;专门为触摸屏开发的应用层函数库&#xff0c;开源 功能与作用&#xff1a;作为触摸屏驱动和应用层之间的适配层&#xff0c;封装了读取和解析触摸屏数据的复杂过程&#xff0c;提供API接口 数据处理&…

c++ 高精度加法(只支持正整数)

再给大家带来一篇高精度&#xff0c;不过这次是高精度加法&#xff01;话不多说&#xff0c;开整&#xff01; 声明 与之前那篇文章一样&#xff0c;如果看起来费劲可以结合总代码来看 定义 由于加法进位最多进1位&#xff0c;所以我们的结果ans[]的长度定义为两个加数中最…

零基础学SpringBoot(一)--初识SpringBoot

1. SpringBoot简介 SpringBoot 是Spring家族中的一个全新的框架&#xff0c;它用来简化Spring应用程序的创建和开发过程&#xff0c;也可以说Spring Boot能简化我们之前采用SSM(SpringMVC Spring MyBatis)框架进行开发的过程。 以前我们采用SSM框架进行开发的时候&#xff0c…

vue3前端开发-小兔鲜项目-二级分类页面无限加载的实现

vue3前端开发-小兔鲜项目-二级分类页面无限加载的实现&#xff01;实际的项目开发中&#xff0c;经常会遇到这需求。产品内容庞大&#xff0c;但是用户不可能一次性全部都加载请求的。当客户向下滚动&#xff0c;触碰到插件的底部时&#xff0c;会再次申请下一页内容。这样就会…

Adobe国际认证详解-动漫制作专业就业方向和前景

动漫制作专业的就业方向和前景随着创意产业的蓬勃发展而愈发广阔。这一专业涵盖了从角色设计、场景绘制到动画制作、特效合成等多个环节&#xff0c;是创意与技术相结合的典型代表。随着数字媒体和互联网的普及&#xff0c;动漫制作专业人才的需求正不断增长&#xff0c;为该专…

2024 杭电多校第一场

目录 目录 树 博弈 传送 树 给一棵根为 1 的有根树&#xff0c;点 i 具有一个权值 Ai 。 定义一个点对的值 f(u,v)max(Au,Av)|Au−Av| 。 你需要对于每个节点 i &#xff0c;计算 ansi∑u∈subtree(i),v∈subtree(i)f(u,v) &#xff0c;其中 subtree(i) 表示 i 的子树。 请…

Vscode离线下载对应版本的ms-python.vsix

一、查看vscode的版本号和发行时间 vscode界面中Help-About查看版本号和发行时间&#xff0c;ms-python的发行时间需要和这个时间相近&#xff1a; 二、在github仓库中查看ms-python有什么版本&#xff0c;以及发行时间 github仓库路径 https://github.com/microsoft/vsco…

虚幻引擎,体积雾、体积光、镜头泛光

1、体积雾 这里介绍的是用于地面的体积雾效果&#xff0c;效果如图1-1&#xff1a; 图1-1 首先&#xff0c;需要场景中存在指数级高度雾并开启体积雾&#xff08;如图1-2&#xff09;。然后创建材质&#xff0c;材质域选择“体积”&#xff0c;混合模式选择“Additive”。材质节…

shell脚本中for循环和while循环

目录 for循环 while 循环 前面说完了if判断语句&#xff0c;现在该来学习shell脚本中的另一个重点内容了&#xff0c;那就是循环语句。循环语句分为 for 循环和 while 循环&#xff0c;二者本质上来说是没有太大区别&#xff0c;但针对不同的情况&#xff0c;使用不同的语句可…

【Git-常用命令】一文搞懂学会git的常用命令以及使用技巧

【Git-常用命令】一文搞懂学会git的常用命令以及使用技巧 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免费获取相关内容文档关注&…

Lua 语法学习笔记

Lua 语法学习笔记 安装(windows) 官网&#xff1a;https://www.lua.org/ 下载SDK 解压&修改名称&#xff08;去除版本号&#xff09; 将lua后面的版本号去掉&#xff0c;如lua54.exe->lua.ext 配置环境变量 数据类型 数据类型描述nil这个最简单&#xff0c;只有值n…

Java基础(二十四):网络编程

目录 一、网络通信要素1、通信要素一&#xff1a;IP地址和域名1.1、IP地址1.2、域名 2、通信要素二&#xff1a;端口号3、通信要素三&#xff1a;网络通信协议 二、传输层协议&#xff1a;TCP与UDP协议1、TCP协议2、UDP协议3、三次握手4、四次挥手 三、网络编程API1、InetAddre…

收藏必备!ChatGPT助你快速阅读AI论文的全流程解析

尽管论文的旅程尚未开始&#xff0c;但在初次研究地图时&#xff0c;感觉就像在解读天书&#xff0c;难度很大&#xff01; 有什么有效的方法呢&#xff1f; 我们可以借助ChatGPT的强大功能。只需输入相关文献&#xff0c;它便能立刻解析出文献中的关键信息&#xff0c;迅速让…

【瑞芯微RV1126(板端摄像头图像数据采集)】②使用v4l2视频设备驱动框架采集图像数据

RV1126开发板&#xff1a;使用v4l2视频设备驱动框架采集图像数据 前言一、按键二、LCD显示三、V4L2 摄像头应用编程四、完整代码 前言 本系列的目的是&#xff0c;不仅仅将能够进行图片推理的模型部署于板端&#xff0c;还提供了两种摄像头数据采集的方法&#xff0c;集成到自…

redis的集群模式

为什么使用redis 提高并发性和可用性 提供了三种集群模式&#xff1a; 第一种&#xff1a;主从模式 概念&#xff1a;redis主从模式表示一个主节点跟若干个从节点。主节点负责读和写操作&#xff0c;而从节点只负责读操作&#xff0c;主节点的数据会自动同步到从节点上。 如何搭…

树莓派4B从装系统raspbian到vscode远程编程(python)

1、写在前面 前面用的一直是Ubuntu系统&#xff0c;但是遇到一个奇葩的问题&#xff1a; 北通手柄在终端可以正常使用&#xff0c;接收到数据 但在python程序中使用pygame库初始化时总是报错&#xff1a;Invalid device number&#xff0c;检测不到手柄 经过n次重装系统&am…

瑞吉外卖学习(一)

pom文件的导入中 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.6</version><relativePath/> <!-- lookup parent from repository --></…

AIGC Kolors可图IP-Adapter-Plus风格参考模型使用案例

参考: https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus 代码环境安装: git clone https://github.com/Kwai-Kolors/Kolors cd Kolors conda create --name kolors python=3.8 conda activate kolors pip install -r requirements.txt python3 setup.py install…

linux虚拟机主机配置网卡

问题复现 我的虚拟主机了连不上远程工具windTerm ,但是我的另一台虚拟主机可以连上 我的解决思路 ping ip 地址 发现能够 ping 通 查看 ifconfig 配置信息 我对比另一个虚拟主机 发现了我的子网掩码netmask有问题 解决方式 第一种 连接配置 配置 ipv4.addresses 192.168.1…

JavaScript青少年简明教程:赋值语句

JavaScript青少年简明教程&#xff1a;赋值语句 赋值语句&#xff08;assignment statement&#xff09; JavaScript的赋值语句用于给变量、对象属性或数组元素赋值。赋值语句的基本语法是使用符号 () 将右侧的值&#xff08;称为“源操作数”&#xff09;赋给左侧的变量、属…