Unity 资源热更概念与分包地图:Catalog、Manifest、Bundle、Package、DLC 与打包粒度

Unity 开发高级/资深 热更新 资源管理
Unity 资源热更概念与分包地图:Catalog、Manifest、Bundle、Package、DLC 与打包粒度

资源热更这块最大的沟通成本不在实现,而在名词。同一个词在不同框架里指不同文件,同一个文件在不同框架里叫不同名字:Addressables 叫 Catalog,YooAsset 里没有 Catalog 这个类却有一份 BuildinCatalog;Manifest 既可能是框架的运行时清单,也可能是 Unity 构建过程产出的一个副产物;而 DLC 干脆不是技术名词,是发行名词。

这篇文章不按框架 API 组织,而是按名词组织:每个词先给一句话定义、再给它在文件系统里的落点、最后给"怎么验证它"。目标是下次看到别人文档里的一个词,能立刻判断它说的是哪一层,需要打开哪个文件。

后半篇(分包规则 起)换一个视角:按机制组织——打包规则怎么决定粒度、粒度怎么影响请求 / 内存 / 热更、压缩怎么选、运行时怎么验证、优化怎么用 ROI 排序。每个结论都落到一个可执行的验收指标上。

目录

章节 内容
先分清三套版本号 App 版本、资源版本、代码版本,混在一起就永远对不上
Package:资源包 它不是安装包;划分边界决定了后面所有能力的上限
Catalog:清单目录 三种语境下同名不同物,以及它的两种粒度
Manifest:清单字段逐项 AssetList / BundleList 每个字段的用途与坑
Bundle:物理容器 BundleName 与 FileName 的区别,依赖是双向的
DLC:可下载内容 为什么它不是技术概念,以及工程上怎么落地
PlayMode 与资源的三层位置 内置 / 沙盒 / 远端,以及各自的清理语义
代码热更的名词 热更 DLL 与 AOT 补充元数据,两个 DLL 分工完全不同
速查词表 中英对照总表,可直接当交接文档用
分包规则 收集器、打包规则、共享包:三层机制与常见配置错误
粒度与请求数 “请求"是什么、两个极端的代价、共载聚类
压缩格式 Uncompressed / LZMA / LZ4,以及双重压缩为什么无效
加载与内存 加载 Bundle ≠ 资源进内存,卸载与引用计数
热更代价 为什么是整包替换、改一张图要重下多少
运行时观测 四种手段:调试器、Profiler、内存快照、API
报告驱动与 ROI 指标分诊、ROI 排序、验收对照数
怎么查:实操路径 定位资源在哪个包、确认运行时用的哪个版本
常见误解 一整张"以为是 A 其实是 B"的对照表
落地清单 接入新框架时要确认的问题

先分清三套版本号

所有资源热更的排障,第一步都是"你到底在比哪个版本号”。这三套版本号的生命周期、比较方、更新代价都不一样。

版本 典型字段 / 载体 谁负责比较 更新手段 需要重新出包吗
App 版本 原生 versionName / versionCode、强更配置 启动时服务端下发 应用商店覆盖安装 是
资源版本 PackageVersion、res_version.txt 里的整数 客户端与远端清单比对 资源热更 否
代码版本 热更程序集文件(如 hlwd.game.bytes) 随资源版本一起走 资源热更 否

三条必须记住的结论:

  1. 代码热更不是独立通道。 热更 DLL 就是被打进 Bundle 的一个资源,它走的是和贴图、预制体完全相同的版本比对和下载流程。所以"代码版本"实际是"资源版本"的一个子集,不要把它当第四套版本号去设计流程。
  2. 资源版本和 App 版本不是包含关系,是两条平行线。 一个 App 版本可以对应任意多个资源版本;但资源版本不能向下兼容到比它更老的 App 版本,除非你主动做了降级逻辑。
  3. 比较动作只有"相等 / 不等",不要设计成大小比较。 资源版本用来寻址 CDN 目录(换版本换目录),不是用来判断"用户是不是落后了"。想做灰度、回滚,就换一份清单指向,而不是去比数字大小。

为什么先讲这个:后面所有名词——Package、Catalog、Manifest、Bundle——都存在"每个版本一份"的情况。分不清版本号,就会拿 A 版本的清单去解释 B 版本的报错。

Package:资源包

一句话定义:一组资源的集合,拥有独立的清单、独立的版本号、独立的下载器与独立的缓存目录。

最容易搞混的一点:Package 不是安装包。 安装包叫 APK / AAB / IPA,项目里通常叫"包体";Package 是工程内部对资源的逻辑切分,一个安装包里可以有多个 Package。

一个 Package 到底"独立"了什么

维度 是否独立 说明
清单文件 ✅ 每个 Package 一套,互不影响
版本号 ✅ 可以单独涨版本,其他包不动
下载任务 ✅ 可以有各自的并发与超时策略
缓存目录 ✅ 可以单独清理、单独校验
引用计数 ✅ 跨包引用需要显式依赖声明,不会自动共享
CDN 目录 ✅ 通常按 v{版本}/{包名}/ 分层

划分边界的常见口径

切分维度 适用场景 代价
主资源 / 非主资源 控制首包体积 首启需要一次完整清单比对
语言 多语言发行,按系统语言只下次需要的 文案常被其他资源引用,容易跨包
活动 / 赛季 活动内容独立上下线、独立回滚 活动数量多时清单数量爆炸
DLC / 付费内容 按需购买后下载 需要处理授权与卸载
清晰度 / 画质档 高端机下高清资源 同一资源存在多份,跨档不能复用
平台 桌面与移动格式不通用(如 BC 与 ASTC) 这是硬约束,基本必须分

划分边界是架构决策,不是"顺手分个组"。 一旦按某个维度切了 Package,跨越这个边界的资源引用就会变成"跨包依赖"——它会带来加载时序、卸载粒度、清单复杂度三方面的额外成本。新项目建议先只切"必须切的"(平台、首包/非首包),把 DLC 这类业务维度留到有明确需求时再加。

Catalog:清单目录

这是本文最容易读错的一个词,因为三种语境下都叫 Catalog,但不是同一个文件。

语境 名字 实际文件 回答什么问题
Addressables Content Catalog catalog.json、catalog.hash、catalog_{时间戳}.json 有哪些资源、地址是什么、在哪个 bundle、依赖谁
YooAsset 没有 Catalog 类,由两个文件分担 BuildinCatalog.json、{Package}_{版本}.json 内置带了哪些 bundle;资源到 bundle 的映射
Unity 原生 AssetBundleManifest OutputCache.manifest(构建副产物) bundle 之间的依赖关系

