外观
Stage05|Performance
Lesson060|Texture Memory:图片为什么最容易吃掉内存?
Tags: #Creator2.x #Stage05 #Texture #Memory #VRAMDifficulty: ⭐⭐⭐⭐☆
真实问题:头像只有 50 KB,列表为什么占了几百 MB
50 KB 是网络 JPEG/PNG 的压缩文件大小。若解码为 1024×1024 RGBA8888:
text
1024 × 1024 × 4
≈ 4 MiB几十个不同头像再叠加下载缓冲、CPU 解码像素、GPU Texture 和缓存,很快进入百 MB 数量级。
一、本课目标
图片经常是移动游戏内存的最大来源。本课解释:
- 文件大小为什么不等于运行时占用。
- CPU 内存、显存和压缩纹理的区别。
- 为什么 SpriteFrame 释放不一定马上降低内存。
- 如何设计图片规格和加载策略。
二、磁盘大小不等于运行时大小
一张 PNG 可能只有几百 KB,但解码到 RGBA 后大致需要:
text
宽 × 高 × 4 字节例如 2048×2048:
text
2048 × 2048 × 4 ≈ 16 MB这还没有计算 mipmap、缓存、临时解码缓冲和 GPU 副本。
三、图片可能同时存在多份
text
磁盘压缩文件
→ 解码缓冲
→ CPU 纹理对象
→ GPU 纹理
→ SpriteFrame / Atlas 引用平台和纹理格式不同,实际副本数也不同。不能只用资源管理器里的文件大小估算内存。
四、Texture、SpriteFrame 和 Atlas
text
Texture:底层像素图
SpriteFrame:Texture 的区域和显示信息
Atlas:多个图片合并后的大纹理及区域映射多个 SpriteFrame 可能共享同一个 Texture:
text
Atlas Texture
├── iconA SpriteFrame
├── iconB SpriteFrame
└── iconC SpriteFrame释放一个 SpriteFrame 不一定释放共享纹理。
五、纹理规格的选择
需要综合考虑:
text
显示尺寸
屏幕分辨率
透明通道
压缩格式支持
采样质量
GPU 解码和带宽把所有图片都导出成最大尺寸,会增加内存和上传成本;过度压缩则可能造成颜色、边缘或清晰度问题。
六、加载策略和内存峰值
一次性加载整个商城的所有图片会造成峰值:
text
旧页面资源仍在
新页面资源开始解码
临时缓冲同时存在
纹理上传还未完成更稳妥的方式可能是:
- 分页加载。
- 只加载可见 Item。
- 预加载下一页而不是全部页面。
- 场景切换前后分阶段释放。
- 控制同时进行的解码数量。
七、为什么释放后内存不立即下降
可能原因:
- 仍有 SpriteFrame 或组件引用。
- 纹理被其他页面共享。
- Loader 或 Bundle 缓存仍保留。
- Native/GPU 延迟释放。
- 内存分配器保留空闲块。
- 工具显示的是进程保留量而非可用量。
要区分“对象仍被引用”和“分配器暂未归还系统”。
八、案例:头像列表
100 个头像各为 512×512 RGBA:
text
单张约 1 MB 解码内存
100 张约 100 MB如果实际头像显示只有 64×64,直接使用原图会浪费大量内存。可以考虑:
- 服务端提供缩略图。
- 客户端按显示尺寸分级。
- 分页和对象池只保留可见区域。
- 使用合适的 Atlas 或压缩格式。
九、常见误区
误区一:PNG 文件 500 KB,所以内存只占 500 KB
运行时通常需要解码后的像素数据。
误区二:释放 SpriteFrame 就一定释放 Texture
多个 SpriteFrame 和对象可能共享 Texture。
误区三:纹理越小越好
过小会导致模糊、采样问题和频繁缩放,应该按实际显示需求选择。
误区四:内存工具中的进程值下降才算释放成功
平台分配器可能保留内存,应结合对象引用、纹理统计和趋势判断。
十、练习与答案
- 2048×2048 RGBA 图片解码后大约需要多少内存?
- 为什么多个 SpriteFrame 可能只对应一个 Texture?
- 场景切换后纹理不降,应该先查哪些地方?
- 头像显示尺寸很小却使用大图,会带来什么问题?
答案:
- 约 16 MB,不含其他缓存和副本。
- SpriteFrame 只是同一纹理中的区域描述。
- 外部引用、缓存、Bundle、共享纹理、GPU 延迟释放。
- 浪费解码内存、显存、带宽和上传时间。
一张图片的内存账本
text
网络响应/磁盘文件
CPU 压缩字节缓存
CPU 解码像素
引擎 Texture 对象
GPU 纹理
SpriteFrame/Atlas 元数据
临时缩放或转换缓冲这些形态是否同时存在、何时释放,取决于平台和加载实现。不能把 Texture Memory 数字简单等价为 JS 堆。
Creator 2.4.x 实验:同尺寸不同文件大小
准备
导入两张 1024×1024:
- 大面积纯色 PNG;
- 噪声 PNG。
它们磁盘压缩大小可能差很多,但解码到相同 RGBA 格式时基础像素量相近。
TextureMemoryProbe.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class TextureMemoryProbe extends cc.Component {
@property(cc.Sprite)
target: cc.Sprite = null;
private owned: cc.SpriteFrame[] = [];
load(path: string) {
cc.resources.load(
path,
cc.SpriteFrame,
(err, frame: cc.SpriteFrame) => {
if (err) {
cc.error(err);
return;
}
frame.addRef();
this.owned.push(frame);
this.target.spriteFrame = frame;
const texture = frame.getTexture();
const estimate =
texture.width * texture.height * 4;
cc.log(
'[texture-memory]',
path,
texture.width,
texture.height,
'rgba estimate=', estimate
);
}
);
}
releaseOwned() {
this.target.spriteFrame = null;
for (const frame of this.owned) {
frame.decRef();
}
this.owned.length = 0;
}
onDestroy() {
this.releaseOwned();
}
}估算假定 RGBA8888 且不含 mipmap/副本,只用于数量级。实际以导入格式和平台工具为准。
实验步骤
- 冷加载第一张,记录文件大小、回调和纹理指标。
- 加载第二张,比较估算。
- 重复加载同一路径,验证缓存身份。
- 清空 Sprite 但不 release owned,观察所有权仍存在。
- releaseOwned 后等待合理时间并多轮测试。
头像列表的正确缩放边界
如果 UI 只显示 96×96,没必要长期保留 1024×1024:
text
服务端提供缩略图
或下载后生成受控尺寸缓存
→ 使用稳定 URL/缓存键
→ 限制同时活跃 Texture
→ Item 回收清除旧 Frame
→ LRU/预算淘汰客户端仅把 Sprite 节点 scale 变小不会减少底层 Texture 像素。
真实项目故障:无限滚动头像导致阶梯增长
根因
text
每个 URL 带随机防缓存参数
→ 同一用户头像每次成为新缓存键
→ 列表 Item 回收但全局 Map 永久保存 Frame
→ Texture 无法淘汰修复
- 用内容版本而非随机参数;
- 缩略图尺寸进入 URL/缓存键;
- 缓存按总像素/字节预算而非仅数量;
- 淘汰时清除业务使用并归还所有权;
- 记录命中率、下载量、纹理峰值和重载次数;
- 连续滚动固定数据集验证稳定平台。
Memory 诊断
JS 堆下降,进程内存不降
纹理可能在原生/GPU,分配器也可能保留内存。分别看 JS、Native、Texture/GPU 指标。
释放后重新打开变慢
说明缓存命中减少。释放策略要平衡内存预算与重载成本,不是越彻底越好。
Atlas 只用一个图仍占很大
底层以整张 Atlas Texture 为单位,按生命周期重新分组。
版本边界与练习答案
纹理格式、CPU 副本和 GPU 统计强依赖 Creator 2.4.x 小版本、Web/Native 和 GPU。估算必须与真机工具校准。
- 节点 scale=0.1 会降低 Texture Memory 吗? 不会,它只改变显示几何,底层纹理尺寸不变。
- 缓存按图片数量限制为何不够? 100 张 64×64 与 100 张 2048×2048 内存差距巨大,应按像素/估算字节预算。
- 图片 release 后进程内存不降是否必然泄漏? 不必然,可能是共享引用、GPU 延迟或分配器保留;用多轮平台和所有权日志判断。
- 服务端缩略图为何有效? 从源头减少下载、解码、CPU 缓冲和 GPU Texture 像素。
十一、本课总结
text
文件大小不等于运行时纹理内存。
Texture 是底层像素,SpriteFrame 是区域引用。
图片内存优化需要同时考虑规格、格式、加载峰值和释放策略。十二、下一课预告
text
Lesson061|Node 和 Component 太多为什么会慢?