Android-高级控件-ListView的优化技巧
2018-03-25 10:06
417 查看
今晚重新看了一下Android 消息传递的机制,其中发现了每一个Looper的对象放在了一个ThreadLocal对象里面,当时就非常的好奇,ThreadLocal这个类是怎么实现的。于是结合了Java 8的源代码和网上的资料对这个类有了一定的理解,在此记录一下,如果有错误之处,请各位指正!
本文参考资料:
1.深入分析 ThreadLocal 内存泄漏问题
2.【Java 并发】详解 ThreadLocal
3.周志明老师的《深入理解Java虚拟机》
其实,大家可以看看上面两篇文章,上面的两篇文章比我写的好。我这里写的都是我自己对ThreadLocal的理解!哈哈!
set方法的代码非常短,至少比我之前看的其他代码短得多。
首先我们是获取的是当前的线程,然后调用getMap方法来获得TreadLocalMap对象,再来看看getMap方法获取的是什么:
哎呀,这个也太简单了吧。这里简单的介绍一下threadLocals表示的含义,threadLocals其实就是Thread类的一个成员变量,也就是说每个Thread对象里面都有一个这个变量,这样就能保证这个变量在线程之间是保持独立,线程之间不会相互的影响。
回到我们的set方法里面来,然后调用map的set方法来进行赋值。请注意这里,第一个参数传入的是this,也就是说是当前的ThreadLocal,而这个参数是干嘛的呢?就是key。也就是说,这里是ThreadLocal对象来作为key的。
map的set方法又在干嘛呢?这里我不解释,待会在讲解ThreadLocalMap类时在详细解释set方法的作用。
get方法也是非常的简单,最终还是到了ThreadLocalMap里面去了。看来不得不去ThreadLocalMap是一个什么东西了。
这个成员变量也是非常的少,之前在看HashMap源代码的时候,那家伙!!!
Entry的封装也是非常简单,继承了WeakReference类,表示是一个弱引用。这里弱引用是什么东西呢?其实这个就能扯到JVM的GC那一块了,由于不是本文的重点,所以就不讲解(其实是自己太菜了!!!!)。这里就简单的介绍一下Java中的四种引用,摘抄自周志明老师的《深入理解Java虚拟机》。
1.强引用:在程序代码中普遍存在的,类似 Object obj = new Object()。这类的引用便是强应用。只要强引用存在,GC是永远不会回收该引用的对象。
2.软引用:用来描述一些有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收对象范围之中,进行第二次的回收。如果这次回收还没有足够的内存的话,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
3.弱引用:也是用来描述非必须的对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
4.虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
回到Entry里面来,我们需要注意的是key被弱引用关联了。这就是因为这一点,出
4000
现了ThreadLocal的内存泄露问题。详细请看:深入分析 ThreadLocal 内存泄漏问题。
这里先简单的概括一下,这段代码主要实现了的是什么。set方法的目的,我们都知道,就是将一个键值对成功保存在Entry数组里面,而保存的index不是指定,而是通过key的hashCode来计算的。将一个key的hashCode通过一个hash函数得到一个index值,而这个值就是这个键值对在数组里面的位置。
但是理想是美好的,现实是残酷的,当两个不同的key而产生相同的index时,就出现了hash碰撞。这种情况下,通常来说有两种解决的办法:1.开放地址法;2.链地址法。由于这两种方法在数据结构中是比较重要的内容,大家应该都学过,所以这里就不解释这两种方法的区别。
在ThreadLocalMap里面使用的是开放地址法,也就是说当出现了哈希碰撞时,会从当前的位置往后找一个为null的位置来保存键值对。
接下来详细的分析一下set方法的代码,首先是:
可能有些老哥在看到这段代码时,就有点懵逼了,这特么是什么鬼东西?如果我说这个其实就是取模运算,老哥们会不会what fuck?
我们详细的解释一下这段代码。当len为2的n次方时,key.threadLocalHashCode & (len-1)就相当于是一个取模运算;例如,当len为16时,上面的代码相当于就是对16取模。这个是为什么呢?因为16的二进制是:10000,当减1变成15时,二进制变为:1111。这样len - 1与任何一个数字进行与运算时,最终剩下来的都是那个数字的低4位,而低4位就是对16取模的结果。如果还不懂的话,看图:
如图,相当于是20%16的结果。但是,需要注意的是,len必须是2的n次方,这样才能保证-1之后,低位全为1。这个也是Map为什么是2倍扩容的原因之一。
通过上一步的hash操作,算是找到一个index来存储我们键值对,但是必须考虑到hash碰撞的情况。其中当hash碰撞了之后,是通过这个方法来获取下一个index位置:
当获取的inedx对应的数组中为null时,表示这个位置没有被占据。如果当前key已经在Entry数组中里面存在,直接替换值即可:
当时k为null这种情况怎么里面?这个就得引出之前需要注意的Entry中key为WeakReference关联,也就是说,在Entry数组里面,每个Entry对象的key可能随时都会被GC回收,从而导致k为null,由于Entry是一个对象,虽然Entry里面的key被回收了,但是key对应value并没有回收,从而导致这个value不可能再被get到。由于value是被一个强引用关联,除非value所在的Entry对象被回收,value才会被回收,由于这个Entry在数组中有一个强引用,所以除非收到将数组的相应位置置为nulll,否则这个强引用会一直存在。
由于以上原因,导致Entry对象不能回收,从而导致value内存泄露。
卧了个槽,怎么去分析内存了,跑题了跑题了。回到主题,根据上面的解释,我们知道了k为null是因为相应k被释放导致的,此时为了防止内存泄露,会去清理垃圾对象。如下:
当然如果找到了空位置的话,就占了。
添加数据之后,必不可少的就是判断是否达到了扩容的条件。
这个就是整个set的过程。这里,简单的总结一下。
1.在set方法里面,将key的hashCode对len进行取模运算来获取index。这里需要注意的是len必须是2的n次方。
2.如果发生了哈希碰撞了,set方法采用的是开发地址方法来解决的。
3.在进行开放地址方法时,有可能会出现key被回收的情况,这里会可能会导致内存泄露的问题。官方的手段是,对key为null的Entry进行清理。
从这段代码里面,我们可以看出来,如果index能获取得到的Entry的key与想要找到的key是一样的话,那么直接就返回,否则的话,就通过开放地址法来循环遍历寻找。
同时我们发现, 在getEntryAfterMiss方法里面是通过循环遍历找一个Entry。
1. ThreadLocal实现线程的局部变量是通过Thread的一个ThreadLocalMap成员变量,因为每个线程对象都持有自己的ThreadLocalMap对象,所以线程之间不会有影响的。
2.ThreadLocalMap使用的存储结构与普通的Map结构非常相似,只是ThreadLocalMap使用的开放地址法来解决哈希碰撞的。
3.ThreadLocalMap的key使用的是WeakReference对象关联,所以会出现key为null,但是value不能被释放的情况。官方在每次的set和get方法里面,会对Entry进行清理。
关于内存泄露的问题,大家可以看看这篇文章:深入分析 ThreadLocal 内存泄漏问题
本文参考资料:
1.深入分析 ThreadLocal 内存泄漏问题
2.【Java 并发】详解 ThreadLocal
3.周志明老师的《深入理解Java虚拟机》
其实,大家可以看看上面两篇文章,上面的两篇文章比我写的好。我这里写的都是我自己对ThreadLocal的理解!哈哈!
1.初识ThreadLocal
这里就不对ThreadLocal类的基本使用进行展开了,我们直接从源码入手,来理解ThreadLocal实现的原理。(1).set方法
当我们在使用ThreadLocal类时,通常会使用set方法来给当前的线程设置一个变量。我们来看看set方法到底为我们做了什么。public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
set方法的代码非常短,至少比我之前看的其他代码短得多。
首先我们是获取的是当前的线程,然后调用getMap方法来获得TreadLocalMap对象,再来看看getMap方法获取的是什么:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
哎呀,这个也太简单了吧。这里简单的介绍一下threadLocals表示的含义,threadLocals其实就是Thread类的一个成员变量,也就是说每个Thread对象里面都有一个这个变量,这样就能保证这个变量在线程之间是保持独立,线程之间不会相互的影响。
回到我们的set方法里面来,然后调用map的set方法来进行赋值。请注意这里,第一个参数传入的是this,也就是说是当前的ThreadLocal,而这个参数是干嘛的呢?就是key。也就是说,这里是ThreadLocal对象来作为key的。
map的set方法又在干嘛呢?这里我不解释,待会在讲解ThreadLocalMap类时在详细解释set方法的作用。
(2).get方法
在使用ThreadLocal类时,get方法也是不可避免的,通常我们调用get方法来获取在ThreadLocal里面保存的变量。public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
get方法也是非常的简单,最终还是到了ThreadLocalMap里面去了。看来不得不去ThreadLocalMap是一个什么东西了。
2.ThreadLocalMap
(1).成员变量
在理解ThreadLocalMap之前,我们还是来看看这类的里面有那些成员变量://默认的容量 private static final int INITIAL_CAPACITY = 16; //Entry数组,用来保存每个键值对的 private Entry[] table; //map的size private int size = 0; //阈值,用来扩容的 private int threshold; // Default to 0
这个成员变量也是非常的少,之前在看HashMap源代码的时候,那家伙!!!
(2).Entry
在理解代码之前,我们必须还说Entry这个东西:static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry的封装也是非常简单,继承了WeakReference类,表示是一个弱引用。这里弱引用是什么东西呢?其实这个就能扯到JVM的GC那一块了,由于不是本文的重点,所以就不讲解(其实是自己太菜了!!!!)。这里就简单的介绍一下Java中的四种引用,摘抄自周志明老师的《深入理解Java虚拟机》。
1.强引用:在程序代码中普遍存在的,类似 Object obj = new Object()。这类的引用便是强应用。只要强引用存在,GC是永远不会回收该引用的对象。
2.软引用:用来描述一些有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收对象范围之中,进行第二次的回收。如果这次回收还没有足够的内存的话,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
3.弱引用:也是用来描述非必须的对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
4.虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
回到Entry里面来,我们需要注意的是key被弱引用关联了。这就是因为这一点,出
4000
现了ThreadLocal的内存泄露问题。详细请看:深入分析 ThreadLocal 内存泄漏问题。
(3).set方法
准备的差不多了,我们开始研究ThreadLocalMap的set方法了。这里先贴出全部的代码,然后逐一的分析。private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
这里先简单的概括一下,这段代码主要实现了的是什么。set方法的目的,我们都知道,就是将一个键值对成功保存在Entry数组里面,而保存的index不是指定,而是通过key的hashCode来计算的。将一个key的hashCode通过一个hash函数得到一个index值,而这个值就是这个键值对在数组里面的位置。
但是理想是美好的,现实是残酷的,当两个不同的key而产生相同的index时,就出现了hash碰撞。这种情况下,通常来说有两种解决的办法:1.开放地址法;2.链地址法。由于这两种方法在数据结构中是比较重要的内容,大家应该都学过,所以这里就不解释这两种方法的区别。
在ThreadLocalMap里面使用的是开放地址法,也就是说当出现了哈希碰撞时,会从当前的位置往后找一个为null的位置来保存键值对。
接下来详细的分析一下set方法的代码,首先是:
int i = key.threadLocalHashCode & (len-1);
可能有些老哥在看到这段代码时,就有点懵逼了,这特么是什么鬼东西?如果我说这个其实就是取模运算,老哥们会不会what fuck?
我们详细的解释一下这段代码。当len为2的n次方时,key.threadLocalHashCode & (len-1)就相当于是一个取模运算;例如,当len为16时,上面的代码相当于就是对16取模。这个是为什么呢?因为16的二进制是:10000,当减1变成15时,二进制变为:1111。这样len - 1与任何一个数字进行与运算时,最终剩下来的都是那个数字的低4位,而低4位就是对16取模的结果。如果还不懂的话,看图:
如图,相当于是20%16的结果。但是,需要注意的是,len必须是2的n次方,这样才能保证-1之后,低位全为1。这个也是Map为什么是2倍扩容的原因之一。
通过上一步的hash操作,算是找到一个index来存储我们键值对,但是必须考虑到hash碰撞的情况。其中当hash碰撞了之后,是通过这个方法来获取下一个index位置:
i = nextIndex(i, len)
/** * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
当获取的inedx对应的数组中为null时,表示这个位置没有被占据。如果当前key已经在Entry数组中里面存在,直接替换值即可:
if (k == key) { e.value = value; return; }
当时k为null这种情况怎么里面?这个就得引出之前需要注意的Entry中key为WeakReference关联,也就是说,在Entry数组里面,每个Entry对象的key可能随时都会被GC回收,从而导致k为null,由于Entry是一个对象,虽然Entry里面的key被回收了,但是key对应value并没有回收,从而导致这个value不可能再被get到。由于value是被一个强引用关联,除非value所在的Entry对象被回收,value才会被回收,由于这个Entry在数组中有一个强引用,所以除非收到将数组的相应位置置为nulll,否则这个强引用会一直存在。
由于以上原因,导致Entry对象不能回收,从而导致value内存泄露。
卧了个槽,怎么去分析内存了,跑题了跑题了。回到主题,根据上面的解释,我们知道了k为null是因为相应k被释放导致的,此时为了防止内存泄露,会去清理垃圾对象。如下:
if (k == null) { //将旧的对象替换程新的Entry对象,并且清理垃圾对象 replaceStaleEntry(key, value, i); return; }
当然如果找到了空位置的话,就占了。
tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
添加数据之后,必不可少的就是判断是否达到了扩容的条件。
这个就是整个set的过程。这里,简单的总结一下。
1.在set方法里面,将key的hashCode对len进行取模运算来获取index。这里需要注意的是len必须是2的n次方。
2.如果发生了哈希碰撞了,set方法采用的是开发地址方法来解决的。
3.在进行开放地址方法时,有可能会出现key被回收的情况,这里会可能会导致内存泄露的问题。官方的手段是,对key为null的Entry进行清理。
(3).getEntry方法
看完了set方法,我们再来看看get方法。在ThreadLocal的get方法里面,是通过调用getEntry来获取一个Entry的private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
从这段代码里面,我们可以看出来,如果index能获取得到的Entry的key与想要找到的key是一样的话,那么直接就返回,否则的话,就通过开放地址法来循环遍历寻找。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
同时我们发现, 在getEntryAfterMiss方法里面是通过循环遍历找一个Entry。
3.总结
这个ThreadLocal感觉也不是很难,可能是自己太菜了吧,很多的问题都没有发现。在这里对ThreadLocal做一个总结。1. ThreadLocal实现线程的局部变量是通过Thread的一个ThreadLocalMap成员变量,因为每个线程对象都持有自己的ThreadLocalMap对象,所以线程之间不会有影响的。
2.ThreadLocalMap使用的存储结构与普通的Map结构非常相似,只是ThreadLocalMap使用的开放地址法来解决哈希碰撞的。
3.ThreadLocalMap的key使用的是WeakReference对象关联,所以会出现key为null,但是value不能被释放的情况。官方在每次的set和get方法里面,会对Entry进行清理。
关于内存泄露的问题,大家可以看看这篇文章:深入分析 ThreadLocal 内存泄漏问题
相关文章推荐
- Android-高级控件-ListView的优化技巧
- Android高级控件之ListView的优化以及下拉刷新页面
- [Android优化进阶] 提高ListView性能的技巧
- 浅谈Android控件中的ListView优化
- Android控件ListView优化
- Android高级控件ListView和GridView原理分析
- Android基本控件之ListView(二)<ListView优化>
- Android高级UI控件—ListView
- Android ListView控件优化
- Android的高级控件——ListView
- Android UI 设计(11):ListView 控件使用优化(五)
- Android 控件 ListView 的性能优化
- 浅谈Android开发中ListView控件性能的一些优化方法
- Android高级UI-listView的原理及优化
- Android控件之ListView的开发技巧
- 浅谈Android开发中ListView控件性能的一些优化方法
- Android高级控件之ListView
- Android用户界面之常用控件ListView 详解加优化
- Android基本控件之ListView(二)<ListView优化>
- Android高级控件系列二之第三方控件PullToRefreshListView下拉刷新的使用