通用语义:catalog 是"目录",而且是两级的

抛开框架,catalog 这个词的本义是目录。而在资源框架里它有两个粒度,两者常被混为一谈:

  1. 清单的清单(对应 BuildinCatalog):包里内置了哪些 Bundle、当前有哪些清单版本可用。它是"找清单"的入口。
  2. 资源的目录(对应 Manifest / Addressables Catalog 的主体):某个资源地址 → 它属于哪个 Bundle → 它依赖哪些 Bundle。它是"找资源"的入口。

所以当有人说"catalog 不对",你要追问的是:是入口清单不对,还是资源映射不对? 这两个问题的排查路径完全不同。

一份真实的内置清单结构

{
  "FileVersion": "1.0.0",
  "PackageName": "DefaultPackage",
  "PackageVersion": "12",
  "Wrappers": [
    {
      "BundleGUID": "531607a7cd2af3b8a31565ec3f2d374c",
      "FileName": "defaultpackage_assets_gamemain_scenes_531607a7cd2af3b8a31565ec3f2d374c.bundle"
    }
  ]
}

读这份文件的正确姿势是倒着读:先看 PackageVersion(当前内置到第几个版本),再看 Wrappers 里每个 FileName 是否真的存在于磁盘上。出现"清单说有这么个包,但磁盘上没有"的时候,问题就锁定在内置产物不完整,而不是下载逻辑。

hash 文件的作用

清单旁边通常有一个同名 .hash 文件(或清单里带 Hash 字段)。它的唯一用途是:让客户端在不下载整个清单的情况下,判断清单有没有变。

  • 只下 hash(极小的文件)→ 和本地比对 → 相同就跳过清单下载 → 直接进下载阶段。
  • 不同才下完整清单。

这就是"每次启动都要检查更新,但流量消耗很小"的实现基础。反过来说,如果 hash 的生成逻辑和清单内容不匹配(例如清单被重新序列化但 hash 没重算),会出现永远认为需要更新的死循环。

必须记住的坑:BundleID 是数组下标

清单里代表 Bundle 的字段在很多框架里是整数 ID,它不是稳定标识符,而是同一份清单内 BundleList 数组的下标。

# 给定一个 bundleID,反查是哪个 bundle
$manifest = Get-Content .\DefaultPackage_12.json -Raw | ConvertFrom-Json
$bundleId = 15
$bundle   = $manifest.BundleList[$bundleId]
$bundle.BundleName

由此推出两条结论:

  • 这个 ID 一旦清单重建就会变,不能持久化、不能跨版本比较、不能写死在配置表里。
  • 排查时要拿同一份清单内部的 ID 去查,不要拿 A 版本的报错 ID 去 B 版本清单里找,那样必然对不上。
  • 反查"这个资源依赖了谁"的正确方式是读 DependBundleIDs(一组下标),再逐个映射回 BundleList。人眼读 JSON 很痛苦,优先用构建报告(见下节)。

Manifest:清单字段逐项

Manifest 是"资源目录"的本体。不同框架字段名不同,但结构高度一致:一张资源表 + 一张容器表,用 ID 互相引用。

以 YooAsset 的 PackageManifest 为例:

AssetList(资源表)

字段 含义 注意事项
Address 逻辑地址,业务层加载时用 默认用资源路径;配了可寻址规则后可能变成短名
AssetPath 工程内路径 与 Address 同时存在时,需明确哪个是"对外契约"
BundleID 所属 Bundle 是下标,见上一节
DependBundleIDs 依赖的 Bundle 集合 一组下标;共享资源会在这里指向 share 包
Tags 标签 用于按标签批量下载、预加载

BundleList(容器表)

字段 含义 注意事项
BundleName 逻辑名 相对稳定,会体现打包目录结构
FileName 物理名 BundleName_HashName.bundle,含内容 Hash
FileHash 内容 Hash 判断"这个包的内容变没变"的唯一依据
FileCRC 校验值 下载完整性校验
FileSize 字节数 用于下载进度与体积预算
DependBundleIDs 依赖的其他 Bundle 加载一个包会先递归加载它

读法建议:别读 JSON,读构建报告

原始 JSON 适合程序读,不适合人读。构建过程通常会额外产出一份可读报告,包含四个部分:

报告区块 内容 什么时候看
Summary 整体统计:资源数、包数、总大小 发现包体异常膨胀时
AssetInfos 每个资源:路径、所属包、依赖包 查"这个资源被谁引用"
BundleInfos 每个包:逻辑名、物理名、依赖包、被谁依赖、包含哪些资源 查共享包、查环形依赖
IndependAssets 没有任何包引用、也没被引用的独立资源 查无用资源、查漏加入收集器的资源

其中 BundleInfos 里的 ReferenceBundles(被谁依赖)是最有价值的一列。原始 JSON 只存了"我依赖谁"(正向),要回答"谁依赖我"必须遍历全表构建反向索引——报告已经帮你算好了。

判断一个共享包是否健康,就看它的 ReferenceBundles 数量:只有 1 个引用者的"共享包",说明拆包规则切错了,它白白多了一次 IO 和一份清单开销。

Bundle:物理容器

Bundle 是磁盘上真实存在的文件,前面所有概念最终都要落到它身上。

两个名字,别混用

名字 形态 稳定性 用途
BundleName defaultpackage_assets_gamemain_materials 相对稳定 日志、依赖分析、人工排查
FileName defaultpackage_assets_gamemain_materials_abc123....bundle 内容一变就变 磁盘文件名、CDN 路径、缓存 key

为什么要带 Hash 后缀:因为要支持"同一路径下新旧内容共存"。如果文件名固定,CDN 缓存会给你旧文件;CM 部署新版本时也无法灰度。带 Hash 后,abc123 和 def456 是两个不同 URL,缓存互不干扰,代价是旧文件需要清理策略。

依赖是双向的

清单里存的是正向依赖(我依赖谁),但排查时经常需要反向信息(谁依赖我):

  • 共享资源的判定:一个材质被 10 个预制体引用 → 抽成 share 包 → 这 10 个包都会在 DependBundleIDs 里指向它。
  • 卸载安全:卸载 A 包前要确认没有其他已加载包依赖它,否则会出现"资源凭空消失"。
  • 环形依赖:A 依赖 B、B 又依赖 A,加载时死锁。这类问题在正向清单里很难看出来,看报告的反向列表反而一目了然。

