ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity Addressable缓存路径重定向:把C盘资源缓存改到D盘的完整方案

Unity Addressable缓存路径重定向:把C盘资源缓存改到D盘的完整方案 C盘被塞满的那天早上我先是以为某个美术资源制作软件开了自动备份查了一圈没找到后来是另一个同事的电脑也中招才顺着日志定位到AppData/LocalLow底下一个按公司名和产品名生成的目录里面全是 Addressable 下载组解出来的 AssetBundle。从那之后我养成了一个习惯只要主工程用了 Addressable接入第三天就一定把缓存路径重定向到其他盘绝不跟系统盘纠缠。这篇内容写给正在用 Unity Addressable 做资源热更、或者刚把项目升级到 Addressable 的团队。文章不聊怎么让 C 盘变干净这种一次性清理而是把缓存根目录彻底改到 D 盘或 E 盘的完整方案包括初始化时机、代码写法、配置文件做法、启动参数覆盖以及为什么很多时候你改了代码却不生效。适合技术负责人、客户端主程、负责打包管线和资源管理的同学参考。1. 默认落盘逻辑先搞懂 C 盘里那堆文件是谁写的1.1 默认目录到底从哪里来Unity 里有个核心属性叫Application.persistentDataPath它在 Windows 上默认指向C:/Users/你的用户名/AppData/LocalLow/公司名/产品名Addressable 的远端资源组下载后默认缓存就交给UnityEngine.Caching管理而Caching的默认写入目录正是基于persistentDataPath生成的。也就是说你根本不需要主动在代码里写任何文件只要 Addressable 在运行时从远程加载过一次资源这个目录下就会慢慢累积出几百 MB 甚至几十 GB 的 AssetBundle 分片。很多团队不重视这件事是因为开发和早期测试阶段资源量小C 盘通常扛得住。可一旦进入持续迭代期每次包体更新都会触发新版本的下载旧版本的文件又未必及时回收你就能看到空间像漏水一样每天少一点。等到 Windows 报“磁盘空间不足”再一个个去删缓存已经晚了。1.2 远程组和本地组谁在写缓存谁只读要理解为什么改 Build Path 不等于改缓存得先把 Addressable 的分组逻辑掰开。本地组Local构建时打进安装包运行时从StreamingAssets加载路径由LocalBuildPath和LocalLoadPath决定不进入下载流程所以不会往persistentDataPath写东西。远程组Remote构建产物上传到 CDN 或自建服务器运行时通过 URL 下载。下载后的资源默认就落在Caching管理的目录里也就是我们看到的 C 盘膨胀元凶。本地组的问题是包体变大远程组的问题是运行时要拉数据。大多数项目会同时开两组基础 UI、启动流程、核心玩法放本地组关卡资源、角色皮肤、活动配置放远程组。远程组一旦启用缓存路径就会被访问所以自定义路径这件事本质上是在管远程组的落盘位置。只要理解了“缓存目录只和下载场景强相关”后面读代码就不会一头雾水。你改LocalLoadPath只影响本地包的加载位置不会让远程缓存自动挪窝。2. Addressable 自带的那几个路径设置为什么救不了你2.1 改了 Build Path 和 Load Path缓存还是进 C 盘不少刚接触 Addressable 的同学会去AddressableAssetSettings里改远程组的 Build and Load Path希望这样就能把资源放到 D 盘。这个做法能解决“构建产物放在哪”但构建产物只是发布到服务器之前的过程文件它在本地工程目录下的位置和运行时缓存完全是两件事。举个例子远程组的RemoteBuildPath通常配置成ServerData/{平台名}这个目录只是给打包机上传用的最终会被传到 CDN。RemoteLoadPath配置的是http://xxx/{平台名}/这类完整 URL打包机只在构建时替换 URL 模板不会把它映射成本地物理磁盘路径。运行时下载好的文件由 Caching 系统接管路径完全由CachingAPI 决定。所以你想通过 Addressable 设置面板把运行时缓存指到 D 盘根本没有对应选项。2.2 直接改 persistentDataPath此路不通那绕回更底层的Application.persistentDataPath能不能在游戏启动时把这个属性改了答案是不能。它是 Unity 提供的只读属性底层原生实现会按平台规则计算路径官方没有暴露可写的 setter。想通过它实现“一键换目录”是不现实的除非你自己用原生插件去劫持文件系统映射但这种方案跨平台工作量大、维护成本极高任何一次操作系统大版本升级都可能让你翻车不建议碰。所以正确做法是用UnityEngine.Caching的 Cache 对象来指定缓存根目录。它本来就是绕开persistentDataPath的官方后门Addressable 内部的下载模块会按Caching.currentCacheForWriting找缓存设对了后续所有远端资源都会落到你指定的目录里。3. 自定义路径的真正实现启动脚本、配置文件、命令行覆盖3.1 先定初始化时机再写重定向代码Addressable 在运行时有一个初始化操作叫做Addressables.InitializeAsync()。这个操作会读取资源目录Content Catalog并准备好下载管理器和缓存处理器。必须在它执行之前完成缓存目录重定向。如果项目其他模块在启动场景里调用了Addressables的任何 API比如LoadAssetAsync、CheckForCatalogUpdates那么初始化可能已经被隐式触发了。推荐的挂载时机是RuntimeInitializeOnLoadMethod配合初始化参数BeforeSceneLoad。这样能在任何场景代码执行之前先把缓存路径设定好。你也可以把它放在自己写的 Bootstrap 场景脚本里只要确保它在首个 Addressable 调用之前执行即可但BeforeSceneLoad是最省心的方式因为它不依赖场景加载顺序也不需要你去调整脚本执行优先级。3.2 最小可用的缓存路径重定向脚本下面这段代码是我项目里最核心版本的简化里面包含从环境变量读路径、从默认值兜底、创建物理目录、绑定当前可写缓存、输出日志。using System; using System.IO; using UnityEngine; public static class AddressableCacheRedirector { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void RedirectBeforeAddressablesInit() { #if !UNITY_EDITOR string cacheRoot Environment.GetEnvironmentVariable(ADDRESSABLE_CACHE_ROOT); if (string.IsNullOrEmpty(cacheRoot)) { cacheRoot D:/AddressableCache/ Application.companyName / Application.productName; } try { Directory.CreateDirectory(cacheRoot); Cache cache Caching.GetCacheByPath(cacheRoot); if (cache.valid) { Caching.currentCacheForWriting cache; Debug.Log($[CachePath] current cache path - {cache.path}); Debug.Log($[CachePath] spaceOccupied - {cache.spaceOccupied} bytes); } else { Debug.LogWarning($[CachePath] invalid cache at {cacheRoot}, fallback remains {Caching.currentCacheForWriting.path}); } } catch (Exception ex) { Debug.LogError([CachePath] redirect failed: ex); } #endif } }注意我加了#if !UNITY_EDITOR。编辑器下 Addressable 通常走Use Asset Database模式也就是直接引用源资源不下载、不缓存如果强制在编辑器里切换缓存目录反而可能干扰你查看真实资源状态。更稳妥的做法是让编辑器逻辑保持原样真机测试和正式包里才启用重定向。代码里有几个细节值得展开。Caching.GetCacheByPath会按路径找到已登记缓存如果这个路径第一次出现它会自动创建并登记一条 Cache 记录返回值就是 Cache 对象。判断cache.valid是防止路径非法、磁盘不可写时接着往下执行。Caching.currentCacheForWriting是全局唯一属性赋一次值就够了后续所有需要写缓存的操作都会找它。3.3 把缓存根目录做成可配置项避免每台机器都改代码固定写死D:/AddressableCache只能解决你自己的电脑对于工作室内的多台制作机、测试机来说有人 C 盘是固态有人 D 盘是机械还有人外接移动硬盘插在 E。如果每次换机器都改代码重新打包效率太低了。常见的做法是引入一个轻量配置文件程序启动时读取没有配置文件就走默认值。配置我放在StreamingAssets目录下文件名叫cacheConfig.json{ cacheRoot: D:/AddressableCache, maxCacheSizeMB: 10240 }读取逻辑放在重定向脚本里如果文件存在且内容合法则用文件里的缓存放根目录。代码会优先读环境变量其次读配置文件最后走默认值using System; using System.IO; using UnityEngine; [Serializable] public class CacheConfig { public string cacheRoot; public long maxCacheSizeMB; } public static class AddressableCacheRedirectorV2 { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Run() { #if !UNITY_EDITOR string cacheRoot GetCacheRoot(); if (string.IsNullOrEmpty(cacheRoot)) return; Directory.CreateDirectory(cacheRoot); Cache cache Caching.GetCacheByPath(cacheRoot); if (cache.valid) { Caching.currentCacheForWriting cache; } #endif } private static string GetCacheRoot() { string envRoot Environment.GetEnvironmentVariable(ADDRESSABLE_CACHE_ROOT); if (!string.IsNullOrEmpty(envRoot)) { return envRoot; } string configPath Path.Combine(Application.streamingAssetsPath, cacheConfig.json); if (File.Exists(configPath)) { string json File.ReadAllText(configPath); CacheConfig config JsonUtility.FromJsonCacheConfig(json); if (!string.IsNullOrEmpty(config.cacheRoot)) { return Path.Combine(config.cacheRoot, Application.companyName, Application.productName); } } return D:/AddressableCache/ Application.companyName / Application.productName; } }这里把配置根目录和最终缓存目录做了拆分配置里写的是D:/AddressableCache实际业务缓存目录会在后面再拼上公司名和产品名。这样多个项目共享同一个盘符时不会互相污染。如果你只做一个游戏不拼公司名也行但拼上一层至少能免掉你日后排查莫名其妙的资源冲突。3.4 支持启动参数-cachePathxxx配置文件适合策划、测试同学手工在本地调试但很多自动化测试环境是用命令行批量跑客户端的没法每次手工编辑文件。为了兼容这种场景我在初始化脚本里加了启动参数解析逻辑很简单private static string ParseCommandLineCachePath() { string[] args Environment.GetCommandLineArgs(); for (int i 0; i args.Length - 1; i) { if (args[i] -cachePath !string.IsNullOrEmpty(args[i 1])) { return args[i 1]; } } return null; }调用优先级变成启动参数 环境变量 配置文件 默认值。测试机需要临时换盘时在 CI 脚本里多拼一个参数就行不需要重新出包。这一步看起来不起眼但遇到现场 QA 反馈“磁盘写满导致测试中断”时这个参数能让你保持冷静因为你只要远程改一下启动命令就可以改善情况而不是等第二天重新打一个包跑过去。4. 验证路径迁移是否真实生效4.1 日志里看三条关键信息写完重定向脚本后不能只盯着第一次启动的日志就完事你需要确认 Caching 系统真的把可写缓存切过去了。在开发阶段我建议把以下三个关键值全部输出Caching.currentCacheForWriting.path当前可写缓存物理位置。Caching.currentCacheForWriting.spaceOccupied已占用字节数。Caching.currentCacheForWriting.spaceFree剩余可写字节数。启动一次控制台如果能看到类似下面这段日志说明基础绑定已经成功[CachePath] current cache path - D:/AddressableCache/ExampleCompany/ExampleGame [CachePath] spaceOccupied - 0 bytes [CachePath] spaceFree - 268435456 bytes注意首次重定向后spaceOccupied为 0 是正常的因为旧缓存还在 C 盘旧地址里当前 Cache 对象刚指向新路径并未发生任何下载。接下来去远程组触发一次资源下载比如进入一个新地图再等下载完成回去看spaceOccupied应该变成几 MB 或几十 MB。4.2 到文件系统里确认文件真的落在新目录日志会说话但文件系统作更好验证。打开资源管理器手动跳到D:/AddressableCache/ExampleCompany/ExampleGame正常情况下你应该能看到一层层目录最底层放着带哈希命名的 AssetBundle 文件。文件名通常不是给人类看的而是一长串十六进制加.bundle扩展名这就是 Caching 系统根据 Content Catalog 的 Hash 生成的落盘键。如果你在断网状态下第二次启动同一个版本的游戏PlayMode 下同样能进入之前已下载过的资源说明资源已经命中本地缓存不需要重新下载。这个“断网二次启动”是最有说服力的验证方式它能证明下载后的数据确实保存到了新路径并且 Addressable 的校验逻辑读到了它而不是每次启动都回到远程拉一遍。4.3 用一条 PowerShell 命令看盘子到底少了多少如果嫌一个个点目录麻烦可以打开 PowerShell 快速算目录体积$size (Get-ChildItem -Path D:/AddressableCache -Recurse -File | Measure-Object -Property Length -Sum).Sum {0:N2} GB -f ($size / 1GB)输出结果对比任务管理器里的磁盘占用能直观看到迁移效果。这个命令也适合定期用来观测缓存增长曲线比如每周记录一次判断是不是某个版本把缓存写爆了。5. 常见失效原因与排查链路改了不生效问题出在哪5.1 失效场景对照表我在内部技术群里接过很多次“重定向没效果”的提问归纳下来真正的原因是重复的、可枚举的。这里列一个排查表你按照顺序对号入座会比翻源码更快。现象根因处理方式日志里没有出现[CachePath]脚本没进 Android 构建或者被宏裁剪确认挂了RuntimeInitializeOnLoadMethod且平台宏正确日志里有重定向成功但资源还是从 C 盘读Addressable 初始化在重定向前已被隐式触发搜索所有 Addressable API 调用把首个调用延后到重定向之后cache.valid false路径不可写或不存在权限预先创建目录检查盘符下写权限旧缓存文件还在 C 盘不消失重定向只影响新下载不影响历史缓存手动删除旧目录或写工具清理指定旧缓存WebGL 平台没有生效浏览器环境下 Caching 底层可能是 IndexedDB 模拟路径不要指望在 WebGL 上通过 Caching API 改本机任意路径播放模式 Play Mode 不生效编辑器走 Use Asset Database 不模拟缓存用真机构建验证不要拿编辑器播放模式下结论5.2 最容易被忽略的初始化顺序问题很多团队把重定向脚本写好发布会前一天才发现问题生产包里资源加载总是走到老路径客户端表现是下载重复、缓存越积越大。我拉日志看过之后判断通常是有一个 SDK 在Awake或者OnEnable阶段提前调用了Addressables.InitializeAsync甚至只是调用了Addressables.GetDownloadSizeAsync也会触发初始化。排查方法是全局搜索Addressables\.关键字把所有调用点挨个确认。如果在CachePathRedirector之前有任何其他脚本里的 Addressable 调用初始化就已经完成后续你再设置currentCacheForWriting地址缓存仍然可能存在旧路径或产生两个 Cache 记录并存的不稳定状态。从工程架构上讲最好把 SDK 初始化、共享界面、资源预加载这些操作放在一个由你掌控的入口之后。不要让多个 SDK 各自在Awake里启动否则不只缓存路径很多全局配置都可能碰上有理说不清的时序问题。5.3 一个容易引发新问题的清理操作为了清 C 盘很多人会直接调Caching.ClearCache()或Cache.ClearCache()然后发现下了很多次资源还是反复重新下载。原因是清除缓存把整个缓存索引和内容文件一次性删了下次启动又要从远端拉一轮这个行为在某些版本里可能不兼容 Addressable 的目录校验导致某一两个 Group 下载失败。如果你只想清某个旧版本的资源更稳妥的做法是把D:/AddressableCache/下面对应的公司名和产品名目录整个删除或者在 Caching 模块里找到对应 Cache 对象用cache.ClearCache()精确清理。清理旧缓存这个动作我建议写成一个独立工具按钮不要在生产代码里频繁自动调用。产品逻辑上缓存本身就该被调试工具管理而不是被业务逻辑动不动全清。6. 延伸思路按剩余空间自动选盘打造更省心的缓存体系6.1 启动时按空闲空间选择最优盘符固定到 D 盘解决了一个问题也引入了一个新问题D 盘如果也快满了怎么办。对着多块硬盘的用户来说更加适用的逻辑是启动时扫描所有固定磁盘自动选剩余空间最大的那一块。这样你既不需要打包前确定具体盘符也不会因为某块盘满了就卡住。using System.IO; using System.Linq; using UnityEngine; public static class DiskSpacePicker { public static string GetMostFreeLetter() { DriveInfo[] drives DriveInfo.GetDrives(); DriveInfo best drives .Where(d d.IsReady d.DriveType DriveType.Fixed) .OrderByDescending(d d.TotalFreeSpace) .FirstOrDefault(); return best ! null ? best.RootDirectory.FullName : C:/; } }把这个方法放进重定向脚本里当配置文件没指定根目录时自动选择剩余空间最大的盘。要注意过滤掉不可读的盘比如光驱、U 盘、网络映射盘统一用DriveType.Fixed限定。如果你希望外接移动硬盘也参与选择那就把条件放宽但移动硬盘读写不稳定我一般不推荐作为正式缓存盘顶多用来做临时资源断点测试。6.2 加入缓存空间配额超过线才开始清理有了动态选盘缓存膨胀仍然是长期战。Caching 本身有配额概念Cache 对象里有cache.spaceFree可以读取剩余配额。你可以在业务层做一个简单的“磁盘缓存卫士”当某个 Cache 的占用超过总空间阈值的 80%就触发一次最久未使用资源的清理。private static void EnforceCacheLimit(Cache cache, long maxBytes) { if (cache.spaceOccupied maxBytes) return; cache.ClearCache(); Debug.LogWarning([CachePath] cache exceeded limit, cleared.); }这段代码非常朴素——检查到超限直接清空当前缓存。适合原型项目不适合正式游戏因为正式游戏不能把所有远端资源都清掉否则玩家下次进游戏会有一段时间疯狂卡顿。更精细的做法是结合最新目录列表把最不常访问的那批文件删掉但那就需要自己做文件统计和引用关系管理了。Addressable 没有提供按文件级别驱逐的开箱即用 API这一步通常要根据项目运营阶段决定是否投入人力去封装。6.3 注意 WebGL 和小游戏环境的差异化处理WebGL 构建里Caching 的底层和原生平台完全不同。浏览器为了安全不允许游戏登录任意本机路径Unity 内部会把缓存模拟到 IndexedDB 里对游戏代码来说路径概念很弱。我见过有人把重定向脚本放进 WebGL 包里结果什么都没改变不是代码有 Bug而是平台限制。包括微信小游戏这类环境也是一样的逻辑。如果项目要发 WebGL 或小游戏缓存设计通常靠平台侧提供的本地存储接口不能直接套用这套 D 盘重定向方案。但重定向代码本身保留不会影响其他平台你只需要在宏里把!UNITY_EDITOR的条件额外追加 !UNITY_WEBGL就行免得无效代码在 WebGL 上白跑。7. 我的实操心得与最后的提醒缓存路径重定向看起来是几行代码的小事但它牵涉的启动顺序、构建平台、团队协作方式都挺容易被低估。我踩过的几个比较大的坑这里列出来给你留个底。第一个是别把重定向逻辑写进某个 UI 控制器的Awake里。UI 控件的生命周期受场景激活顺序影响很可能你的Awake还没执行别的地方已经拿到 Addressable 目录数据了。重定向必须放在RuntimeInitializeOnLoadMethod(BeforeSceneLoad)这种没有歧义的最早入口。第二个是验证一定要在真机上做而且要在没有安装旧缓存的全新机器上做。开发机上即使你改了脚本只要之前跑过旧包缓存的索引和文件都在表面上很难察觉路径有没有切成功。测试标准必须是“干净环境下首次启动后C 盘没有新增大量数据D 盘出现新目录”。第三个是别以为重定向完就可以一直不管了。C 盘不会再爆但 D 盘的缓存会随着年龄增长和资源更新不断膨胀。现在 Addressable 对旧版本资源的自动回收并不总是及时建议在运营维度定期清理缓存目录比如写成一个内置调试面板、或者服务器下发一个“清缓存”指令让特定客户端运行时执行一次精确清理。这样比依赖玩家手动去手机设置里清数据要靠谱得多。最后再分享一个我个人很喜欢的用法给重定向脚本增加一个-cachePath启动参数这样工作室里所有人拿到测试包时不用统一修改 C 盘或 D 盘而是每个人根据自己电脑的磁盘状况指定缓存根目录。打包机在 CI 里也可以多传一个参数把缓存打进构建分支对应的目录避免多分支并发把 C 盘写满。这套方案没有引入任何额外插件只依赖 Unity 原生 Caching API 和一个小的启动脚本接进现有项目的成本很低长期收益却很直接。希望它也能帮你把那个被磁盘爆满打断的开发节奏找回来。
返回列表