不太优雅的治理 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 了,它能干什么呢?它可以帮你 监听对象的回收
但是有两个注意点:
- phantomReference.get() 永远为 null。
- 一定要强引用这个 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。