HarmonyOS 图像管线连载02:缩略图目标尺寸与内存预算【共创稿事节】
图像导入的第一步已经建立了明确的数据入口:检查文件大小,创建ImageSource,读取图像信息,解码PixelMap,再把像素对象交给页面。海边栈道这张4032×3024的照片,压缩文件只有3.8MB,但如果以RGBA_8888保留完整像素缓冲,基础大小达到48,771,072字节,约46.5MiB。让每张图库卡片都持有这样一份原图,会把接下来列表滚动和并发处理的资源预算迅速吃完。
我准备在PhotoPipeline中增加缩略图尺寸策略,而不是在Image组件上缩小原始PixelMap。两者视觉上可能差不多,内存账却完全不同:图片先完整解码再缩小组件宽度,位图数据仍然巨大;按目标尺寸解码,则可以为列表持有更合适的像素缓冲。此前的导入状态、文件检查、错误区分以及ImageSource与PixelMap所有权约束都继续沿用,本篇增加的只是尺寸决策和替换策略。

一、缩略图尺寸由承载区域决定
先把问题说具体。海边栈道为4:3横图,列表里一张卡片的图片区域设计为160vp宽、120vp高。若按这一界面的2倍密度需求准备像素,可以选择320×240;更大的预览区可能需要640×480,而进入图片详情才有理由考虑较高分辨率。这里的2倍是页面的目标清晰度策略,并不是拿来宣称设备显示密度固定为某个值。
图片的源尺寸与目标区域不一定同宽高比。若原图是竖图,强行将它拉伸到横向的320×240会变形。我会先计算“在目标边界内等比例缩放”的宽高,再交给解码参数。像素宽度从业务策略计算,显示控件再通过objectFit决定留白或裁切,这两个责任不要颠倒。对于需要完整展示的素材卡,我选择等比容纳,宁可有少量留白也不砍掉图片边缘信息。
原图4032×3024进入320×240的缩略档,两条边都缩小12.6倍,像素个数减少约158.76倍。RGBA_8888基础缓冲从46.5MiB降到0.293MiB;640×480档则约1.172MiB。实际内存仍可能受解码缓存、行对齐和绘制副本影响,因此这是一种比较档位的预算,而不是进程占用实测值。
interface ImageSize { width: number; height: number }
interface ThumbnailPlan { target: ImageSize; rgbaBytes: number; label: string }
function fitInside(source: ImageSize, box: ImageSize,
label: string): ThumbnailPlan {
if ([source.width, source.height, box.width, box.height]
.some(n => !Number.isFinite(n) || n <= 0)) {
throw new Error('invalid image dimensions');
}
const ratio = Math.min(1, box.width / source.width, box.height / source.height);
const width = Math.max(1, Math.round(source.width * ratio));
const height = Math.max(1, Math.round(source.height * ratio));
return { target: { width, height }, rgbaBytes: width * height * 4, label };
}
const sourceSize: ImageSize = { width: 4032, height: 3024 };
const small = fitInside(sourceSize, { width: 320, height: 240 }, '列表');
const medium = fitInside(sourceSize, { width: 640, height: 480 }, '预览');
const mib = (n: number): number => n / (1024 * 1024);
// 列表:320×240,0.293MiB;预览:640×480,1.172MiB。
这里用Math.min(1, …)是不对小图片盲目放大。用户选中的源图若只有120×90,按原图大小存入列表档即可,避免用插值生成更大的内存对象。业务若需要放大查看,可以另选显示缩放策略,而不是提前在缓存里长期保存放大版本。
二、把解码目标写进ImageSource请求
ImageSource.createPixelMap的解码选项支持desiredSize,它提供的是期望输出的像素尺寸;实际返回宽高应当再通过PixelMap的图像信息确认。在此阶段我把“请求的目标尺寸”和“实际生成尺寸”分开记录,不能因为传入320×240就不检查结果。图片格式、旋转信息和解码器处理路径都可能影响最终像素对象。
前篇最重要的规则是源对象与像素对象各自负责释放。这里仍然先等待createPixelMap完成,读取返回对象尺寸,再释放ImageSource;返回的PixelMap交给当前缩略图卡片。若读取尺寸或后续步骤抛出异常,已经创建出来的PixelMap要在错误路径释放,不能等卡片消失后才回收。源对象也需要finally保障收口。
import { image } from '@kit.ImageKit';
interface ThumbAsset {
pixelMap: image.PixelMap;
width: number;
height: number;
rgbaBytes: number;
}
async function decodeThumb(path: string, desired: ImageSize): Promise<ThumbAsset> {
const source: image.ImageSource = image.createImageSource(path);
let owned: image.PixelMap | undefined = undefined;
try {
owned = await source.createPixelMap({
desiredSize: { width: desired.width, height: desired.height },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});
const info = await owned.getImageInfo();
const result: ThumbAsset = {
pixelMap: owned,
width: info.size.width,
height: info.size.height,
rgbaBytes: info.size.width * info.size.height * 4
};
owned = undefined; // 当前卡片接管返回的PixelMap
return result;
} catch (err) {
if (owned) await owned.release();
throw err;
} finally {
source.release();
}
}
真实工程里还需要检查图片路径对应的授权是否仍有效;这里的path代表前篇经过输入检查、应用可以访问的本地文件。desiredSize并不取代文件是否为空、扩展名是否接受等前置判断,尤其0B的破损样本应继续在解码前被拒绝。这样统计维持INPUT 2、VALID 1、REJECT 1,真正交付一张缩略图后THUMB READY才为1。
三、把图片卡和内存所有权连起来
只计算目标尺寸还不足以避免内存浪费。用户可能快速从列表进入详情,再返回列表,期间发起两次不同目标宽高的解码。若没有当前请求身份,较慢的640×480请求可能在页面已回到列表后返回,覆盖刚刚准备好的320×240缩略图。我为每个图片卡保留请求世代,并用“图片标识+目标尺寸”形成缓存键。
缓存键用原始文件的稳定ID,不要只用文件名。两个相册目录都可能有“海边栈道.jpg”,尺寸不同或内容已更新时也应得到不同缓存版本。之后图片文件发生修改,可以加内容修订值或时间戳;尺寸改变只影响缓存版本,不应该改变文件在导入记录里的身份。
我更希望把“能不能继续显示”与“何时可以释放”写成明确事件。用户切到大图时,小图可以留作列表的备用,但要受总预算约束;页面彻底退出、图卡销毁或缓存淘汰时,必须先解除Image绑定并停止依赖它的异步处理,再释放PixelMap。单靠对象被垃圾回收并不足以表达原生图像资源的时序。
interface ThumbRequest { assetId: string; generation: number; width: number; height: number }
interface ThumbResult { request: ThumbRequest; rgbaBytes: number }
class ThumbGate {
private latest = new Map<string, number>();
issue(assetId: string, width: number, height: number): ThumbRequest {
const generation = (this.latest.get(assetId) ?? 0) + 1;
this.latest.set(assetId, generation);
return { assetId, generation, width, height };
}
accept(result: ThumbResult): boolean {
return this.latest.get(result.request.assetId) === result.request.generation;
}
invalidate(assetId: string): void {
this.latest.set(assetId, (this.latest.get(assetId) ?? 0) + 1);
}
}
const gate = new ThumbGate();
const first = gate.issue('coast-asset', 640, 480);
const latest = gate.issue('coast-asset', 320, 240);
// 较早的完成结果被拒绝后,持有者需要释放其PixelMap。
世代数只决定结果归属,不会自动释放像素对象。这个界限很重要:accept返回false时,调用方必须走释放分支;返回true时,才更新页面当前缩略图引用。后续TaskPool章节会使用同一代际规则,避免队列中已等待的旧任务重新占用卡片。这样当前章节建立的尺寸决策可以直接移交并发阶段。

