截图整理工具的命门在数据库

做智能截图整理工具,最绕不开的一步是把 OCR 识别出的文字、分类标签、特征向量全部存进本地数据库。开发阶段拿几十张图测试,一切正常。用户相册里导进来两万张截图,点击“开始整理”,界面直接冻住,十几秒后应用闪退。

日志里没有崩溃堆栈,但 onCreate 里串行的初始化逻辑和后续的批量写入把主线程堵死了。relationalStore 是本地数据持久化的基础,单条 insert 走的是“开事务、写磁盘、提交”的完整链路,两万条记录就是两万次磁盘往返,主线程扛不住是必然的。

能 batchInsert 就不要循环 insert

batchInsert 底层使用预处理语句批量执行,在 Native 层复用编译好的 SQL,省掉了上千次 ArkTS 与 Native 之间的跨语言开销,比“事务+循环 insert”还要快数倍。循环 insert 每一条都是一次独立的磁盘写入和事务提交,20 条记录大约需要 100ms,数据量越大性能越差。

typescript

import { relationalStore } from '@kit.ArkData';

async function batchInsertScreenshots(
  store: relationalStore.RdbStore,
  records: ScreenshotRecord[]
): Promise<number> {
  const buckets: relationalStore.ValuesBucket[] = records.map(r => ({
    id: r.id,
    file_path: r.filePath,
    ocr_text: r.ocrText,
    category: r.category
  }));

  // batchInsert 内部已含事务机制,不要在外面再包一层巨型事务
  const count = await store.batchInsert('screenshot_table', buckets);
  return count;
}

batchInsert 返回实际插入的行数。如果返回值小于 buckets.length,说明有行因为主键冲突或约束被跳过,不是全部成功。常见错误是只判断有没有抛异常,不看返回值,缓存缺了一块还以为写全了。

超大批次必须分片,长事务会锁住整个写通道

batchInsert 虽然快,但不代表可以把两万条数据一次性塞进去。RDB 的写连接是串行化的,一个巨型事务会长时间占住唯一的写锁,后台的报表查询、缩略图索引更新全部排队等锁,用户看到的就是“导入进度卡在 30% 不动了”。

推荐的策略是按 500 到 1000 条一批切分,每批独立执行 beginTransaction → batchInsert → commit,提交一次立刻释放写锁,下一批再拿。

typescript

async function chunkedInsert(
  store: relationalStore.RdbStore,
  allRecords: ScreenshotRecord[],
  batchSize: number = 800
): Promise<void> {
  for (let i = 0; i < allRecords.length; i += batchSize) {
    const chunk = allRecords.slice(i, i + batchSize);
    store.beginTransaction();
    try {
      const buckets: relationalStore.ValuesBucket[] = chunk.map(toBucket);
      await store.batchInsert('screenshot_table', buckets);
      store.commit();
    } catch (e) {
      store.rollBack();
      throw e;
    }
  }
}

rollBack 必须在 catch 里出现,不能只打日志然后让函数正常返回。某一批失败时,前面的批次已经提交,后面的批次还没开始,数据处于“部分完成”状态,业务侧需要记录进度游标,下次从断点继续。

批量写入的执行线程选择

beginTransaction 是同步 API,它不会替你切线程。这段代码跑在哪个线程,事务就占哪个线程。批量导入必须放到 TaskPool 里,在子线程中获取 RdbStore 实例、执行分片写入、串行提交,主线程只负责接收进度更新。

常见错误是在 TaskPool 的 @Concurrent 函数里引用主线程的 RdbStore 实例。@Concurrent 函数运行在独立线程,外部闭包捕获的对象在跨线程时不会按预期工作,必须在子线程内部重新 getRdbStore,或者通过参数把必要的配置传进去。

两万条截图的导入,循环 insert 可能要半分钟以上,分片 batchInsert 配合 TaskPool 能压到三秒以内。数据库的批量写入不是“把循环换成另一个 API”那么简单,批次大小、事务边界、执行线程三件事都对上,导入过程才不会变成用户的一次耐心测试。

Logo

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

更多推荐