对于性能优化,主要是从这几个方面进行探讨:数据结构,启动速度,布局,电量和网络,apk的大小等。本文从数据结构的角度进行举例:
1.学习数据结构优化的必要性:
在我们开发过程中,选好一个合适的数据结构或者说一个数据容器是尤其重要的,根据其优点,规避其缺点。比如ArrayList的优点是取元素快,但是存(这里指代替换某个元素)或者删除则效率并不高。所以当一个功能替换某个元素频繁或者删除某个元素频繁时候,则显然不是一个好的选择。一款app除了要有令人惊叹的功能和令人发指的交互之外,在性能上也应该追求丝滑的要求,这样才能更好地提高用户体验。但是如何更好的理解或者说自己能写出一个好的算法,则取决于我们对各个容器的特定认识。并且这个东西一天两天是讲不清楚的,还需要长久的去刷题,去练习。这里提供一个刷题网站。-----------leetcode. 相信每天刷几道,将会收获很多。
本文将从这几个点进行分析:ArrayList,LinkedList,HashMap,SparseArray,ArrayMap
首先我们从ArrayList看起:
它是一个线性数据结构,并且又是一个顺序表。它的特点是:
优点:它在物理上(内存)连续,所以查找元素快。
缺点:删除和增加元素需要大量移动元素。增删慢。
比如:(ArrayList内部实际上是一个数组)一个数组:int[] elementData. 如何能准确获取elementData数组的第i个元素呢?因为我们知道内存是连续的。elementData占四个字节,假设elementData的内存地址是0xfaff456 则第i的元素为:0xfaff456+I*4.
那如何解决这个缺点呢?这里就有一个LinkedList:
LinkedList是个什么?它是一个链表结构的容器。
优点:空间不连续,逻辑上连续,删除增加元素快
缺点:物理上不连续,查找要轮询。查找慢。
LinkedList是一个双链表,内部有一个指向下一个节点和一个指向上一个节点的指针( 如果第一个指向最后一个元素 最后一个指向第一个节点:形成了环,就是环形链表),为什么说它删除添加元素快呢?如下图:
所以LinkedList不需要去重新移动元素(直接挪动指针),从而让添加和删除元素性能得到提高,但是获取元素就只能去遍历了(内存不是连续的)。
现在纠结一个问题,有没有一种数据结构能同时包含上面两种优点呢:HashMap
现在在面试上HashMap是面试大概率是会问的,所以在这里我们来探究一下HashMap.
HashMap是由什么构成的呢?
在1.7版本之前,是由数组+链表构成的。
在1.8版本之后,是由数组+链表+红黑树组成的。
看一下HashMap是如何存放数据的:
存放:
先计算hash的值,根据hash的值找到数组位置(下标),再往链表中添加元素。
(源码里:h & (length-1)源码里) == hashCode % length = 下标的位置: 这两个公式其实是相等的。
纠结:为什么源码要用位运算,因为位运算效率是更高的。
hash碰撞:当存入的两个key值计算的hash是相同的,则就产生了hash碰撞,所以引入了链表解决hash碰撞。
查找:
先计算key的hash的值,根据hash值找到数组位置,再进行遍历数组,找到对应的元素。
删除:
先计算key的hash的值 ,根据hash值找到数组位置,再从遍历的链表中删除节点。
HashMap 的扩容(请看源码):
加载因子:Default_load_factory = 0.75;
阈值:0.75 * 16 = 12;
hash 的元素大于12的时候就扩容。超过百分之75容量的时候就扩容
源码中有一个加载因子默认是0.75,则其阈值位0.75 * 16 = 12 ,也就是说,当HashMap的元素个数大于12的时候就进行扩容,这里有一个标准,就是扩容的大小是2的次幂。
扩容的意义:(默认的hashMap的大小 == 16)
数学家研究发现在0.6~0.75最合适,因为避免hash值总是计算相同,这样很多元素都放在了一个链表中,这样形成了单链表,很显然这样会影响性能,这样扩容是为了避免冲突成单链表.
纠结一个问题,扩容肯定是会影响性能的。因为在扩容的时候,原本长度变了,所以hash值就变了,原本的元素都要重新进行计算,则就影响了性能。 那么hashMap是如何进行初始化的,为了节省内存,hashmap的初始化操作是在put中进行的(在用到的时候才真正初始化hash表,不用的时候就不初始化了);
那么为什么HashMap是2的次幂呢?
是为了这个运算 h& (length-1)
如果是length = 16 则16-1: 二进制为1111
如果不是2的次幂,如果是length = 10呢? 10-1:二进制1001
现在有一个hash值为6 二进制为110
现在有一个hash值为7 二进制为111
现在6对length = 16求模运算: 现在6对length = 10求模运算:
110. 110
1111 1001
结果:0110 结果:0000
现在7对length = 16求模运算: 现在7对length = 10求模运算:
111 111
1111 1001
结果:0111 结果: 0001
可以看出:只有最高位和最低为有用,如果都是1,则四位都可以得到不一样的值,所以更可以大的避免hash碰撞,从这个细节上来说又进行优化了一次。
在前面可以看到我们因为只有75%的空间可以使用,但是浪费了25%,我们还有什么数据结构对这个空间进行优化了吗?
Android量身定制:sparseArray;
sparseArray :
双数组结构。 一个数组存key,一个存value。但是key值只能存放int型的数据。内部采用了二分查找的方法.
增加:
根据key值进行二分查找,找到可以添加元素的位置,然后插入数据并移动数据
查找:
根据key值进行二分查找,找到数组下标,取出对应的value值
删除:
根据key值进行二分查找, 找到数组下标, 然后将value数组上的元素标记为delete,并不是真的删除,等下次再有有效的元素直接 添加进来,不用进行移动数据.
但是这个容器也是有缺点的因为key只能放入int型的数据。
ArrayMap: 任意类型的key
hashmap + sparseMap.
这个也是有hash冲突的,因为key值也要进行hash计算,当有hash冲突的时候,则直接进行添加到第二个位置,比如在1的下标处有一个节点了,则直接追加到2的位置。