四、为什么不在页面滚动时解码大图
列表滚动本身是高频事件,不能把每次卡片刚进入可视区域都解释成“立即解码原图”。如果几十张图片同时进行解码,即使每张文件很小,峰值内存仍可能被像素缓冲冲高。我准备以缩略图尺寸与缓存预算作为后续队列的输入,让可见卡片优先请求列表档;同一资源多次请求时,可复用已完成且尺寸合适的对象。
当前阶段先区分四种状态:待检查、准备缩略图、可显示、失败。图片在等待解码时可以占位;完成后才增加THUMB READY计数。若用户在卡片完成前快速切走,旧请求的完成回调仍可能到来,但它不能写入新页面。页面生命周期与请求世代必须一同参与判断,不然仅用图片ID仍会让被销毁的页面复活。
尺寸选择同样影响清晰度。320×240足以支撑160vp×120vp、按2倍像素目标设计的卡片,但如果用户将这张缩略图强行撑到全屏,会有明显放大。进入详情时应明确申请更高档位,同时限制大图驻留数量。不要为了少一次请求,把所有列表卡都预先解码成4032×3024原图。
五、用数据而非感觉比较三档开销
当前样本只有一张有效照片,所有档位的像素成本都能直接算出来:4032×3024按每像素4字节约46.5MiB;640×480约1.172MiB;320×240约0.293MiB。小档相对原图降低约158.76倍,预览档相对原图降低约39.69倍。数字来自宽高和像素格式的理论计算,能帮助检查“目标尺寸是不是选错了”,但不能代替真实进程里对内存峰值的持续观察。
UI上我将有效图片标成“海边栈道.jpg”,显示4032×3024的源尺寸与320×240的列表缩略图尺寸。另一个“破损样本.jpg 0B”仍然是拒绝状态,不进入解码队列。这样界面不会把“拒绝空文件”算到缩略图失败里,调试日志里的VALID、REJECT和THUMB READY也有各自清晰的业务口径。

