此文章依赖知识点
- Java内存模型
- Java类加载机制
- EventBus 使用
- EventBus 源码阅读
问题由来
- 学习EventBus过程中发现,粘性事件和普通事件都会被保存在一个final修饰的Map集合中。
- 由此就得出疑问:① 为什么不用static修饰?(按我的理解应该用static,因为static修饰这个集合就能一直活到App结束);② 为什么不是用static final进行修饰? ;③ 这个final集合能活多久?(太短的话,A、B两个Activity,A调用了register方法,B调用post方法发东西给A。如何保证post时,A不被回收?);④ 平常定义常量时,为什么要用static final?为什么叫编译期常量?
分析过程
首先是 static、final、static final的区别?
-
所有文章都传递了一个观点:static表达“唯一”,final表达“不变”
-
这就让我有个疑问了:
- 众所周知,static是类变量,类加载后会保存在方法区的.class文件中,因此static表达“唯一”,没问题;但是,final修饰的是常量,常量会保存在方法区的.class文件里的常量池中,因此也是类独有的。那么final也能表达“唯一”才对啊?
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等
- 解答:实际上final修饰基本数据类型,才算是常量,只有值会被放到常量池中,只有值可以算是“唯一”的,变量引用不能;而final修饰对象类型,不能算作是常量!final修饰的Map,是对象类型,可以看成是成员变量,应该会放在堆中;EventBus用final修饰Map,只是为了保证它不被指向其他内存地址罢了。
-
再来看看 问题④:为什么平常定义常量时,要用static final?为什么叫编译期常量?
-
如果只用static,那么①:它会在类加载的准备阶段,分配类变量的内存,并设置初始值,在类加载的初始化阶段(<clinit>)进行实际的赋值,这就说明,static变量会在编译期赋好值然后塞到.class对象中(这就是编译期的意思)。②:但是它可以在运行期间被更改,而且因为是类所有,一个对象实例更改后,其他对象实例拿到的都是更改后的值,则它不是一个常量。
比如:class B{ public static int a = 5; }
B b1 = new B(); B b2 = new B(); b1.a = 10; //b2.a 的值为10
总结:只用static可以保证编译期赋值,不能保证运行期不被更改!
-
如果只用final,那么 ①:它会在编译期(<clinit>)时,把值存到常量池中(比如final int d = 20,20会存到常量池),运行期调用<init>后才赋值,每个对象实例拿到的可能都不是同一个引用。②:但是运行期每个引用都不会再被更改。
比如:class A{ public final int i = new Random(30).nextInt(21); }
A a1 = new A(); A a2 = new A();
这里a1.i的值跟a2.i的值不一样!说明运行期才赋值。
总结:只用final可以保证<init>赋值后不会被更改 -
用static final,先得出结论:编译期就被赋值,而且永远不会被更改!
比如:
public static final int f = 20;
在类加载的准备阶段,把 f 初始化成0,然后初始化阶段<clinit>赋值为20,接着把20保存到常量池。
这样就能保证 编译期就生成了不会被更改的变量!
-
回到问题:①为什么不用static修饰?② 为什么不是用static final进行修饰? ③ 这个final集合能活多久?;
-
首先思考,final修饰对象引用的变量,可以算作是成员变量,那么会被保存到堆中,而堆中的东西会在对象被回收时GC
比如:class EventBus{ private final Map map= new HashMap; }
这个 map 会在回收 EventBus 时被回收,而EventBus什么时候被回收呢?
假如有Activity A 和 B,A中调用了new EventBus().register(xxx)
,把东西保存到map中, 接着跳转到B,此时GC,则A被回收了,A中相应的 EventBus实例 也被回收,导致map也被回收。那么在B中无论如何post,都找不到保存在map中的A了。
总结:只用final无法保证Map集合在EventBus.post
调用时不会被回收 -
此时觉得很奇怪,为什么 EventBus 只是用final修饰了Map呢? 我用static修饰,才会正确的执行啊:
比如:class EventBus{ private static Map map= new HashMap; }
此时这个map会被保存在方法区的.class文件中,是类共享的。
何时回收呢?在类卸载的时候才会回收。
何时卸载呢?没有任何引用的时候(① 加载它的类加载器被回收 ② ……③ ……)。
加载它的类加载器什么时候被回收呢?在App结束后。
因此,map能跟App活一样长。
而再加上final能保证map不会被指向其他map
所以,光用final修饰时肯定不对的,一定要用static!!
那难道是 EventBus 的编写者错了?
- 忽略了一点,我们使用EventBus时,都是
EventBus.getDefault().register(this)
这么用的,而 getDefault() 是一个单例,单例中的对象实例是用static修饰的,因此能保证它能活的跟App一样长,则它内部的Map也不会被回收。