命名规则会泄漏架构

BundleName 通常由"打包目录 + 资源类型"拼出来,例如:

defaultpackage_share_assets_gamemain_materials
^^^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^^
   包名前缀    标记   目录路径      资源类型

所以看一个项目的 BundleName 分布,基本就能推断出它的打包策略:出现大量 share 前缀说明启用了共享包规则;目录层级很深说明按目录打包;同名资源出现在多个包名里说明收集器配置有重叠。

DLC:可下载内容

DLC(Downloadable Content)是发行与商业概念,不是技术概念。

这句话是理解 DLC 的关键。在资源框架里你找不到任何叫 DLC 的 API——因为 DLC 描述的是"这个东西在产品上怎么卖、怎么交付",而技术侧只提供"按需下载一组资源"这个能力。把 DLC 当技术名词去找,就会一直在文档里找不到答案。

DLC 与相邻概念的边界

概念 属于哪个维度 回答的问题 典型关注点
DLC 发行 / 商业 主包之外,用户按需购买或获取的内容单元是什么 定价、授权、上下架、评级
热更(Patch) 交付机制 已发布的内容怎么替换成新版本 版本比对、差异下载、回滚
分包(Split) 包体结构 首包放什么、其余按什么边界拆出去 首包体积、下载时机
Package 技术实现单元 实现上述目标时,资源按什么边界分组成独立清单 清单数量、跨包依赖

一句话概括四者关系:DLC 是一类需求,分包是实现它的包体结构策略,Package 是落地时的技术单元,热更是让它们能被持续更新的机制。

技术侧怎么落地一个 DLC

既然没有 API,落地方式就是组合既有能力,通常需要同时满足下面几条:

  1. 独立 Package:DLC 内容单独一个 Package(或一组),独立清单、独立版本号——这样才能"用户没买就不下载"。
  2. 独立下载入口:不进入默认启动下载流程,只在用户触发时(购买成功、点了某个入口)才开始下载。
  3. 两道门:可达 + 授权。技术上"资源下载完了"和"用户有权访问"是两件独立的事。很多线上事故是资源已经能加载,但授权状态没同步,于是未购买用户通过其他入口意外加载到了内容。不要用"资源没下载"来当权限校验。
  4. 卸载与回收:DLC 是第一个真正需要"用户主动卸载"的场景,要能清理清单与缓存,且不影响主包。
  5. 回滚与下架:DLC 下架时,已经买过的用户要能继续用(点对点服务),没买过的不能再看到入口。这要求版本清单支持"指向旧清单"。
  6. 依赖交叉:DLC 与主包互相引用时,谁先加载?主包更新后 DLC 的清单是否过期?这是 DLC 方案里最容易出问题的一环。

平台差异(同一套技术,不同的约束)

平台 主要约束 对 DLC 的影响
Android(AAB) 有分片机制,但资源分片与资源配置是两套东西 原生分片不能替代资源 DLC 方案
iOS 审核对"下载后可改变功能"的内容敏感 内容形态与文案要提前对齐审核口径
PC / 主机商店 有正式的 DLC SKU 与授权体系(Entitlement) 商业侧的 DLC 与技术侧的资源包要建立映射表
微信小游戏等 有明确的分包数量与体积上限 “DLC” 实际是分包能力的重新组合,不能照搬移动端结论

结论:DLC 方案 = 发行策略 + 包体结构 + 资源框架能力三者的组合。 三方任何一方没对齐,都会出现"技术做完了但商业侧不理解"或"商业卖完了但技术下不下去"的情况。

PlayMode 与资源的三层位置

同一份资源在运行时可能来自三个地方,PlayMode 决定它们的优先级和组合方式。

模式 资源来源 典型用途 关键差异
Editor 模拟模式 直接读 AssetDatabase 日常开发 不走 Bundle,加载行为与真机不同
离线模式 只读内置(StreamingAssets) 单机、无网、首包自包含产品 完全不请求远端,也无法热更
联机模式(HostPlay) 内置 + 沙盒缓存 + 远端 绝大多数热更产品 三层按优先级查找,沙盒覆盖内置
Web 模式 远端 HTTP 直读 小游戏、WebGL 无本地缓存概念,依赖浏览器缓存

三层位置与清理语义

位置 物理路径 内容 谁会清它
内置 StreamingAssets 下 随包发布的清单与 Bundle 只能靠覆盖安装更新
沙盒 persistentDataPath 下 热更下载下来的清单与 Bundle、版本记录 应用自身策略;覆盖安装/重装会清
远端 CDN,通常 v{版本}/{包名}/ 权威清单与 Bundle 由 CM 部署流程控制

这里面有两个必须理解的行为:

  1. 沙盒的生效方式是把内置清单"复制"过来再更新。 首启时如果沙盒为空,框架会把内置清单复制进沙盒,然后在沙盒那份上继续做增量更新。这解释了为什么首启离线也能跑——它读的是内置产物,而不是远端。
  2. 覆盖安装会清沙盒。 用户覆盖安装新 App 版本后,沙盒里的清单与 Bundle 可能被清掉,于是回到"用内置清单 + 重新下载"。如果新 App 版本内置的是老清单,用户就会先回退到老版本资源,再更新上去——这段窗口期是线上 bug 的高发期,尤其是"覆盖安装后必须重启一次才正常"这类问题。

版本记录文件

热更流程通常会在沙盒里留一个极小的版本文件(例如一行整数),记录"上次成功更新到哪个资源版本"。

  • 它的作用是下次启动的目标基线,避免每次都从头比对清单。
  • 它必须在下载全部成功之后才写入。写早了会出现"版本号是新的,资源还是旧的"这种最难查的状态:清单说资源都在,实际加载的是旧内容。
  • 它和远端不一致时,不要直接报错退出,而是重新走一次完整比对,这是最常见的自愈路径。

代码热更的名词

代码热更会额外引入几个词,其中最容易被混为一谈的是:热更 DLL 和 AOT 补充元数据。两者都是 .bytes 文件、都放在资源里、都由同一份清单管理,但职责完全不同。

