4、纹理与内存优化:让车载图形不再“卡顿”
好,我们进入第四章。这一章聊的是纹理和内存,说白了就是怎么让GPU吃得少、跑得快。我在车载项目里见过太多因为纹理爆炸导致系统卡死的案例,嗯,今天就把这些坑一个个填上。
4.1 纹理压缩格式选择:ASTC vs ETC2
纹理压缩,不是简单的“压缩图片”。它决定了你的显存占用、带宽消耗,甚至渲染质量。我习惯把纹理压缩比作“给GPU吃的压缩饼干”——既要管饱,又不能硌牙。
4.1.1 ETC2:老牌选手,兼容性之王
ETC2是OpenGL ES 3.0的标配。几乎所有Android设备都支持。它的压缩比固定为4:1(每个像素4比特)。
- 优点:兼容性极好,老设备也能跑
- 缺点:不支持透明通道的独立压缩(Alpha通道质量一般)
- 适用场景:非透明纹理、UI背景、普通贴图
4.1.2 ASTC:新一代王者,灵活度拉满
ASTC是ARM推出的格式,支持从4x4到12x12的块大小。压缩比可以从2:1到8:1自由调节。说白了,你可以根据纹理重要性“按需分配”显存。
| 块大小 | 压缩比 | 质量 | 推荐场景 |
|---|---|---|---|
| 4x4 | 2:1 | 极高 | UI图标、重要纹理 |
| 6x6 | 3:1 | 高 | 普通贴图 |
| 8x8 | 4:1 | 中等 | 背景纹理 |
| 12x12 | 8:1 | 低 | 模糊纹理、法线贴图 |
4.1.3 我的选择策略
我个人习惯这样选:
- UI纹理:ASTC 4x4(质量优先)
- 3D模型贴图:ASTC 6x6(平衡)
- 背景/远景:ASTC 8x8或ETC2(省显存)
- 兼容性兜底:ETC2(老设备回退)
4.2 纹理图集(Texture Atlas)技术
纹理图集,就是把一堆小纹理拼成一张大图。为什么要这么做?因为GPU切换纹理的开销很大。你想想看,每切换一次纹理,GPU就要重新绑定、刷新缓存,这中间的时间足够渲染好几个三角形了。
4.2.1 图集的核心优势
- 减少Draw Call:一张图集对应一个纹理绑定,多个UI元素可以一次绘制
- 提高缓存命中率:GPU的纹理缓存更喜欢连续访问同一张大图
- 降低显存碎片:小纹理分散存储容易产生碎片,图集统一管理
4.2.2 图集生成的注意事项
我在项目中遇到过一个问题:图集里的纹理边缘出现“出血”现象。说白了,就是相邻纹理的颜色互相渗透。原因很简单——纹理采样时,GPU会读取相邻像素。
// 图集UV计算示例
// 假设图集大小为1024x1024,小纹理位于(100, 200),尺寸为64x64
float atlasWidth = 1024.0f;
float atlasHeight = 1024.0f;
float texWidth = 64.0f;
float texHeight = 64.0f;
// 计算UV坐标(带padding)
float padding = 2.0f;
float u0 = (100.0f + padding) / atlasWidth;
float v0 = (200.0f + padding) / atlasHeight;
float u1 = (100.0f + texWidth - padding) / atlasWidth;
float v1 = (200.0f + texHeight - padding) / atlasHeight;
4.3 内存抖动(Memory Jank)的避免
内存抖动,就是内存频繁分配和释放导致的卡顿。在车载系统里,这简直是“隐形杀手”。你开着导航,突然界面卡了一下——很可能就是内存抖动在作祟。
4.3.1 抖动的根源
- 频繁创建Bitmap:每次加载图片都new一个Bitmap对象
- 临时纹理对象:渲染过程中不断创建临时纹理
- GC频繁触发:Java层的垃圾回收会暂停所有线程
4.3.2 避免抖动的实战技巧
- 预分配内存池:在初始化阶段分配好所有纹理内存
- 对象池模式:复用Bitmap和纹理对象,避免频繁new/delete
- 使用inBitmap:Android的Bitmap复用机制,允许重用已分配的内存
- 避免在渲染循环中分配内存:所有内存分配放在初始化或后台线程
4.4 Bitmap复用与缓存策略
Bitmap是Android图形系统里最吃内存的对象之一。一张1920x1080的ARGB_8888图片,就要吃掉8MB内存。你想想看,如果同时加载10张这样的图片,80MB就没了。
4.4.1 Bitmap复用:inBitmap的妙用
Android 3.0+提供了inBitmap选项,允许你复用已有的Bitmap内存。说白了,就是告诉系统:“别给我分配新内存了,用这块旧的就行。”
// Bitmap复用示例
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 必须设置为可变
options.inBitmap = reusableBitmap; // 复用已有的Bitmap
// 注意:新图片的尺寸必须小于等于reusableBitmap的尺寸
// 且格式必须兼容(建议都用ARGB_8888)
Bitmap newBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.new_image, options);
4.4.2 三级缓存策略
我推荐使用“内存-磁盘-网络”三级缓存。但车载场景下,网络缓存用得少,主要是内存和磁盘。
| 缓存层级 | 存储位置 | 容量 | 淘汰策略 |
|---|---|---|---|
| L1:内存缓存 | LruCache | 应用可用内存的1/8 | LRU(最近最少使用) |
| L2:磁盘缓存 | DiskLruCache | 50-100MB | LRU + 文件大小限制 |
// 内存缓存示例(使用LruCache)
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8; // 1/8的内存用于缓存
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024; // 以KB为单位
}
};
// 加载图片时先查缓存
Bitmap bitmap = memoryCache.get(key);
if (bitmap == null) {
// 从磁盘或资源加载
bitmap = loadBitmapFromDisk(key);
memoryCache.put(key, bitmap);
}
4.4.3 缓存更新的时机
嗯,这里要注意:缓存不是“存进去就不管了”。在车载系统中,UI主题切换、分辨率变化、甚至白天/黑夜模式切换,都可能需要刷新缓存。
- 主题切换:清空内存缓存,重新加载
- 内存紧张:收到onTrimMemory回调时,释放部分缓存
- 应用切到后台:保留磁盘缓存,释放内存缓存
下一章我们会聊渲染管线的优化,包括如何减少Overdraw、使用硬件层加速等。到时候见。