六、替换缩略图前还要管好失败路径
如果用户选择的图片过大、像素尺寸非法或者访问路径失效,应用应在创建PixelMap前提供可读错误提示;如果创建过程发生异常,卡片状态进入FAILED,ImageSource仍要释放。若获得PixelMap后读取图像信息出错,也应释放已经创建的对象。真正提交给页面后,所有权才从解码函数移交给卡片。
我会保留源尺寸与实际缩略尺寸两个字段。源尺寸用于决定下一次目标大小,实际缩略尺寸用于重新计算缓存预算,不能把desiredSize的输入直接当成返回结果。对于竖图,应继续按等比容纳算出目标尺寸;对于尺寸很小的图片,fitInside限制放大,可避免额外生成无意义的像素对象。
另一个边界是旋转信息。照片的显示方向可能受元数据影响,因此资源进入卡片时,界面需要根据最终PixelMap的尺寸安排布局,而不是假设所有源文件头记录的宽高方向都代表最终显示方向。缩略图如果与容器宽高比不同,使用合适的objectFit策略呈现完整内容,并统一规定背景留白颜色,减少列表跳动。
七、下一阶段的工程入口
华为开发者的Image Kit图片解码指南可参阅:https://developer.huawei.com/consumer/en/doc/atomic-guides/atomic-image-decoding ,ImageSource和createPixelMap的使用方法应结合具体目标设备支持的图像格式核对。关于解码选项中期望尺寸的字段,另见:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/capi-image-nativemodule-oh-decodingoptions 。后者属于Native接口资料,讲的是同一尺寸规划思路,不应把Native结构体接口直接套进ArkTS。
到这里,PhotoPipeline从“能将有效文件解码为PixelMap”推进到了“按显示目标分配合理的位图大小”。真正难的下一步是多个文件在队列中争用解码资源时,如何限制并发数,按可见优先级排序,并在页面退出后处理迟到结果。尺寸策略和代际判断已经准备好,不需要靠扩大缓存来掩盖队列设计的问题。
更多推荐



所有评论(0)