名词 是什么 数量 何时需要更新
热更程序集 被编译成热更 DLL 的业务代码(Hotfix 层) 通常 1 个 业务逻辑改动时
AOT 补充元数据 AOT 程序集在热更代码里被泛型实例化时需要的元数据 一组 AOT 层改动、或热更代码新用了 AOT 泛型时
link.xml 告诉裁剪器"这些类型/方法不要裁掉" 1 个 反射、序列化、泛型被裁剪导致运行时才报错时
AOT 泛型引用收集产物 构建时扫出来的"热更代码实际用到的 AOT 泛型组合" 1 个 与 AOT 补充元数据配套

关键推论:

  • 只要热更代码新用到一个 AOT 泛型组合,就必须更新补充元数据。 这是"代码热更上去后,只有某个功能崩溃,报 ExecutionEngineException / Attempting to call method ... for which no ahead of time code was provided“这类问题的根因。
  • AOT 层改动无法热更。 它只存在于 App 版本里。所以架构上要把"AOT 尽可能薄"当作硬约束——AOT 越厚,需要强更的改动就越多。
  • 加载顺序必须是先补元数据、后加载热更程序集。顺序反了会在加载阶段就失败。

速查词表

名词 英文 / 常见写法 一句话定义 常见落点
资源包 Package 一组资源的集合,独立清单 + 独立版本号 框架名称空间下的 Package 对象
清单目录 Catalog “目录"的总称;两级粒度见正文 catalog.json / BuildinCatalog.json
内置清单 BuildinCatalog 包里内置的 Bundle 与清单版本索引 StreamingAssets 下
资源清单 Manifest / PackageManifest 资源地址到 Bundle 的映射表 {Package}_{版本}.json
摘要文件 .hash 用来快速判断清单是否变化 清单同目录
资源 Asset 最小的加载单位 工程内资源路径
地址 Address / Location 业务层加载时用的逻辑名 清单的 Address 字段
容器 Bundle / AssetBundle 磁盘上的物理包文件 *.bundle
逻辑名 BundleName 相对稳定的包名 清单的 BundleName 字段
物理名 FileName 带内容 Hash 的真实文件名 CDN 路径、本地缓存
共享包 Share Bundle 被多个包共同依赖的包 BundleName 含 share 标识
正向依赖 DependBundles 我依赖谁 清单的 DependBundleIDs
反向依赖 ReferenceBundles 谁依赖我(需构建反向索引) 构建报告的 BundleInfos
可下载内容 DLC 主包之外按需获取的内容单元(发行概念) 无 API,用独立 Package 落地
热更 Patch / HotUpdate 把已发布内容替换为新版本的机制 版本比对 + 下载
分包 Split 首包与其余内容的包体划分策略 收集器与打包规则
首包 Buildin / Base 随安装包一起发出的内容 StreamingAssets
沙盒 persistentDataPath / Sandbox 热更内容的本地落地目录 可写目录
播发模式 PlayMode 决定资源来源组合的运行模式 框架初始化参数
内容校验 CRC / Hash 下载完整性与内容变更判定 清单字段
构建报告 Report 人可读的构建产物说明 *.report
强更 Force Update 必须覆盖安装才能继续使用 App 版本号比对
授权 Entitlement 用户是否有权访问某内容 服务端 / 商店侧

分包规则:三层机制

一句话定义:分包规则不是一条规则,而是三层机制的叠加——收集器决定"哪些资源进哪个组”,打包规则决定"组内资源怎么变成 bundle 文件”,共享包规则与依赖闭包决定"bundle 之间怎么互相引用"。

理解这三层,才能解释"为什么我只是改了一个目录名,产物结构全变了"。

第一层:收集器(Collector)

收集器回答"这个目录下的资源,归谁管"。它以目录为单位配置,一个收集器绑定一段工程路径。

字段 作用 典型取值
收集路径 扫哪个目录 Assets/GameMain/...
收集类型 主资源 / 静态资源 / 依赖资源 Main / Static / Depend
地址规则 业务加载时用什么字符串 按文件名 / 按工程路径 / 按 GUID / 按组+文件名
打包规则 组内怎么切 bundle 见第二层
筛选规则 目录里只收哪类资源 全部 / 场景 / 预制体 / 精灵
忽略规则 哪些文件跳过 常规忽略(.、~ 开头)/ 关闭忽略
标签 用于按标签批量下载 业务自定义(如 preload)

收集类型是这里最容易用错的一格:它表达的是"这个资源是内容本身,还是别人的依赖"。分错会导致共享资源被重复打进多个包——这类问题在报告里表现为"同一资源出现在多个包中"。

第二层:打包规则(PackRule)

打包规则决定"组内的资源怎么切成文件"。这是粒度控制的唯一旋钮,掌握下面这几档就够用:

打包规则 切法 粒度 适用
按目录打包 一个目录一个 bundle,含子目录 中 默认;目录结构恰好约等于生命周期时用
按顶层目录打包 只按最上层目录切,子目录合并 粗 目录层级过深、想减少包数
按文件单独打包 一个资源一个 bundle 最细 需要独立热更 / 独立卸载的大资源
按收集器打包 一个收集器一个 bundle 粗 整块内容要整体上下线
按组打包 一个收集器组一个 bundle 最粗 极小项目或强制整包
按原始文件打包 原始文件各自成包、不改名 最细 视频、下载器管理的二进制
按着色器打包 Shader 集中到一个包 特例 配合自动收集 Shader

关键认知:打包规则的粒度 = 热更粒度 = 卸载粒度。 一个 bundle 无法只更新其中一部分,也无法只卸载其中一部分。

第三层:共享包与依赖闭包

前两层只决定"哪些资源在一起",第三层决定"它们怎么互相引用"。

  • 共享包规则:当一个资源被多个收集器 / 多个包引用时,把它单独抽成一个 share 包,其余包以"依赖"的方式指向它。开启后产物里会出现 ..._share_assets_... 这类包名;关闭则共享资源会被并进第一个引用它的包里。
  • 依赖闭包:加载一个包时,框架会递归加载它依赖的所有包。所以"加载一个资源的实际代价" = 该资源所在包 + 其依赖闭包的总体积,而不是那个包本身的大小。

这两条一起解释了报告里最有价值的一列——反向依赖(被谁依赖):它是判断共享是否合理的唯一依据。

从产物反推规则

BundleName 是规则的"打印输出"。看名字基本能还原配置:

defaultpackage_share_assets_gamemain_fonts
^^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^^
   包名        共享  目录    资源类型
  • 大量 share 前缀 → 开了共享包规则。
  • 名字里目录层级很深 → 用的是按目录打包。
  • 同名资源出现在多个包名里 → 收集器路径重叠或收集类型分错。

