一张照片能打开,不等于它适合立刻进入超分流水线。用户看到的是“选择图片”,程序看到的却可能是一张超过千万像素、解码后需要持续占用大块内存的输入。真正容易出问题的地方,往往不是算法按钮,而是算法开始前那一小段没有边界的准备工作。

本文设计一个独立的 PixelBudget 演示工程,任务号固定为 SR-421。它只回答“这张沙箱内的图片是否满足本次业务像素预算”,不运行超分模型,也不把界面数据说成真机测量结果。演示输入为 source_421.jpg,图像信息预设 4032×3024,乘积 12,192,768 像素;业务阈值取 12,000,000 像素,因此应得到“已拦截:像素超出预算”。阈值是示例产品规则,不是 HarmonyOS 官方限制。

一、把“图片有效”和“可以进入超分”分开

1. 不要用文件体积替代像素预算

JPEG 文件小,并不意味着解码内存小。压缩大小反映的是编码后的数据量;真正进入像素处理流程以后,宽度、高度、像素格式、解码时的中间缓冲区和后续模型张量都会影响占用。拿 4032×3024 的图像做简单上界估算,如果按每像素四字节计算,仅一份未压缩像素缓冲区就约有 46.5 MiB,尚未包括额外副本。这只是算术估算,不是设备实测峰值。

PixelBudget 因此分两道闸。第一道判断路径是否来自应用可以合法访问的沙箱,第二道在不创建 PixelMap 的前提下读取图像尺寸。通过不等于一定能运行算法,只代表允许继续进入后续处理队列。解码格式、图像是否损坏、目标倍数、设备可用内存和算法模型限制,还需要下游再核验。

2. 明确图片来自哪里

本文代码以 getContext(this).cacheDir + '/source_421.jpg' 作为示例路径。要复现它,必须先由应用写入一个真实的图片文件;仅把这个字符串传给接口,并不会神奇地生成图片。系统相册返回的 URI、媒体授权与沙箱路径也不是同一类对象,不能将不同来源直接互换。演示只覆盖可读的沙箱文件,不申请广泛相册读取权限。

二、让 ImageSource 只做轻量预检

1. 创建图源、读取尺寸、释放对象成对出现

解决的问题是“异常发生在获取宽高之前时,句柄仍能收回”。官方 Image Kit 的 image.createImageSource 创建图源,ImageSource.getImageInfo(0) 返回图像信息,release() 负责释放相关资源。下面是 entry/src/main/ets/service/ImagePreflight.ets 中的独立服务逻辑:

import { image } from '@kit.ImageKit';

export interface PreflightResult {
  width: number;
  height: number;
  pixels: number;
  limit: number;
  allowed: boolean;
  reason: string;
}

export async function checkImage(filePath: string,
  limit: number = 12000000): Promise<PreflightResult> {
  let source: image.ImageSource | undefined = undefined;
  try {
    source = image.createImageSource(filePath);
    const info: image.ImageInfo = await source.getImageInfo(0);
    const width: number = info.size.width;
    const height: number = info.size.height;
    const pixels: number = width * height;
    if (width <= 0 || height <= 0 || !Number.isSafeInteger(pixels)) {
      throw new Error('INVALID_IMAGE_SIZE');
    }
    return {
      width, height, pixels, limit,
      allowed: pixels <= limit,
      reason: pixels <= limit ? '可进入后续队列' : '像素超出预算'
    };
  } finally {
    if (source !== undefined) {
      await source.release();
    }
  }
}

这里故意没有调用 createPixelMap(),因为预检的目标并不是预先解码整张图。getImageInfo(0) 使用单帧的索引 0,不能推断动图的每一帧都符合预算。finally 会在正常返回与读取异常时执行,但释放过程自身也可能报错;生产环境可把释放异常记录为次级故障,避免覆盖更重要的原始错误。不要在 release() 之后继续使用这个 ImageSource,也不要并发调用同一个图源实例的方法。

2. 页面状态必须区分拦截与失败

“超预算”是业务拒绝,并非 SDK 调用失败;而“文件不存在”“格式不支持”“图像头损坏”才属于预检错误。它们都不能流入模型任务队列。页面示意文件为 entry/src/main/ets/pages/PreflightPage.ets,只持有返回的普通数值,不把资源对象塞进 @State。

import { checkImage, PreflightResult } from '../service/ImagePreflight';

@Entry
@Component
struct PreflightPage {
  @State status: string = '待预检';
  @State detail: string = '请选择输入';
  @State busy: boolean = false;
  private readonly taskId: string = 'SR-421';

  async runPreflight(): Promise<void> {
    if (this.busy) { return; }
    this.busy = true;
    this.status = '预检中';
    try {
      const path = getContext(this).cacheDir + '/source_421.jpg';
      const result: PreflightResult = await checkImage(path);
      this.status = result.allowed ? '符合预算' : '已拦截';
      this.detail = `${result.width}×${result.height},` +
        `${result.pixels}/${result.limit} 像素:${result.reason}`;
      console.info(`PixelBudget ${this.taskId} pixels=${result.pixels}` +
        ` limit=${result.limit} ${result.allowed ? 'accept' : 'reject'}`);
    } catch (e) {
      this.status = '预检失败';
      this.detail = '无法读取图像信息,请重新选择文件';
      console.error(`PixelBudget ${this.taskId} preflight error`);
    } finally {
      this.busy = false;
    }
  }

