Android中引用类型说明

###前言
在java中存在GC机制,会回收掉那些它认为不再被使用的的对象,从而释放内存空间。

###在java中有4中引用类型

  1. 强引用(Strong References)
  2. 软引用(Soft References)
  3. 弱引用(Weak References)
  4. 虚引用(Phantom References)

    1.强引用

    强引用的对象不会被GC回收.

    <例1>
    class Test{

    Object a = new Object();
    

    }

该对象被回收的情景:
1.a = null,变量a释放掉对象的引用
2.Test类的对象被销毁。

1.1扩展 静态的强引用
<例2>
class Test{
    static Object a = new Object();
}

与<例1>不同就是在声明变量时添加了static关键字,从而把成员变量变成静态成员变量。

该对象被回收的情景:

  1. a = null,变量a释放掉对象的引用
  2. Test类被回收。

这里解释一下<例1>和<例2>的第二点
<例1> 变量a存在于Test类的对象中,例如:new Test()中就存在一个变量a,变量a引用着一个对象new Object();

<例1>变量引用图

<例2> 静态变量a存在于Test类中,只要Test.class被加载入内存,静态变量a就初始化完成,并生成、引用Object对象。

<例2>变量引用图

<例1>和<例2>的区别就在于,<例1>的变量a是由对象所持有的,<例2>的变量a是由类所持有的。

由于类的回收比对象的回收更加麻烦,所以静态成员变量的生命周期普遍比对象的变量的生命周期长,所以需要注意静态变量a的初始化和释放工作。

2.软引用

Android 2.3之前软引用只有在内存不足的时候才会被GC回收.
软引用在强引用释放了对象的引用后,会继续持有对象的引用,直到内存不足后才会释放。所在再做一些内存紧张时可以停止的工作时,可以使用软引用进行相关操作。
例如,Android的图片加载框架,通过软引用维护一个集合,保存一些最近常用的图片缓存(内存缓存)。在内存紧张的时候,这个集合的中的软引用所持有的对象被回收,达到释放内存的效果。

<例3>
Object a = new Object();
Log.e("ref","a所引用的对象的内存地址:"+a.toString());
Reference<Object> ref = new SoftReference<>(a);
a = null;
System.gc();
if(ref.get() != null){
    Log.e("ref","ref所引用的对象的内存地址:"+ref.get().toString());
}else{
    Log.e("ref","aRef.get() 为空");
}

可以看到日志打印的结果:

a所引用的对象的内存地址:java.lang.Object@19579822
aRef.get() 为空

从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

LruCache本质上是通过一个LinkedHashMap来维护内存缓存的数据,采用的是Lru(最近最少使用)算法。

LruCache<String,Bitmap> bitmapLruCache = new LruCache<String,Bitmap>(
                                            (int) (Runtime.getRuntime().maxMemory()/8/1024)){
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount()/1024;
    }
};

注意: 因为初始化传入的maxSize = Runtime.getRuntime().maxMemory()/8,单位为字节(B)。maxSize/1024即转化单位为KB.
为了保持一致,sizeof里面的value.getByteCount()单位(B)也需要转化为KB.

所以,在Android2.3之后,软引用其实也很容易被回收,并不一定是在内存不足的情况下才会被回收。

3.弱引用

GC只要发现只具有弱引用的对象,都会回收该对象的内存。

Hander使用例子图

在Android的日常编码中,我们很多时候会使用到handler进行子线程与UI线程的通信。可是直接使用会如上图那样让我们使用静态类的模式,即:

<例4>    
static class MyHandler extends Handler{
    Reference<Activity> mAtyRef;
    public MyHandler(Activity activity) {
    this.mAtyRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
    super.handleMessage(msg);
    if(mAtyRef.get() != null){
        //...
        }
    }
}

主要原因就在于直接使用匿名内部类默认会挟持当前activity的上下文对象。而如果持有该handler的子线程在进行耗时操作,那么如果activity意外关闭了,子线程依然在执行并调用了handler发送消息给Ui线程。那么很可能会导致程序抛出异常。

由于

  1. 子线程执行的不确定性
  2. handler本身可以发送延时信息(eg:mHandler.postDelayed(nRunnable,1000);)

所以在编码中需要假设当前activity == null 的情况下,代码的正确执行。

<例4>中用到弱引用持有当前的activity对象,那么当activity被finish后,持有activity对象的强引用会在接下来的一定时间内被释放。那么当子线程使用handler与UI线程交互时,因为activity已经不存在强引用了,所以mAtyRef.get()为空,以下代码不再执行。

注意:activity在finish后,对象不是立即被回收。

在类中使用弱引用持有对象,主要是为了使本类不会成为被持有对象被回收的限制,同时可以在此对象存在是进行一些操作。
如:<例4>中MyHandler类不会阻碍activity的回收,同时在activity未被回收的时候可以进行一些操作。

测试代码:

<例5>
Object a = new Object();
Object b = new Object();
Log.e("ref","a所引用的对象的内存地址:"+a.toString());
Log.e("ref","b所引用的对象的内存地址:"+b.toString());
Reference<Object> aRef = new WeakReference<>(a);
Reference<Object> bRef = new WeakReference<>(b);
a = null; //释放了a的强引用
System.gc();//注意System.gc()不是真正的执行GC,只是让JVM知道可以执行了.所以下面加个延时效果会更好。
if(aRef.get() != null){
    Log.e("ref","aRef所引用的对象的内存地址:"+aRef.get().toString());
}else {
    Log.e("ref","aRef.get() 为空");
}
if(bRef.get() != null){
    Log.e("ref","bRef所引用的对象的内存地址:"+bRef.get().toString());
}else {
    Log.e("ref","bRef.get() 为空");
}

运行结果:

a所引用的对象的内存地址:java.lang.Object@1cb54ea3 
b所引用的对象的内存地址:java.lang.Object@2c3a55a0 
aRef.get() 为空
bRef所引用的对象的内存地址:java.lang.Object@2c3a55a0

4.虚引用

<例6>
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> ref = new PhantomReference<>(a,queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

参考文章:
java中四种引用类型

分享到