常见配置错误

症状 根因 修法
同一资源被打进多个包 收集类型分错(该 Depend 的填了 Main),或收集器路径重叠 修正收集类型、去重路径
只有一个引用者的 share 包 打包规则切太细,把独占资源也拆了 合并回引用方
包多得离谱 用"按文件单独打包"扫了一个大目录 换成按目录 / 按顶层目录
改一处资产,大量包变 hash 共享包把无关资源捆在一起 按生命周期重切共享包

粒度:从一个包到每资源一包

一句话定义:粒度就是"一次打包产出的 bundle 有多粗"。它是所有资源问题的总开关——请求数、峰值内存、卸载能力、热更体积,四个指标全部由它决定,而且方向互相冲突。

先明确"请求(Request)“是什么

一次请求 = 加载一个 bundle 文件 = 一次文件 IO(本地)或一次 HTTP GET(远端)。

它和资源数是两个维度:

概念 单位 一个真实项目的量级 决定因素
资源数 个 6852 工程内容量
包数 个 621 打包规则
首次加载请求数 次 ≤ 621 包数(已加载的包不重复请求)

加载 100 个资源,如果它们都在同一个包里,只产生 1 次请求;如果分在 100 个包里,就是 100 次请求。 请求的代价不在"传输”(本地 IO 几乎免费),而在每次请求的固定开销:打开文件、读索引、建 bundle 对象、走一遍依赖解析。

远端场景下请求更贵,因为每个请求都带 HTTP 往返。所以要分清两类病:

指标 受什么影响 该用什么手段治
下载总量 包的实际字节数 减资源、换压缩格式、提高压缩率
请求数 包的个数 合并小包、减少碎片
首加载延迟 请求数 + 压缩格式 合并小包、LZ4
峰值内存 最大单包体积 + 压缩格式 拆分大包、LZ4

两个极端的完整代价

维度 全部一个包 每资源一个包
请求数 1 ≈ 资源数(上例约 6600+)
重复资源 无(天然无重复) 无(但依赖会被拆散)
固定开销 最低(只有一份 header / 索引 / TypeTree) 最高(每包都有一份)
卸载粒度 无(要么全在,要么全没) 每个资源独立
热更体积 全量(改一个字节重下整包) 最小(只重下改动的那一个)
构建耗时 最短 最长
压缩率 最高(大文件上下文长) 最低(小文件压不动)
加载延迟 首包最大,之后最省 每次都有请求开销
失败影响面 一个包坏了,全游戏不可用 单点故障
适用场景 一次性小内容、原型验证 大资源按需下载

两个极端都能用,但都只在特定场景下才是对的。绝大多数项目要的是中间态。

中间态:按"共载聚类"切

中间态的原则只有一句:让 bundle 的边界等于资源生命周期的边界。

判据是"这些资源会不会在同一时间被同一功能加载、又在同一时间一起不用了"。会,就放一个包;不会,就切开。

分层 特征 典型资源 建议粒度
常驻层 全程不卸载 主界面图集、公共 Shader、字体 合并成少数几个包
场景层 进出场景 / 功能时整体装卸 一个关卡、一个模块的资源 一个功能一个包或几个包
单体层 大且按需 高清立绘、BGM、视频、大图集 一个资源一个包

共载聚类为什么是"最优解":它同时压住四个指标——常驻层包数少(请求少)、单体层拆得开(卸载 / 热更粒度细)、同类资源同包(压缩率不塌)、没有跨生命周期捆绑(热更不会误伤)。

判断粒度是否切错的三个信号

  1. 一个包里的资源,加载时机各不相同 → 切粗了,应该拆。
  2. 加载一个资源时,顺带拉起了几 MB 的依赖闭包 → 共享包切错了,或该资源没独立。
  3. 一个包一年都不会单独热更,却因为别的资源变更被重下 → 切细了,应该合。

压缩:Uncompressed / LZMA / LZ4

一句话定义:压缩选项决定 bundle 在磁盘 / 网络上的体积,以及加载时要不要把整个包解到内存——它是"下载体积"和"峰值内存"之间的一个交换。

先分清两层数据

一个 bundle 里其实有两类数据,可达性不同:

层 内容 是否可随机访问
索引层 文件头、资源目录、TypeTree、块索引 是(映射后可直接读)
数据层 每个资源的实际内容 取决于压缩格式

只有数据层会被"压缩 / 解压"影响,索引层永远要能被随机读。

三种选项

选项 压缩机制 体积 加载行为 峰值内存 适用
不压缩 不压缩 最大 直接映射,懒读取 最低 需要极速启动、本地已无冗余的内容
LZMA 流式全量压缩 最小 整个包一次性解压到内存 最高 只下载不常驻的内容、包体极度紧张
LZ4(分块) 按块压缩(Unity 为 128KB / 块) 中等 分块按需解压 低 绝大多数默认场景

LZ4 的关键:分块 + 随机访问

LZ4 在 AB 里是按固定块压缩的。带来的直接结果是:

  • 文件可以被映射,只解压你要读的那几块;其它块在磁盘上原样待着。
  • 加载一个小资源,不会把整个包解到内存 → 峰值内存 ≈ 该资源大小,而不是包大小。
  • 代价是压缩率略低于 LZMA(块与块之间不能共享上下文)。

这也是为什么运行时默认选 LZ4:它在体积、加载速度、峰值内存三者之间给了最稳的组合。

LZMA 的陷阱

LZMA 压缩率最好,但它是流式的:要读包里任何一个资源,必须先把整个包解压出来。

  • 一个 50MB 的 LZMA 包,加载第一个资源就要先付出约 50MB 的峰值内存和解压时间。
  • 它适合"下完就写盘、之后用 LZ4 版本"这种离线流程,不适合运行时高频加载。

结论:LZMA 省的是流量,付的是内存和首加载延迟。

双重压缩:为什么"再压一次"没用

LZ4 / LZMA 只能压缩还有冗余的数据。而 AB 里的大头——贴图、音频——在导入时已经是压缩格式:

资源类型 导入后形态 再上 LZ4 / LZMA 的收益
贴图(ASTC / ETC2 / BC) 块压缩,已高度熵编码 极低(通常个位数百分比)
音频(Vorbis / MP3 / AAC) 有损压缩 极低
文本 / JSON / 代码 / Shader 原始文本 高
网格 / 动画(未压缩) 二进制 中高

