动态像素预算与 OOM 防护
为什么需要像素预算
Android 的 Low Memory Killer(LMK)是一个无情的进程管理机制——当系统内存不够时,它会直接杀掉任何一个申请了太多内存的进程。被 LMK 杀掉的应用不会收到任何通知,就是"瞬间消失"。
图片处理是 LMK 的高频触发场景:一张 4096×4096 的图 → RGBA 解码需要约 64 MB → Real-ESRGAN 推理期间中间激活可能需要几倍 → 再加上瓦片分块的并行 tile → 实际峰值内存轻松超过 200 MB。
Android 端如果不做防护,大图处理 = 大概率被 LMK 杀。
ZenConverter 的像素预算机制
ZenConverter 在 v0.1.9 版本引入了基于设备 RAM 的动态像素预算:
| 规则 | 数值 |
|---|---|
| 基础预算率 | 32 MP / GiB RAM |
| 最低预算 | 64 MP |
| 最高预算 | 512 MP |
换算一下:
| 设备 RAM | 动态预算 | 大约能处理的最大边长 |
|---|---|---|
| 4 GB | 128 MP | 约 11000 px |
| 8 GB | 256 MP | 约 16000 px |
| 12 GB | 384 MP | 约 19500 px |
| 16 GB | 512 MP(封顶) | 约 22600 px |
预算怎么生效
像素预算是输出级别的限制,作用于以下场景:
- Real-ESRGAN 4× AI 超分:如果原图超分后像素数超过预算上限,自动按比例 downscale 输出(保持宽高比);
- Contact Sheet 设计器:画布总像素数超过预算自动缩放(上限 8192 px / 32 MP 是另外两条独立限制,和预算共同生效);
- 图片格式互转:某些极端大尺寸的输入可能在 Bitmap API 解码时就已经超过 Android BitmapFactory 的默认限制——这种 ZenConverter 干预不了(BitmapFactory 是系统 API)。
AI 超分的特殊优化
Real-ESRGAN 的瓦片式分块推理天然降低了瞬时显存压力(每次只处理一个 tile)。但 ZenConverter 还做了额外优化:
- 动态线程数:根据设备 CPU 核数和负载情况调整推理线程数;
- 内存池:v0.2.1 引入,让中间张量复用内存而不是每次重新 malloc/free;
- 全图优化:NCNN 推理图加载前做全图级别的内存优化;
- tile 尺寸放大:v0.2.1 把 tile 从 256×256 放大到更大尺寸,减少 tile 间 overlap 区域的重复推理。
对比其他 Android 转换器的做法
| 方案 | OOM 防护 | 缺点 |
|---|---|---|
| ZenConverter(像素预算) | ✅ 动态预算 + 瓦片 + 内存池 | 极端大图会自动缩小输出 |
| 不做任何防护 | ❌ LMK 直接杀进程 | 用户体验极差 |
| 固定 tile 尺寸 + 硬编码上限 | ⚠️ 只适合特定设备 | 低端设备可能仍然 OOM |
| 上传云端处理 | ❌ 隐私风险 | 回到云端路线 |
如果预算不够
像素预算不够时会发生什么?
- 输出图片会自动按比例缩放下来(保持宽高比);
- 任务不会失败,也不会崩溃;
- 你拿到的是一张比你期望小一点的图,但完整、没损坏。
这是 ZenConverter "诚实但不失败"的设计哲学的又一个体现——宁可缩小输出也不让用户拿到一个半成品或直接崩溃。
手动干预?
像素预算是自动计算的,不暴露给用户改。如果你觉得"系统算的预算太小、我的设备能跑更大的图",ZenConverter 不会给你这个选项。
理由很直接:像素预算经过了在 Android 真机上的实际调试(v0.1.9 到 v0.2.1 迭代了三版),调整需要同时考虑 BitmapFactory 解码限制、NCNN 推理激活、tile overlap 内存、以及 Android 系统自身的运行时内存开销。让用户改一个数字很容易导致崩溃,不如自动计算更安全。
相关页面
- Real-ESRGAN 深度学习 4× AI 超分 —— AI 超分如何用瓦片式分块降显存
- 视频 AI 补帧 —— RIFE 也有 UHD 自适应下采样策略
- Contact Sheet 设计器 —— Contact Sheet 有三重上限(100 帧、8192 px、32 MP)