节日限时场景资产临时卸载与内存控制:避免中秋活动爆内存

在大型在线游戏中,中秋、春节等节日限时运营活动通常会向主城或专属副本中堆砌大量高精度的临时资产:发光灯笼阵列、巨型月相雕塑、特色烟花粒子、限时 NPC 服饰以及多套节日 UI。然而,低端机(如 3GB/4GB 内存的 Android 机型及部分 iPhone)在主城日常运行时的物理内存(PSS)占用已处于 1.8GB ~ 2.2GB 的危险边缘。如果限时活动资产未经精细化生命周期管理与引用计数追踪,多加载 300MB 纹理与模型就会直接触发系统 OOM(Out of Memory)强制杀进程。
建立一套严密的引用计数追踪(Reference Counting)、动态 LRU 驱逐策略与分级纹理降采样释放机制,是避免节日活动期间 OOM 崩溃率飙升的核心防线。
内存泄漏与常驻陷阱分析
在节日资产管理中,导致内存只增不减的三大典型陷阱:
[资产引用追踪拓扑]:
[节日活动管理器]
│ (强引用持有)
▼
[FestivalAssetHandle] ──(引用计数 RefCount = 0)──> [LRU 待回收淘汰队列]
│ │
├─ RefCount > 0: 保持驻留 │ (超过生存时间 / 内存超标)
│ ▼
└─ 立即清除: GameObject.Destroy ──> Resources.UnloadUnusedAssets / Addressables.Release
引用计数与自动化 LRU 资产释放管理器实现
为了对节日限时资产做到“用完即走、精准归零”,我们构建了显式引用计数句柄(Asset Handle)与带内存预算警戒线的 LRU 驱逐器:
// FestivalAssetManager.cs – 节日限时资产生命周期与内存预算控制
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class FestivalAssetManager : MonoBehaviour
{
private static FestivalAssetManager s_Instance;
public static FestivalAssetManager Instance => s_Instance;
[Header("内存预算控制 (MB)")]
public float MaxFestivalMemoryBudgetMB = 180.0f;
public float EvictionCheckInterval = 5.0f;
// 资产缓存条目
private class AssetCacheEntry
{
public string AssetAddress;
public AsyncOperationHandle Handle;
public int RefCount;
public float LastAccessTime;
public long EstimatedSizeBytes;
}
private readonly Dictionary<string, AssetCacheEntry> m_ActiveAssets = new Dictionary<string, AssetCacheEntry>();
private readonly List<string> m_EvictionCandidates = new List<string>();
private float m_Timer = 0.0f;
void Awake()
{
s_Instance = this;
}
void Update()
{
m_Timer += Time.deltaTime;
if (m_Timer >= EvictionCheckInterval)
{
m_Timer = 0.0f;
TrimUnusedAssets();
}
}
// 异步加载限时资产并增加引用
public void RetainAsset<T>(string address, Action<T> onLoaded) where T : UnityEngine.Object
{
if (m_ActiveAssets.TryGetValue(address, out var entry))
{
entry.RefCount++;
entry.LastAccessTime = Time.realtimeSinceStartup;
if (entry.Handle.IsDone)
{
onLoaded?.Invoke(entry.Handle.Result as T);
}
else
{
entry.Handle.Completed += h => onLoaded?.Invoke(h.Result as T);
}
return;
}
// 新建加载句柄
var handle = Addressables.LoadAssetAsync<T>(address);
var newEntry = new AssetCacheEntry
{
AssetAddress = address,
Handle = handle,
RefCount = 1,
LastAccessTime = Time.realtimeSinceStartup,
EstimatedSizeBytes = 1024 * 1024 * 2 // 默认估算 2MB
};
m_ActiveAssets[address] = newEntry;
handle.Completed += h =>
{
if (h.Status == AsyncOperationStatus.Succeeded)
{
// 精确获取 Native 内存大小
newEntry.EstimatedSizeBytes = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(h.Result);
onLoaded?.Invoke(h.Result);
}
else
{
m_ActiveAssets.Remove(address);
Debug.LogError($"[FestivalAssetManager] 资产加载失败: {address}");
}
};
}
// 释放引用
public void ReleaseAsset(string address)
{
if (m_ActiveAssets.TryGetValue(address, out var entry))
{
entry.RefCount = Mathf.Max(0, entry.RefCount – 1);
entry.LastAccessTime = Time.realtimeSinceStartup;
// 引用归零,立即标记为可回收
if (entry.RefCount == 0)
{
CheckBudgetAndEvictImmediately();
}
}
}
private void TrimUnusedAssets()
{
long currentTotalBytes = 0;
m_EvictionCandidates.Clear();
foreach (var kvp in m_ActiveAssets)
{
currentTotalBytes += kvp.Value.EstimatedSizeBytes;
if (kvp.Value.RefCount == 0)
{
m_EvictionCandidates.Add(kvp.Key);
}
}
float currentMB = currentTotalBytes / (1024f * 1024f);
// 按最后访问时间升序排序 (LRU: 最久未使用的优先释放)
m_EvictionCandidates.Sort((a, b) => m_ActiveAssets[a].LastAccessTime.CompareTo(m_ActiveAssets[b].LastAccessTime));
foreach (var addr in m_EvictionCandidates)
{
// 如果内存超出预算或资产闲置超过 30 秒,执行彻底释放
if (currentMB > MaxFestivalMemoryBudgetMB || (Time.realtimeSinceStartup – m_ActiveAssets[addr].LastAccessTime) > 30.0f)
{
var entry = m_ActiveAssets[addr];
Addressables.Release(entry.Handle);
m_ActiveAssets.Remove(addr);
currentMB -= entry.EstimatedSizeBytes / (1024f * 1024f);
}
}
}
private void CheckBudgetAndEvictImmediately()
{
// 当内存紧迫时立即触发轻量裁剪
TrimUnusedAssets();
}
// 退出节日场景或活动结束时的全量强制清退
public void ForceUnloadAllFestivalAssets()
{
foreach (var kvp in m_ActiveAssets)
{
Addressables.Release(kvp.Value.Handle);
}
m_ActiveAssets.Clear();
Resources.UnloadUnusedAssets();
GC.Collect();
}
}
生产环境纹理降级与流式加载(Mipmap Streaming)
除了彻底卸载,对中低端设备应用**纹理 Mipmap 流式加载(Mipmap Streaming)**是压制节日显存峰值的另一利器:
网硕互联帮助中心

评论前必须登录!
注册