Skip to content

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。

误区三:纹理越小越好 ​

过小会导致模糊、采样问题和频繁缩放,应该按实际显示需求选择。

误区四:内存工具中的进程值下降才算释放成功 ​

平台分配器可能保留内存,应结合对象引用、纹理统计和趋势判断。


十、练习与答案 ​

  1. 2048×2048 RGBA 图片解码后大约需要多少内存?
  2. 为什么多个 SpriteFrame 可能只对应一个 Texture?
  3. 场景切换后纹理不降,应该先查哪些地方?
  4. 头像显示尺寸很小却使用大图,会带来什么问题?

答案:

  1. 约 16 MB,不含其他缓存和副本。
  2. SpriteFrame 只是同一纹理中的区域描述。
  3. 外部引用、缓存、Bundle、共享纹理、GPU 延迟释放。
  4. 浪费解码内存、显存、带宽和上传时间。

一张图片的内存账本 ​

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/副本,只用于数量级。实际以导入格式和平台工具为准。

实验步骤 ​

  1. 冷加载第一张,记录文件大小、回调和纹理指标。
  2. 加载第二张,比较估算。
  3. 重复加载同一路径,验证缓存身份。
  4. 清空 Sprite 但不 release owned,观察所有权仍存在。
  5. 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。估算必须与真机工具校准。

  1. 节点 scale=0.1 会降低 Texture Memory 吗? 不会,它只改变显示几何,底层纹理尺寸不变。
  2. 缓存按图片数量限制为何不够? 100 张 64×64 与 100 张 2048×2048 内存差距巨大,应按像素/估算字节预算。
  3. 图片 release 后进程内存不降是否必然泄漏? 不必然,可能是共享引用、GPU 延迟或分配器保留;用多轮平台和所有权日志判断。
  4. 服务端缩略图为何有效? 从源头减少下载、解码、CPU 缓冲和 GPU Texture 像素。

十一、本课总结 ​

text
文件大小不等于运行时纹理内存。
Texture 是底层像素,SpriteFrame 是区域引用。
图片内存优化需要同时考虑规格、格式、加载峰值和释放策略。

十二、下一课预告 ​

text
Lesson061|Node 和 Component 太多为什么会慢?