工程示例:AssetGate / AssetAuditPage / AG-816
阅读说明:下面所有文件名、状态与日志均为可复现的设计演示数据,不代表应用市场真实审核结论,也不代表代码已经在设备上编译运行。

一、把一次清单检查拆成可以交付的结果

做应用上架准备时,图标、宣传图、功能截图往往先经过内容同学的命名和归档,再进入构建或提审流程。一个很容易被忽略的问题是,文件名在列表里看起来没问题,实际却可能带着尾部空格、未知扩展名或重复路径。等材料到打包环节再发现,排查成本就会上升。这里要解决的不是应用市场的规则,而是项目自己制定的一组上架前内部约束:仅允许小写或大小写混合的 png、jpg、jpeg 扩展名,文件主名不能首尾带空格,不接受空文件名。

我给这个小工具取名 AssetGate。它只接收调用方已经枚举出来的文件名清单,不去申请相册权限,也不读取图片内容。示例任务 AG-816 包含十二条条目,每四条一组,共三组。十一条符合规则,唯一异常项是 cover .png:扩展名前的文件主名留下一个空格。界面最终显示「需人工复核」,而不是宣称某份材料一定会被商店驳回。

这张封面只表达“分片检查—合并报告”的关系。真正值得写清楚的部分是异步边界:三组计算可能乱序完成;用户又可能在上一轮结束前换了一批文件。如果简单地把回调结果追加到同一个状态数组,旧报告很容易混进新报告。这比扫描一个空格更难察觉。

二、先固定数据契约,再谈并发

1. 一个批次只有一个版本号

本示例把任务 ID 与业务版本分开。AG-816 用来标记这份演示清单,epoch=4 表示当前有效的第 4 次检查。换清单、重新开始、离开页面,都要让版本号前进。版本号不是 TaskPool 自动分配的任务标识,也不是文件哈希,只是页面自建的“当前结果所有权”标记。任何任务回来时,都必须确认自己的版本仍有效。

分组大小固定为四项,是为了让日志更直观,并非官方推荐的固定线程数。在真实项目中,应根据任务粒度、设备能力、字符串数组拷贝成本来测试。十二个短名字本来很适合同步执行,这里故意做成 TaskPool 示例,是为了展示更大规模材料列表在扩展时如何管理返回值,而不是把多线程包装成必然更快的优化。

2. 并发函数只做纯计算

先把规则隔离为纯字符串处理,避免在子线程里碰 UI 状态、窗口对象和生命周期组件。@Concurrent 是 TaskPool 并发任务函数的官方机制,taskpool.Task 和 taskpool.execute 用来提交执行。函数参数只传字符串数组,返回也只包含非法名称;这样既容易序列化,也不需要跨线程共享可变对象。

// validator/NameAudit.ets:仅检查文件名,不读取文件内容
@Concurrent
export function auditNameChunk(items: string[]): string[] {
  let invalid: string[] = [];
  for (let name of items) {
    const dot: number = name.lastIndexOf('.');
    if (dot <= 0) {
      invalid.push(name);
      continue;
    }
    const stem: string = name.slice(0, dot);
    const ext: string = name.slice(dot + 1).toLowerCase();
    if (stem.trim() !== stem || stem.length === 0 ||
        (ext !== 'png' && ext !== 'jpg' && ext !== 'jpeg')) {
      invalid.push(name);
    }
  }
  return invalid;
}

代码没有把 cover .png 自动改成 cover.png,这是有意的。自动重命名可能破坏其他配置里的引用关系;检查器只出问题清单,是否修复由材料负责人确认。同时要注意规则只看到文件名:它并不能判断文件是否真实存在、图片能否解码,也不能把扩展名正确视为内容安全。名称合法和素材可用是两个层次。

三、TaskPool 聚合时不要边完成边公开

1. 三个分片一起执行,报告一次性提交

如果每个分片完成就立刻覆盖页面上的“通过数”,用户看到的可能是 4/12、8/12、12/12 来回变化;更麻烦的是,有的分片来自上一次扫描。为此我使用一个 AuditCoordinator:启动时占用新的 epoch,先收齐三个 Promise 的结果,再生成最终报告。完成回调只反馈进度,不发布部分结论。异常时保持失败状态而不是展示上一次成功数字。

// service/AuditCoordinator.ets
import { taskpool } from '@kit.ArkTS';
import { auditNameChunk } from '../validator/NameAudit';

export interface AuditReport {
  invalid: string[];
  total: number;
  epoch: number;
}
export class AuditCoordinator {
  private epoch: number = 0;
  invalidate(): void { this.epoch += 1; }

  async run(names: string[], onChunk: (done: number) => void):
    Promise<AuditReport | undefined> {
    this.epoch += 1;
    const ticket: number = this.epoch;
    const chunks: string[][] = [];
    for (let i: number = 0; i < names.length; i += 4) {
      chunks.push(names.slice(i, i + 4));
    }
    let done: number = 0;
    const requests: Promise<string[]>[] = chunks.map((part: string[]) => {
      const task: taskpool.Task = new taskpool.Task(auditNameChunk, part);
      return taskpool.execute(task).then((value: Object) => {
        if (ticket === this.epoch) { onChunk(++done); }
        return value as string[];
      });
    });
    let results: string[][];
    try {
      results = await Promise.all(requests);
    } catch (e) {
      if (ticket !== this.epoch) { return undefined; }
      throw e; // 仅当前版本的错误上抛给页面
    }
    if (ticket !== this.epoch) { return undefined; }
    return { invalid: results.flat(), total: names.length, epoch: ticket };
  }
}

