WeakRef 不能直接构成 LRU 缓存,需配合显式访问序管理与 FinalizationRegistry 清理失效项;C# 中必须用 TryGetTarget 安全访问;缓存应按内存大小而非数量限容。

WeakRef 本身不能直接构成 LRU 缓存,强行用 WeakRef 包裹值后丢进 lru_cache 或 OrderedDict 会导致缓存项“查得到但取不出”,或者 GC 后残留空引用——这不是缓存失效,是设计断裂。
WeakRef 必须和显式 LRU 排序逻辑解耦实现
弱引用只解决“值是否还活着”,不解决“哪个该先淘汰”。你得自己维护访问顺序,且不能依赖值对象的生命周期来驱动淘汰。常见错误是把 WeakRef 当成缓存主体,结果:
- 缓存字典里堆满
WeakRef实例,deref()全返回None,但键还在、链表节点还在 - LRU 链表按访问时间排序,但没同步清理已失效项,导致尾部淘汰时反复踩空
- 图像被外部强引用持有(比如仍在 UI 控件中显示),却因长期未访问被误删
正确做法是:用普通 dict 或 Map 存键 → WeakRef 映射,另起一个 LinkedList 或 deque 管理 key 的访问序列,每次 get / set 都手动更新位置。
FinalizationRegistry 是清理 dangling 引用的唯一可靠手段
仅靠每次 get 时调用 ref.deref() 检查,无法处理“对象已被 GC、但 key 还卡在 LRU 链表里”的情况。这时必须靠 FinalizationRegistry 在对象真正回收瞬间触发清理。
- 注册时传入的
key必须是不可变值(如"assets/ui/button.png"),不能是图像对象本身或包装对象 -
registry.register(image, cleanup, key)必须在new WeakRef(image)之后立即执行,且key和后续字典 key 完全一致 -
cleanup函数里只能操作元数据:从链表中删除节点、从缓存字典中pop(key),绝不能访问image
漏掉注册,或注册键不一致,就等于关掉了自动清理开关——缓存会缓慢膨胀,直到 OOM。
C# 中 WeakReference 的 TryGetTarget 是安全访问的唯一入口
在 .NET 环境(如 dnSpy 图像服务改造)中,别用 WeakReference.Target 属性,它可能在多线程下返回已释放对象的引用(竞态)。必须用 TryGetTarget(out TValue value):
if (_cache.TryGetValue(key, out var weakRef) && weakRef.TryGetTarget(out var image))
{
_lruList.Remove(key);
_lruList.AddFirst(key);
return image;
}这个组合保证了原子性:既确认对象存活,又拿到强引用副本。漏掉 TryGetTarget 直接解引用 Target,会在 GC 刚完成的窗口期触发 NullReferenceException 或更隐蔽的内存损坏。
缓存容量设置要区分“图像数量”和“实际内存压力”
设 maxSize = 200 看似合理,但如果每张图是 4K RGBA 纹理(~128MB),200 张就是 25GB——这根本不是缓存,是内存炸弹。真正该限制的是总字节数,而不是 key 个数:
- 在
set时计算图像尺寸(image.PixelWidth * image.PixelHeight * 4)并累加当前占用 - 淘汰策略改为“从 LRU 尾部开始,逐个
TryGetTarget,有效则减去其尺寸,直到释放足够空间” - 记录每个
key对应的估算内存开销,存在单独的Dictionary<tkey long></tkey>中
否则,缓存看起来“没超限”,内存却早已失控。这点在图像密集型应用里最容易被忽略。

