  build() {
    Column({ space: 18 }) {
      Text('超分输入预检').fontSize(28).fontWeight(FontWeight.Bold)
      Text(`任务 ${this.taskId}`)
      Text(this.status).fontColor(this.status === '已拦截' ? Color.Red : Color.Blue)
      Text(this.detail)
      Button(this.busy ? '检查中…' : '重新选择图片')
        .enabled(!this.busy).onClick(() => { this.runPreflight(); })
    }.padding(20).width('100%')
  }
}

此处按钮文案只表达“再次发起一次预检”的示意入口,实际挑选图片的 PhotoViewPicker 流程应另外接入;不能误读为按钮本身具有系统选图能力。页面消失以后,已完成的 Promise 仍可能回写旧组件。正式产品需增加任务代际标记或转移到页面外的任务管理器;本例为了把图源收口讲清楚,没有冒充完整生产级调度框架。

3. “预检中”不能代替生命周期保护

在示意页面里,busy 可以阻止用户对同一个按钮连点,却不能阻止系统暂停页面,也不能保证上一次的异步任务已经退出。如果页面被重建,旧实例中的异步回调仍可能完成。适合实际项目的做法是给每次选择生成独立的业务序号,并在返回时与当前序号比较;只允许最新序号更新页面。这里刻意没有把该机制塞进三段核心示例,是为了不把图源释放和回包隔离混成一个难读的大函数,但正式接入时两条保护都应该具备。

图片源对象也不宜作为页面长生命周期字段保存。若 UI 只是展示宽高数字,服务层负责创建、读取和释放,页面只保存结果即可。这样页面退出后没有悬挂的图源引用,也不会因为用户频繁打开结果页而重复持有 native 资源。特别要避免把释放理解为“关闭前随便调一下”:同一 ImageSource 仍在读取元数据时释放它,容易把正常的异步顺序改成竞态,因此应等待读取完成,再进入 finally。

三、用确定性的规则演算验收,而不是编造性能数字

1. 先测规则,不依赖相册、算法或网络

光检查界面有没有变红,很难判断到底是阈值规则生效,还是图片解码刚好失败。应先把纯业务判断单独抽离出来,用伪造尺寸输入验证边界。这类演算可以离线执行,但不能替代设备上的 ImageSource 兼容性测试。

export function withinBudget(width: number, height: number,
  limit: number = 12000000): boolean {
  if (!Number.isInteger(width) || !Number.isInteger(height) ||
      width <= 0 || height <= 0) { return false; }
  const count = width * height;
  return Number.isSafeInteger(count) && count <= limit;
}

// 业务规则模型样例:不是设备运行日志。
const modelCases: boolean[] = [
  withinBudget(4032, 3024),  // false: 12,192,768
  withinBudget(4000, 3000),  // true: 恰好 12,000,000
  withinBudget(0, 3000)      // false: 非法尺寸
];

三个用例分别覆盖越界、等于阈值和非法宽度,避免单纯测试一张图片形成偶然结论。预设界面中的 SR-421 被业务规则拦截,重点日志应是 PixelBudget SR-421 pixels=12192768 limit=12000000 reject。随后可记录 ImageSource released 表达期望的释放路径;图中日志是为了展示检查点而编排的演示行,不是实际 HiLog 采集。

2. 结果页只承诺“预检完成”

演示画面展示源图 source_421.jpg、4032×3024、12,192,768 像素、12,000,000 像素限制和“已拦截”。为了避免理解偏差,不给它加“超分成功”“用时 1.2 秒”“内存降低 30%”等无来源指标。实际业务还应记录输入文件的身份与任务代次,防止同名文件覆盖以后将旧结果误用于新输入。

3. 预检日志还要避免记录敏感路径

业务日志足够记录任务号、尺寸、像素数、阈值、最终判定和释放结果,不应默认把用户真实图片的完整沙箱路径或其他个人数据打进日志。对本例而言,SR-421 是人工设定的演示标识,source_421.jpg 也不是某位用户的真实相册文件。如果后续加入图库选择,最好用应用内部生成的短期请求 ID 对应资源对象,不把公开可传播的原始 URI 当成业务主键。排查错误时,可以区分“打开失败”“读取信息失败”“超出预算”“资源释放失败”四种事件,避免统一打印 ERROR 导致后续无从定位。

灰度发布时也应关注两个相反的代价:阈值太低,会把可以正常处理的照片误拦下来;阈值太高,则可能在低内存设备上造成后续模型资源竞争。因此应在真实设备压力测试以后把阈值放到配置中管理,并留下可回退版本。此处不提供任何由图形界面推导的性能提升百分比,减少把视觉演示误当成压测报告的风险。

四、工程边界与后续扩展

本方案解决的是处理前的准入判断和 ImageSource 生命周期,不等于具备图像超分算法。它没有评估图片内容质量,没有计算纹理特征,没有分配模型 Tensor,也没有替换 Image Kit 的解码器能力;从 UI 上看到“已拦截”,只能说明预设样例在业务阈值下该被挡住。预算值应来自明确的机型策略与实际压测,不能把 1200 万像素当通用 SDK 上限。

需要接入真实产品时,下一步按“取得合法可读源文件 → 元信息预检 → 解码与格式校验 → 模型队列容量确认 → 推理 → 输出资源释放”逐层推进。每层都有独立错误语义,尤其要关注不可读路径、动图多帧、元数据损坏、退后台、用户反复换图、任务取消,以及释放失败。把拦截条件往前放,是为了减少无谓工作;是否真正改善内存与耗时,还必须用目标设备和具体模型测量后才能得出结论。

资料核对: 华为官方 ImageSource API、Image Kit 图片开发指南。本文演示数据、画面、日志均为设计样例,没有进行真实 DevEco 编译、真机图像处理或推理测试。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