目录

不太优雅的治理 MMKV FD 的方式

前言

最近看了很多 fd 的问题,在一些机型上 fd 数量超过 1024 便会抛出 OOM。

mmkv 正常是没什么问题的,但是如果使用了过多实例,也是会占用一部分的 fd。

mmkv 每一个实例会持有两个 fd(如果你没有释放过的话),在一个大工程里,很有可能同时存在 30+ 的 mmkv 实例,也就是 60+ 的 fd,因为每个业务可能都想有自己的 mmkv 实例(文件),甚至同一个业务下的子逻辑也想有自己的 mmkv 实例,加上 java 程序员从不考虑资源释放问题(me too),这也进一步加剧了 fd 不够用···最终 OOM

当然即使 60 多个 fd,相对于 1024 还很遥远,这里只是 提供一个可以回收 mmkv fd 的办法

最优解

最优解肯定是 手动释放资源

但是,工程里 mmkv 可能调用的地方有成百上千处,而且有的 mmkv 实例是需要或者最好是一直存在的,有的是不需要的,很难修改,还要去读业务逻辑。

一开始本来打算使用 finalize 的方式,按照 FileInputStream 之类的写法 close fd,但是 finalize 方法首先在分配对象时就存在消耗,而且是否执行还不会保证,同时还会使对象死而复生,影响 gc,且高版本已经无法使用此方法了~~

所以,最终采用 虚引用 + 单线程(referencequeue consumer) 来实现,不过线程的优先级我设置的比较低。

最后,再次强调,手动释放才是最靠谱的

次优解

使用 PhantomReference

PhantomReference

java 中的几种引用类型,八股面试常备。

StrongReference 就不说了,SoftReference 用来做缓存也在各种框架中见到了很多了,WeakReference Android 常用来防止内存泄露,也可以做特定缓存。

就剩下 PhantomReference 了,它能干什么呢?它可以帮你 监听对象的回收

但是有两个注意点:

  1. phantomReference.get() 永远为 null。
  2. 一定要强引用这个 phantomReference

ReferenceQueue

有关 referencequeue 之前有分析过,而且网上也有各式文章。它跟 gc 和 daemon 线程相关,通过 lock 来休眠和唤醒,具体细节可以去看相应文章。

具体实现

这里就拿想要释放 mmkv 来说,mmkv.close 方法会最终调用到析构函数,最终 close(fd) & munmap 回收资源。

如果不调用 close 方法的话,mmkv 的 native 层本身也会帮我们做一层缓存。所以,在 Java 层去缓存 mmkv 的意义并不是很大。

这里提供的方法,需要对 mmkv 进行一层包装,假设叫做 MMKVWrapper,而且又因为 mmkv 底层本身已经有缓存,多个 MMKVWrapper 对应的可能是同一个 mmkv 实例,所以我们根据 mmkvId引用计数 来判断是否需要回收

public class MMKVResourceRecycler {
  // 死循环,读取 referencequeue
  private Thread mCleanerThread;
  // 引用计数
  private Map<String, MMKVCounter> mMMKVCounterMap = new ConcurrentHashMap<>();
  // 强引用 MMKVKVReference(PhantomReference)
  private Map<Integer, MMKVKVReference> mReferenceMap = new ConcurrentHashMap<>();
  // referencequeue
  private ReferenceQueue<MMKVWrapper> mReferenceQueue = new ReferenceQueue<>();


  // 创建 MMKVWrapper 时,需要调用一下 observe,创建对应虚引用和更新引用计数
  public void observeMMKV(MMKVWrapper wrapper, MMKV mmkv, String key) {
        if (mmkv == null || key == null) {
            return;
        }
        MMKVKVReference reference = new MMKVKVReference(wrapper, mReferenceQueue, key);
        mReferenceMap.put(reference.hashCode(), reference);
        MMKVCounter counter = new MMKVCounter(mmkv);
        if (mMMKVCounterMap.containsKey(key)) {
            // 计数 + 1
            mMMKVCounterMap.get(key).refCounter.incrementAndGet();
        } else {
            mMMKVCounterMap.put(key, counter);
        }
  }

  private void initCleanerThread() {
    mCleanerThread = new Thread(TAG) {
        @Override
        public void run() {
            while (true) {
                try {
                    MMKVKVReference reference = (MMKVKVReference) mReferenceQueue.remove();
                    // 删除强引用
                    mReferenceMap.remove(reference.hashCode());
                    String key = reference.getKey();
                    MMKVCounter counter = mMMKVCounterMap.get(key);
                    // 计数 -1
                    if (counter.refCounter.decrementAndGet() <= 0) {
                        // close 或者其他 自定义逻辑
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
    };
    mCleanerThread.setDaemon(true);
    mCleanerThread.start();
  }

}
public class MMKVCounter {
    // 引用计数
    public AtomicInteger refCounter = new AtomicInteger(1);
    public MMKV mmkv;

    public MMKVCounter(MMKV mmkv) {
        this.mmkv = mmkv;
    }

    public void close() {
        if (mmkv != null) {
            mmkv.close();
        }
        mmkv = null;
        refCounter = null;
    }
}
public class MMKVKVReference extends PhantomReference<MMKVWrapper> {

    private String mId;

    /**
     * Creates a new phantom reference that refers to the given object and
     * is registered with the given queue.
     *
     * <p> It is possible to create a phantom reference with a <tt>null</tt>
     * queue, but such a reference is completely useless: Its <tt>get</tt>
     * method will always return null and, since it does not have a queue, it
     * will never be enqueued.
     *
     * @param referent the object the new phantom reference will refer to
     * @param q        the queue with which the reference is to be registered,
     */
    public MMKVKVReference(MMKVWrapper referent, ReferenceQueue<? super MMKVWrapper> q, String key) {
        super(referent, q);
        mId = key;
    }

    public String getKey() {
        return mId;
    }
}

主体代码如上所示

主要就是依靠一个 cleaner 线程,通过读取 referencequeue 获取到对应 reference 对象(记得后续删除调强引用),然后通过 id 将对应的 counter 计数 -1,如果已经 <= 0,则无人引用,可以执行对应的 close 逻辑。

同时,在创建 MMKVWrapper 时,记得 observe 一下,创建对应的 PhantomReference 和 Counter

其他解

可以直接设置 fd 的上限更大,从而避免因为 fd 数量限制导致的 OOM。