所以"压缩选项"对包体的提升,主要体现在文本类资源上;指望靠 LZ4 / LZMA 把贴图包瘦一圈是不现实的。贴图包要瘦,只能靠减小分辨率、换压缩格式、拆分不共用的大图集。

与分包的联动

压缩率和粒度是反向的:包切得越细,每个文件越小,LZ4 能利用的上下文越短,压缩率越低。

  • 从"一个大包"拆成"一百个小包",总字节数通常会变大(固定开销 + 压缩率下降)。
  • 所以拆包是在用"一点体积"换"热更与卸载的粒度",要算总账,不要只盯压缩率。

补充两点:一是更高压缩率的 LZ4 变体(LZ4HC)只提高压缩率、不改变解压速度,但构建更慢,且主流 AB 构建管线默认不暴露;实际项目里换压缩选项的决策点通常只有不压缩 / LZMA / LZ4 三个。二是如果给 bundle 加了加密,就无法再做映射式随机访问,LZ4 的按需解压优势会被抹掉——加密与"低峰值内存"往往要二选一。

加载与内存:加载 Bundle 不等于资源进内存

一句话定义:打开 bundle 只把它的索引映射进内存;只有真正加载过的资源,才占用内存。把 bundle 文件大小等同于内存占用,是资源预算里最常见的错误。

一次加载的五个阶段

阶段 做什么 内存动作 量级
1. 打开文件 从路径加载 bundle 映射文件、读 header KB 级
2. 建索引 解析资源目录、依赖表、TypeTree 常驻少量结构 KB ~ 低 MB
3. 取资源 加载目标资源对象 解压 + 反序列化 随资源大小
4. 上传 贴图 / 网格上传 GPU 显存 / GPU 内存 随资源大小
5. 卸载 释放 → 引用计数归零 → 强制卸载 释放资源 + 包索引 归零

关键结论:

  • 第 1、2 步的成本与包体积无关(除索引 / TypeTree 大小外),更与"包里有几个资源"无关。
  • 第 3 步才是有代价的那一步,而它只处理你实际用到的资源。
  • 所以:一个 100MB 的包,如果只加载了里面一个 1MB 的贴图,驻留内存约等于 1MB + 索引开销,不是 100MB。

LZ4 让"部分加载"成立

上面的结论只在 LZ4 / 不压缩时成立:

格式 打开文件之后 加载单个资源
不压缩 只映射索引 直接读,不解压
LZ4 只映射索引 只解压相关块
LZMA 整个包已解到内存 从内存里取

这是上一节"运行时默认选 LZ4"的另一个理由:只有 LZ4 能让"大包 + 小用量"不付出大内存。

卸载是"引用计数 + 强制清对象"

  • 每次加载资源都会增加引用计数;释放时减少计数。
  • 计数归零后,框架才真正卸载:YooAsset 的做法是调用 AssetBundle.Unload(true)。
  • Unload(true) 的语义是"卸载这个包,并销毁其中所有已加载的对象";相对地 Unload(false) 会保留对象但让它们失去所属包,容易造成泄漏与悬空引用,实践中基本不用。
  • 因此"资源为什么还在内存里"通常只有两个答案:引用计数没归零,或者有别的对象在引用它(用 Memory Profiler 查引用链)。

文件体积 ≠ 驻留内存

三个数字必须分开记:

数字 含义 怎么拿
磁盘 / 下载体积 bundle 文件大小 构建报告
峰值内存 加载瞬间的最高占用 Profiler 抓峰值
驻留内存 稳定状态下的常驻占用 Memory Profiler 快照

优化内存看后两个,优化流量看第一个。 一个包体积很大,可能驻留内存为零(没用到);一个包体积很小,也可能因为反复加载—卸载造成抖动。

热更代价:改一个资源要重下多少

一句话定义:热更是整个 bundle 文件的替换,不是"资源级增量"。改一个资源,代价是它所在 bundle 的完整体积。

为什么是整包替换

清单里 bundle 的物理名是 BundleName_HashName,Hash 由包内容算出:

defaultpackage_uispriteatlas_a1b2c3d4....bundle
                          ^^^^^^^^ 内容变 → 这里变 → 文件名变
  • 文件名含 Hash 是为了让 CDN 上"新旧内容共存"、可灰度、可回滚。
  • 但代价是:没有任何二进制差分。只要包内容变了,就是换一个全新的文件,客户端要下完整的新文件。

一个真实的反例

在真实项目里出现过一次典型的误判,值得单独记账:

项 数字
图集包 uispriteatlas 体积 18.24 MB,装了 53 张图集
打包规则 按目录打包(53 张图集同属一个目录 → 一个包)
只改其中 1 张图集 该包 Hash 变 → 重下 18.24 MB
误判 曾按"数据库平均包体积"估成约 2MB

教训:估热更量必须查目标资源所在包的实际大小,不能用平均值、更不能按资源本身大小估。资源 50KB,不代表热更 50KB。

拆包后的账

把图集包从"按目录打包"改成"按文件单独打包":

方案 包数 单包平均 改一张图集的重下量
现状(按目录) 1 18.24 MB 18.24 MB
拆开(按文件) 53 约 350 KB 约 0.1 ~ 2 MB(按该图集大小)
收益 — — 降低约 90%+

这就是"粒度 = 热更粒度"的直接体现:同样的资源,只是换了打包规则,单次热更量差一个数量级。

但不要为了热更无限拆细

拆细的隐性代价必须一起算:

代价 说明
体积变大 包多了,固定开销与压缩率损失累加
请求变多 每个包一次 IO,首加载更碎
清单变大 包数直接放大清单条目
构建变慢 包数影响构建与缓存命中

判据:只有当"这个资源的独立热更频率明显高于周围资源"时,拆开才划算。会一起改的放一起,不会一起改的分开——这是热更粒度与共载聚类之间唯一的冲突点,按发布节奏拍板。

运行时怎么观测 Bundle

静态报告(构建产物)能回答"包里有什么",但回答不了"运行时到底谁被加载了、谁在占内存、谁卸不掉"。这一节给四种手段。

手段一:框架自带的 Bundle 调试器(首选)

YooAsset 在调试构建下会自动挂载一个运行时调试组件(RemoteDebuggerInRuntime),把运行中包的数据回传给编辑器窗口 YooAsset/AssetBundle Debugger。它有三个页签:

