从LeakCanary看如何判断对象被回收

733 阅读2分钟

前面已经了解了Service,Fragment,ViewModel对象的销毁时机,那么在触发销毁时机后,我们怎么判断这些对象有没有回收呢?

大家都知道在Java中有强引用,弱引用,软引用,虚引用四种引用方式,而我们判断对象是否回收,就需要通过弱引用来实现,针对弱引用而言。其提供了两种构造方法,如下图所示:

image-20230813162203745

其中我们重点需要关注第二个构造函数,从函数说明可以看出当该弱引用对象创建后,如果该弱引用所引用的对象被GC,则该弱引用对象会被放入给定的ReferenceQueue中,以便系统回收(需注意WeakReference对象和WeakReference引用的对象的区别,前者是WeakReference类的实例,后者是弱引用对象实例,弱引用对象实例在WeakReference的构造函数中传入),这也就意味着我们可以将应该被销毁的对象收集起来,并为他们依此指定ReferenceQueue,通过比对ReferenceQueue中对象情况和我们收集到的对象情况来判断该对象是否被正常销毁。

WeakReference标志化

image-20230813163229106

我们可以看到WeakReference本身是范型对象,这种情况下为了存储多种对象,我们通常会考虑范型T直接指定为Object类型,这也就导致指定WeakReference没办法和其他WeakReference区分,进而进行比较,此时就要求我们要为WeakReference生成唯一标识。

这里我们通过自定义WeakReference来实现,为每一个WeakReference对象赋予一个唯一标识mKey,代码如下:

 public class KeyedWeakReference extends WeakReference {
     private String mKey;
 ​
     public KeyedWeakReference(String key, Object referent, ReferenceQueue q) {
         super(referent, q);
         mKey = key;
     }
 ​
     public String getKey() {
         return mKey;
     }
 ​
     @NonNull
     @Override
     public String toString() {
         return "KeyedWeakReference{ mKey=" + mKey + ",Object=" + get() + " }";
     }
 }

监听对象回收

当某一对象需要回收时,首先我们将该对象包装在WeakHashMap中,以UUID为key,以WeakReference对象为value,随后将该对象的ReferenceQueue指定为我们自定义的,在一段时间后遍历ReferenceQueue,将Queue中存在的所有WeakReference对象按照key从WeakHashMap中移除,HashMap中剩下的就是有可能发生了内存泄漏的对象。

详细的实现代码如下:

 public class ObjectWatcher {
     private static final String TAG = "ObjectWatcher";
     private ReferenceQueue mReferenceQueue;
     private WeakHashMap<String, KeyedWeakReference> mReferences;
 ​
     private ObjectWatcher() {
         mReferenceQueue = new ReferenceQueue<Object>();
         mReferences = new WeakHashMap<>();
     }
 ​
     private static volatile ObjectWatcher mInstance;
     public static ObjectWatcher getInstance() {
         if (mInstance == null) {
             synchronized (ObjectWatcher.class) {
                 if (mInstance == null) {
                     mInstance = new ObjectWatcher();
                 }
             }
         }
         return mInstance;
     }
 ​
     public void watch(Object object) {
         String key = UUID.randomUUID().toString();
         Log.d(TAG, "watch object:" + object + ",key:" + key);
         KeyedWeakReference weakReference = new KeyedWeakReference(key, object, mReferenceQueue);
         mReferences.put(key, weakReference);
         Handler mainHandler = new Handler(Looper.getMainLooper());
         mainHandler.postDelayed(new Runnable() {
             @Override
             public void run() {
 ​
                 KeyedWeakReference keyedWeakReference = null;
                 do {
                     keyedWeakReference = (KeyedWeakReference) mReferenceQueue.poll();
                     Log.d(TAG, "keyedWeakReference:" + keyedWeakReference);
                     if (keyedWeakReference != null) {
                         mReferences.remove(keyedWeakReference.getKey());
                         Log.d(TAG, "object has been destroyed:" + keyedWeakReference.toString());
                     }
                 } while (keyedWeakReference != null);
             }
         }, 5000);
     }
 ​
 }

在上述代码中,我们将需要观察的对象通过watch方法传入,随后创建KeyedWeakReference对象,分别将该对象装入ReferenceQueue(系统底层代码实现)和mReferences WeakHashMap中,随后在5秒后遍历ReferenceQueue,确实监听到了对象被回收,日志打印如下:

image-20230813164420870

结合上文我们就可以判断一个对象是否已经被回收了,当然针对WeakHashMap中仍然存在的对象,我们可以触发一次GC后,再次遍历观察。

为什么是弱引用,相信有熟悉四大引用的朋友,也看到过软引用和虚引用的构造函数,这两种引用的构造函数也可以指定ReferenceQueue,如下图所示:

image-20230813201912910

image-20230813201953656

那么为什么不使用软引用或者虚引用,非要使用弱引用呢?Github issue上也有同样的疑问,如下图:

image-20230813202511693

从图中可以看到,这里主要的考虑应该是触发的频次,对于弱引用而言,其在下次GC时就会触发。