这里的 invalidate 是逻辑取消,不等于系统已经终止任务线程;过期任务仍可能消耗时间,返回后仅被丢弃。Promise.all 也有明确边界:任一任务拒绝,整组结果不应提交,其他任务可能还在运行。正式工程应保留任务引用,在目标 API 能力允许且确有必要时再增加物理取消与背压控制,但不能假定取消操作一定同步生效。

图中工程目录把并发函数与页面文件分开,右侧展示任务 AG-816 的预设结果,底部 HiLog 是按契约排出的演示日志。它们用于解释结构与检查点,不是 DevEco Studio 实际编译、线程调度或性能采样证据。

四、页面只消费一个已确认版本

页面层不应该关心 TaskPool 的每个任务具体落在哪个线程,只需要知道当前是不是还在检查、分片完成多少以及有没有得到完整报告。界面状态固定为 CHECKING、REVIEW_REQUIRED、READY 和 ERROR。示例清单结束后是 REVIEW_REQUIRED;进度是 3/3,对应 100%,通过十一条,异常一条。界面里的“需人工复核”不等于自动修复按钮,也不能伪装成商店审核回执。

// pages/AssetAuditPage.ets,摘录页面状态与触发入口
@Entry
@Component
struct AssetAuditPage {
  private engine: AuditCoordinator = new AuditCoordinator();
  private viewTicket: number = 0;
  @State status: string = 'READY';
  @State chunksDone: number = 0;
  @State passed: number = 0;
  @State badNames: string[] = [];

  async check(names: string[]): Promise<void> {
    const token: number = ++this.viewTicket;
    this.status = 'CHECKING';
    this.chunksDone = 0;
    try {
      const report = await this.engine.run(names, (done: number) => {
        this.chunksDone = done;
      });
      if (!report || token !== this.viewTicket) { return; } // 旧批次不可落屏
      this.badNames = report.invalid;
      this.passed = report.total - report.invalid.length;
      this.status = report.invalid.length ? 'REVIEW_REQUIRED' : 'READY';
    } catch (e) {
      if (token === this.viewTicket) { this.status = 'ERROR'; }
    }
  }
  aboutToDisappear(): void { this.viewTicket++; this.engine.invalidate(); }
  build() { Column() { Text('上架素材预检'); Text(this.status); } }
}

在完整工程里,页面发起新扫描时,应先撤下旧报告或明确标注旧报告所属任务,避免一边显示“检查中”一边保留旧的“全部通过”。还有一个容易漏掉的问题:onChunk 回调只有检查完 ticket 才能更新进度;页面释放之后也会通过 invalidate 拒绝过期提交。本文的状态处理描述了思路,需在项目所使用的 ArkTS 编译器和 UI 生命周期上补齐工程集成验证。

五、怎样判断这套方案真的解决了问题

这组数据契约可以不依赖手机建立单元验收:准备十二个名称,包含 cover .png,按四条分组;人工核对结果应是 3/3 分片、11 条合格、1 条异常。接着在第一个批次尚未结束时调用第二次检查,确保前一次虽然回调,但不会覆盖新批次状态。最后在页面退出时使 epoch 失效,检查不存在页面已销毁但报告又被写回的路径。

手机图展示的是预期产品状态,不是设备运行截图。图中的「12 条素材」「通过 11」「异常 1」「进度 100%」「AG-816」「需人工复核」必须与文章和示意 HiLog 一致。若真实执行时数字不同,应以实际输入清单和真实日志为准重做图,而不是把图当成证据反过来修改数据。

这一轮还需要特别检查异常传播:任何分片失败都应令整个批次进入 ERROR;下一次手动重试必须换 epoch;同名文件是否算重复属于另一项独立策略;路径穿越、符号链接和文件内容校验应在具备可信文件访问能力的专门模块处理,不能通过这段字符串规则代替。TaskPool 的线程通信有序列化与调度成本,小清单同步运行可能更划算,不能把“使用了并发”直接写成“性能优化成功”。

六、边界比通过数字更重要

AssetGate 当前只做发布材料的内部命名预检,没有联网调用 AppGallery Connect,也没有声称替代华为应用上架审核。三分片、十二文件、cover .png 都是用于解释状态机的自定义示例。真正落地时,文件存在性、媒体真实格式、资源引用链、目录权限、大小和隐私要求还应分别建立校验器,并给每种失败分配可追踪的错误码。

我更看重的不是一次检查能不能返回十一比一,而是重复点击、页面销毁、后台耗时变化时,结果是否始终来自当前那批文件。@Concurrent 负责让纯计算有机会离开 UI 主线程;epoch 与“一次性聚合提交”才决定这份报告能否被业务相信。异步任务执行成功,不等于报告当前有效;静态规则通过,也不等于素材一定可以提审。把这两层判断拆清楚,后续扩展真正的图片解码与上架验证才有可靠的基础。

官方核对依据: 华为开发者中心《同步任务开发指导(TaskPool和Worker)》、《TaskPool使用规范》及《使用TaskPool执行独立的耗时任务》。示例仅引用官方已公开的 @Concurrent、taskpool.Task、taskpool.execute 机制;具体规则为应用自建。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