页签 回答什么问题 关键字段
资源列表(Asset List) 这个资源被谁加载、加载多久 加载耗时、所属包、被哪些场景 / 面板引用
包列表(Bundle List) 这个包在不在内存、被谁引用 引用计数、状态(活跃 / 空闲)、被哪些包依赖
操作列表(Operation List) 正在进行的加载任务 任务状态、耗时

用法要点:

  • 窗口上有单次采样 / 自动采样:排查用单次,长期观察用自动。
  • 看"卸载不掉":在包列表里找引用计数 > 0 但业务上已关闭的包,再顺着"被谁依赖"往上找。
  • 看"内存莫名高":在资源列表按加载耗时 / 大小排序,找比预期大的资源。
  • 它只统计框架加载的资源,不覆盖 Resources 等直接加载的对象——不要拿它当唯一真相。

手段二:Unity Profiler

看引擎层的真实占用:

  • Memory 模块 → 详细视图 → Assets 分组,能看到 AssetBundle(容器本身)、SerializedFile(包索引)、Texture2D / Mesh 等具体对象。
  • 用来回答:“内存是花在容器上,还是花在资源对象上。” 容器大 = 包多 / 包大;对象大 = 资源本身大。

手段三:Memory Profiler 包

com.unity.memoryprofiler 适合查驻留与泄漏:

  • 抓快照后能看到进程里所有已加载的 AB、每个包内的对象,以及引用链(谁在引用导致卸不掉)。
  • Profiler 看"有多少",Memory Profiler 看"为什么还在"。

手段四:API 兜底(线上 / 自动化)

需要写日志或在无窗口环境排查时,用代码枚举:

foreach (var bundle in AssetBundle.GetAllLoadedAssetBundles())
{
    Debug.Log($"{bundle.name}");
}

配合框架自己的诊断开关,可以把"当前加载了哪些包"打成日志带到线上。

四种手段怎么选

手段 视角 强项 局限
Bundle 调试器 业务层 引用计数、依赖关系、加载耗时 只覆盖框架加载
Profiler 引擎层 容器 vs 对象的真实内存 引用关系弱
Memory Profiler 对象层 引用链、泄漏定位 需抓快照,较重
API / 日志 脚本层 可自动化、可线上 信息最少

推荐顺序:调试器定位"哪个包"→ Profiler 定位"哪类内存"→ Memory Profiler 定位"为什么没释放"。

报告驱动的优化与 ROI

一句话定义:优化不该靠"感觉包大",而该靠构建报告里的几个可量化指标分诊,再用 ROI 决定先做哪个。

先看指标:报告里哪几列值得盯

指标 健康信号 危险信号 说明
重复资源 0 > 0 同一资源进了多个包,白占体积
单包体积 分布平滑 存在超大包(如 > 5MB) 首加载与热更的痛点
包数 / 碎片 无大量极小包 大量 < 10KB 的包 请求数与固定开销浪费
反向依赖数 共享包被很多包依赖 共享包只有 1 个引用者 拆包规则切错的信号
依赖闭包体积 加载一个资源拉不动多少 加载一个小资源拉起几 MB 共享包粘得太死
环形依赖 无 存在 加载死锁风险

再分诊:问题出在哪一层

现象 大概率问题层 对应动作
总体积太大 内容本身 减资源、换压缩格式、去无用资源
单包特别大 打包规则太粗 按文件 / 按功能拆
包数特别多、单包特别小 打包规则太细 合并、按顶层目录
重复资源 > 0 收集类型 / 路径重叠 修正收集器配置
加载一个小资源很慢 共享包粘死 重切共享包、拆分闭包
一个包有一堆引用者却很小 正常(公共资源) 保持,但确认它常驻

排序:用 ROI 决定先做谁

ROI(投入产出比)= 收益 ÷ 投入。 资源优化里三项都要量化:

项 怎么量
收益 省下的字节数 / 请求数 / 峰值内存 / 单次热更量
投入 改动范围(配置 or 代码 or 架构)、验证成本
风险 是否影响加载时序、是否引入新依赖、是否要回归测试

把它套进候选优化项,大致能排出优先级:

优化项 收益 投入 风险 优先级
关闭 TypeTree 写入 每包固定开销下降,聚沙成塔 一个开关 低(需回归) 高
字体 ttf 裁剪子集 单个超大字库包显著下降 中(要定字符集) 低 高
图集按文件拆包 单次热更 18MB → < 2MB 配置 低 高
BGM 按需解绑 压低峰值内存与首加载 中(改加载时机) 中 中高
小包合并 请求数与固定开销下降 中(需核对共载) 中 中
环形依赖治理 消除死锁风险 高(改架构) 高 低(但要做)

排序原则:先做"一个开关换全局收益"的(TypeTree),再做"针对单个巨型包"的(字体、图集),最后才是需要动架构的(依赖治理)。

验收:优化要有对照数

每项优化都应在同一版本对比下给出:

  • 总包数、总体积 变化多少
  • 超大包 / 碎片包 数量变化
  • 单次典型热更量 变化
  • 首加载请求数、峰值内存 变化
  • 构建耗时 变化(不能让构建时间失控)

没有对照数的优化,不算完成。

怎么查:实操路径

名词认清了,接下来是"遇到问题怎么定位"。这一节给的是从现象到文件的路径。

想知道"某个资源在哪个包里"

  1. 打开构建报告,在 AssetInfos 里搜资源路径 → 拿到所属 BundleName。
  2. 在 BundleInfos 里查这个 BundleName → 看它依赖谁、被谁依赖、包含哪些资源。
  3. 只有拿不到报告时,才回到原始清单里做下标映射。

想知道"这个包为什么这么大"

  1. 看报告 Summary 的体积分布,先确定是"包多"还是"单包大"。
  2. 单包大 → 在 BundleInfos 里看它的 BundleContents 是否有意外的大资源(图集、音频、视频)。
  3. 包多 → 检查收集器规则是否有重叠;IndependAssets 是否有本该合并的资源。

想知道"运行时实际用的是哪个版本"

查什么 怎么查
当前资源版本 读沙盒里的版本记录文件;或框架的 GetPackageVersion()
清单到底加载的是哪一份 对比内置清单与沙盒清单的版本号
资源是从内置还是远端来的 看缓存命中日志;或临时禁用远端,观察是否还能加载
Bundle 的真实内容 用二进制工具列出 Bundle 内的资源名清单,与报告对照

一个通用原则

所有资源问题的证据链都必须落在"文件 + 版本"上。 只看日志会出现"日志说下载成功了,但加载的是旧资源"这种无法收敛的状态;必须同时确认哪一份清单、哪一个版本、哪个物理文件。这也是前面反复强调"分不清版本号就永远对不上"的原因。

常见误解

以为是 A 实际是 B 后果
Catalog 就是 Manifest Catalog 是"目录"的总称,Manifest 是其中"资源映射"这一级 排查时找错文件
Manifest 只有一个 每个 Package、每个版本各一份 拿错版本的清单去解释报错
BundleID 是稳定 ID 它是同一份清单内数组的下标 持久化后跨版本必然失效
DLC 是技术名词 它是发行概念,技术侧只有"按需下载一组资源" 在框架文档里找不到答案
DLC 下载完了就等于有权访问 可达性与授权是两道独立的门 权限绕过,未购买用户加载到内容
热更 DLL 和 AOT 补元数据是一回事 分工完全不同,后者只在特定条件下才需要更新 热更后局部功能崩溃
代码热更有独立通道 热更 DLL 本身就是资源,走同一条链路 设计出多余的第二套版本体系
Bundle 压缩能省内存 容器压缩省磁盘与下载,不省运行时内存 内存优化方向选错
覆盖安装后一切从头开始 沙盒被清 → 先回退到内置清单再更新 “覆盖安装后必须重启一次"类问题
版本号可以比大小 它用于寻址目录,只该做相等判断 灰度与回滚逻辑写歪
加载 Bundle 就等于资源进内存 只映射索引;被加载过的资源才进内存 内存预算严重高估
资源数 = 请求数 请求数由包数决定,已加载的包不重复请求 高估或低估 IO
包拆得越细越好 每包有固定开销,压缩率随包数下降 包数爆炸、加载反而更慢
热更会下"改动的资源” 实际是整个 bundle 文件替换(无差分) 热更量被低估数十倍
已压缩资源再压一次能大省 ASTC / 音频已高度压缩,再压收益极低 压缩收益误判
按目录打包就是最优粒度 粒度应由生命周期决定,目录只是巧合对齐 分包切错
共享包越多越好 只有 1 个引用者的共享包是纯浪费 多余 IO 与清单开销
热更量可以按平均包体积估 必须查目标资源所在包的真实大小 方案评估失真

落地清单

接入或接手一套资源热更框架时,把下面这些问题问清楚,基本就能对齐所有名词:

  • 这个项目的资源版本号存在哪个文件?谁负责写入?写入时机是在下载前还是下载后?
  • 一共划分了哪些 Package?切分维度是什么?有没有跨包引用?
  • Catalog 的入口文件是哪一个?它和资源映射清单分别叫什么名字?
  • 清单是否带 hash 摘要?更新检查是先比摘要还是直接下清单?
  • BundleID 是下标还是稳定标识?项目里有没有把它当稳定 ID 用错的地方?
  • 有没有可读的构建报告?ReferenceBundles 是否用于审查共享包?
  • 是否存在只有一个引用者的共享包?(拆包规则切错的信号)
  • DLC / 活动内容是用独立 Package 落的,还是混在主包里用标签控制?
  • DLC 的授权校验在哪里做?有没有"资源可达即视为有权"的隐患?
  • 三个资源位置(内置 / 沙盒 / 远端)的清理策略分别是什么?覆盖安装会怎样?
  • 热更 DLL 与 AOT 补充元数据是否都进了清单?更新条件是否明确?
  • 有没有回滚路径?回滚时是换清单还是换版本号?
  • 分包边界按什么维度切?(目录 / 生命周期 / 功能)它和"会不会一起加载、一起卸载"一致吗?
  • 是否存在没有任何独立热更 / 卸载需求却单独成包的资源,或跨生命周期被捆在一起的包?
  • 压缩选项是逐包有依据,还是全局一刀切?有没有对已压缩资源(贴图 / 音频)抱有不切实际的体积预期?
  • 是否开启了共享包规则?有没有只有 1 个引用者的共享包?
  • 是否关闭了 TypeTree 写入?关闭后有没有做过反射 / 反序列化的回归?
  • “加载一个资源"的依赖闭包体积上限是多少?有没有超标的包?
  • 单次典型热更量是多少?有没有超大 bundle 让它变成全量重下?
  • 是否存在环形依赖?
  • 有没有可用的运行时观测手段(Bundle 调试器 / Profiler / Memory Profiler / 日志)?
  • 每项优化的验收对照数(体积、请求数、峰值内存、热更量、构建耗时)是否齐备?

一句话总结

资源热更的名词可以归到四层上:Package 是边界,Catalog / Manifest 是地图,Bundle 是实物,DLC 是需求。

  • Catalog 和 Manifest 不是同义词:Catalog 是"目录”,Manifest 是目录里"资源映射"那一级;框架不同名字还不一样,先确认对方说的是哪一层再讨论。
  • BundleID 是下标不是 ID,所有"跨版本对不上"的问题多半源于此。
  • DLC 不是技术概念,技术侧只有"按需下载一组资源";它真正难的地方在授权、卸载、回滚、依赖交叉,而不是下载。
  • 三套版本号必须分开看,代码热更不是第四套通道,它本身就是资源。
  • 所有结论都要落到文件 + 版本 + 物理路径三件事上,只凭日志无法收敛。

机制层:粒度是一切的开关

在名词之下,真正决定体验的是粒度与压缩这两个旋钮。 记住这几条:

  • 粒度决定四个互相冲突的指标:请求数、峰值内存、卸载粒度、热更体积。最优解不是某个极端,而是共载聚类——让 bundle 边界等于生命周期边界。
  • “请求”= 一次 bundle 文件加载,数量由包数决定,不由资源数决定;“请求多"和"包大"要分开治。
  • 加载 bundle ≠ 资源进内存:只映射索引,被加载过的资源才驻留;引用计数归零的强制卸载才真正释放。
  • 压缩只在"体积 vs 峰值内存"之间选:运行时默认 LZ4(分块、可随机访问、低峰值);LZMA 省流量但抬峰值;已压缩资源再压几乎无效。
  • 热更是整包替换,不是资源级增量:估热更量必须查目标包的真实体积;拆包能把单次热更量降一个数量级,但要用请求数与压缩率还账。
  • 先量化再排序:报告指标分诊 → ROI 排序 → 每项优化给出对照数;没有对照数的优化不算完成。

相关阅读:Unity 开发高级/资深 03:资源管理与热更新、Unity 图片资源格式与内存、Unity 开发高级/资深 技能总览。

评论