📦 Flutter 三方库 flutter_native_image 是用于本地图片压缩、缩放与裁剪的 package。我在鸿蒙图片处理页面排查等待时,原有鸿蒙实现只能看到调用总耗时而无法定位内部阶段。我在原库基础上增加了按请求开启的 HiTrace 阶段追踪,并补齐了压缩与 EXIF 阶段的资源释放。
完整代码已提交 PR 至官方仓库 CPF-Flutter/flutter_native_image,有问题可以到仓库提 Issue,也可以来 鸿蒙系统Flutter开源库社区 交流。

前言

我做了一个鸿蒙图片处理页面,先在 Mate X7 模拟器上连续压缩同一张图片,后来又接到 华为畅享 100 Pro Max 真机(型号 YEW-AL80)上复测。页面能显示完成次数和总耗时,但首轮为什么更慢、不同设备慢在哪个环节,光看秒表找不到原因。

于是,我沿着插件里的解码、缩放、编码和清理调用加上 HiTrace 标记,再对照系统线程调度记录。模拟器六次请求中,五次最长的独立阶段是解码,另一次出现了 34.254 ms 的像素对象释放等待;而到了畅享 100 Pro Max 真机上,像素对象释放仅需 0.773—1.070 ms,主要耗时集中在解码与编码两个工作线程阶段。

🔍 有了这些分段,我不用盲猜,就能直接看清当前设备该查解码、编码还是释放。这次补齐的是诊断入口;具体等待原因,还需要沿着长区间继续查。

一、使用场景与归因问题

1.1 在模拟器上复现一次可重复的图片处理

诊断页面在沙箱生成图片,依次调用三次 FlutterNativeImage.compressImage,记录等待时长和输出大小,并显示阶段与完成次数。它没有接入相册或上传服务,观察范围限于本地处理。

折叠屏图片处理页面与图片准备、插件调用、系统处理和追踪分析流程

图1 压缩流程

Example 用一个简单的路径插件返回应用缓存目录,通道名为 pgc/native_image_paths。这个通道只服务于示例工程,不属于图片插件对外接口,既省去额外引入路径库,也减少无关通道消息进入采样时间窗。

拿到目录后,Flutter 用画布生成 1280×960 彩色网格 PNG,输入为 8411 字节。

应用等待 3 秒供外部脚本准备,再以 percentage: 70、quality: 70 连续压缩三次相同字节。准备等待不计入压缩秒表。

输出 JPEG 均为 66754 字节,比紧凑编码的合成 PNG 大。这份样本只用于重复执行处理管线,不证明压缩率或上传速度改善;实际图片需另测。

1.2 先核对已有库,再确定扩展位置

我检查了 CPF-Flutter 与 oh-Flutter 的相关仓库。flutter_native_image 已通过 @ohos.multimedia.image 实现鸿蒙压缩,可以直接复用;缺少阶段埋点属于可补能力,不是原包 Bug。

CPF-Flutter 的 flutter_native_image 仓库文件列表与鸿蒙实现目录

图2 社区仓库

UME 通道观察器、通道监控工具包、flutter_logs 开发分支及 sentry_flutter 鸿蒙实现覆盖消息、日志或事件观察,却未暴露图片插件内部阶段。消息往返时间无法拆出解码、编码和像素对象释放各自的等待。

因此,我在原有 compressImage 上增加默认关闭的 enableOhosTrace。未显式开启时,通道消息不增加字段,原生侧也不向追踪缓冲区写入标记;传入 true 后才输出本次调用的阶段区间。它不自行启动整机采集,也不上传数据,采样时仍需配合设备上的 hitrace 命令同时抓取业务标记与调度切片。

1.3 先理顺处理顺序,再加入追踪

我以社区公开标签 0.0.6_1-ohos-1.0.0 为实验起点,包内版本是 0.0.6+1。我也检查了社区 master、dev 和上游 btastic/flutter_native_image 的 master,用来确认已有修正的来源;核查时,社区两个分支的核心原生文件一致。

🧩 在这个版本上,我按下面的顺序处理:

  1. 先等图片处理完成。 原标签使用回调式 fs.write,没有等待 copyExif,属性循环又用了 forEach(async ...),可能在操作结束前就返回路径。我回迁社区已有的顺序修正,改为同步写盘并等待属性复制结束,保留标签原有的 16 项 EXIF 属性。
  2. 再释放用完的资源。 沿用已有清理方案,在收尾阶段关闭文件,释放图像打包器、像素对象和三个图像源。第一步先把执行顺序理清,才能避免释放仍在使用的对象。
  3. 最后加上阶段追踪。 这是本文重点增加的能力:给 Dart 调用提供可选开关,在解码、缩放、编码和释放前后记录标记,让一次总等待能拆成具体阶段。

后面出现的“基线版”和“清理版”都包含顺序修正与追踪,区别是有没有第二步的主动释放。基线版只供对照,并非未经修改的原标签;默认使用的 patched 清理版包含上述全部改动。

关闭追踪时,新增开关不会改变原有通道参数。但顺序修正和资源清理仍会执行,它们已经调整了返回路径的时机,不能把整份清理版理解成与原标签行为完全相同。

二、实测结果与诊断效果

2.1 先看诊断入口,再看两组运行材料

我在同一 Mate X7 模拟器上测试 patched 的诊断开关。两次构建的插件、Example、宿主配置及五项核心文件 SHA-256 一致,只切换编译参数 PGC_TRACE_ENABLED,分别关闭和开启追踪。

Example 把它传给 enableOhosTrace,并显示、记录开关状态。示例编译参数默认开启,插件公开 API 默认关闭。

两轮均用展开屏 2416×2210、API 26、镜像 7.0.0.107、debug 无签名构建、app+sched 和 32768 KB 缓冲区,按相同输入和参数顺序执行三次。期间还有其他任务使用模拟器,所以只核对本进程回执、标记和文件结果,不评估性能开销。

同一清理版的诊断选项完成调用自定义配对区间目标进程调度行三次输出大小
关闭,PID 25602302257均为 66754 字节
开启,PID 27221390 🟢↑2160均为 66754 字节

表1 开关对照

关闭时没有新增的 pgc14.compress.* 区间;开启时每次 30 个,全部配对,时基核对通过,三个请求总区间内,主线程状态的未知时长均为 0。两组输入和留存的第三次 JPEG 哈希相同,证明本样本输出一致。

两份 HAP 哈希、原始 trace、回执和输出均已留存。关闭后的零区间是预期结果,不是零耗时,也无法用于业务窗口求交。

图 3、图 4 在各自测量轮次结束采集后从模拟器窗口捕获,保留 Mate X7 设备外框,用于核对完成回执与开关标识,不用于判断动画帧率。

关闭诊断采样轮次的完成画面

图3 关闭结果

开启诊断采样轮次的完成画面

图4 开启结果

图 5 使用同一份开启诊断的冻结 HAP 从桌面单独拉起录制,展示生成输入图片、等待采样窗口及三次顺序压缩进入 complete 的运行过程。该演示轮次不开启系统 hitrace,连续窗口截屏不混入耗时样本。

开启诊断冻结包从桌面启动到完成三次压缩的运行过程

图5 运行过程

后面的耗时表来自另一组资源清理对照,采样标识为 baseline-02、patched-02a。两组分别以 com.example.pgc14tagbaseline、com.example.pgc14tagpatched 安装在同一 Mate X7,各运行一轮三次请求。

页面、输入、参数和构建模式一致,区别是资源清理层,两组均已开启追踪。六次输出均为 66754 字节;这些数据展示诊断用途,不是 HiTrace 接入前后的对照。

Example 检查输出长度后删除临时文件,只留第三次的 sample-output.jpg。生成图片、查长度和删文件均在秒表范围之外。

页面 complete 表示调用完成。下表的等待、跨度和运行时间分别取自回执、原始追踪文件及交集分析。

2.2 将接口等待与主线程运行时间分开

Dart 秒表包围 await compressImage,pgc14.compress.total 标记原生处理区间,sched 则记录窗口内的线程运行。宿主主线程按 tid = pid 识别,两组为 21473、26795,不同于 Flutter UI 或 Raster 线程。

组别与请求序号Dart 外层等待 ms原生 total 跨度 ms主线程 Running ms主线程 Sleeping(S) ms
基线组 #1 (任务号 1)39.73537.3276.84128.304
基线组 #2 (任务号 26)35.30334.4113.84827.519
基线组 #3 (任务号 51)40.73339.5796.49232.600
清理组 #1 (任务号 1)118.204108.71819.89585.884
清理组 #2 (任务号 31)55.34253.6388.10044.958
清理组 #3 (任务号 61)50.99149.9958.81440.661

表2 等待与运行

清理组首轮除表中的 Running、Sleeping 外,还有 2.939 ms 处于 Runnable、等待调度。较长的休眠窗口说明主线程并未全程持续占用 CPU。

但 Sleeping 也可能是在等锁或同步结果,不代表事件循环能立即响应。本次没有交互、Flutter 帧耗时或事件循环延迟测量,不能据此认定没有阻塞或卡顿。

图 6 对齐了 patched-02a 的阶段标记和线程状态。

实采阶段区间与宿主主线程状态的对齐视图

图6 调度泳道

三次请求位于系统单调时间 8219.9799—8220.2453 s,首轮释放长尾较明显。OS_FFRT_3_0(线程号 27633)在 decode、packing 窗口分别运行 31.330 ms、9.018 ms;OS_FFRT_3_1(线程号 27744)在 scale 窗口运行 8.954 ms。

图内 31.282 ms 是一根连续调度切片,31.330 ms 则是整个解码窗口内多根重叠切片的总和。

这些后台运行与异步图像处理相符,但脚本只有时间交集,没有关联 FFRT 任务编号或调用栈。不能把窗口内运行全算成该次图像任务的 CPU;进一步归因需要补采任务关联。

属性复制的 11.838 ms 窗口内,主线程 Running 为 8.036 ms,短切片较密集。逐项等待属性操作、返回 ArkTS 推进循环的控制流可以解释这类活动,但不能确定每段切片的具体工作。

Dart 等待比原生跨度多约 0.9—9.5 ms。这可能包含任务投递、结果回传和异步恢复,这里未分别计时,不能全归为 MethodChannel 序列化。

2.3 找到主要等待阶段与首轮释放长尾

表 3 列出调用边界间的墙钟跨度,包含运行和等待。

组别与请求序号decode 解码 msscale 缩放 mspacking 编码 mscopy_exif ms释放合计 ms
基线组 #121.8843.1693.2495.116未执行该清理
基线组 #224.5873.0572.6402.829未执行该清理
基线组 #323.4523.6983.9177.466未执行该清理
清理组 #132.3049.43310.07311.83838.384
清理组 #229.4626.2818.3816.2221.537
清理组 #327.7254.4125.1949.1942.200

表3 阶段跨度

“释放合计”包含图像打包器、像素对象、输入图像源,以及目标和来源两个 EXIF 图像源的释放。后两项已计入属性复制阶段,不能与全部阶段重复相加;基线没有执行清理,也不是零耗时释放。

六次请求中,五次最长的独立阶段是图像解码,对应 createPixelMap 调用。解码跨度 21.884—32.304 ms,宿主主线程只运行 0.166—0.529 ms,排查范围因而收窄到解码调用边界。

清理组首轮的像素对象释放耗时为 34.254 ms,超过解码的 32.304 ms,后两次却降至 0.616 ms、0.907 ms。分段记录使我看到:首轮较慢,还应检查资源释放。

这段释放窗口中,Running、Runnable、Sleeping 分别为 2.144 ms、1.807 ms、30.303 ms,状态覆盖无缺口。这说明首轮长等待落在 bmp.release() 调用边界内,且宿主主线程并未持续做 CPU 忙等;至于底层在等待图形缓冲区回收、跨线程同步还是系统服务响应,仍需结合后续调用栈确认。

2.4 华为畅享 100 Pro Max 真机复测与双环境核验

只在模拟器上看调度切片还不够,我又把相同的三份构建参数包通过本地调试签名装到 华为畅享 100 Pro Max 实体真机(型号 YEW-AL80,固件 7.0.0.100,系统回读 OpenHarmony-7.0.0.105)上,同样使用 hitrace --trace_begin -b 32768 app sched 采集真机阶段与内核线程状态。

在真机开关对照中,关闭诊断(PID 38685)完成三次压缩且自定义 pgc14.* 区间数为 0,目标进程调度记录 5667 行;开启诊断(PID 39891)产出 90 个完整配对区间(基线组 PID 39255 为 75 个),时基核对全部通过,六个 total 区间的宿主主线程未知状态时长均为 0,三次输出 JPEG 均为 65831 字节。表 4 列出畅享 100 Pro Max 真机清理组与基线组的阶段跨度对照。

真机组别与请求Dart 等待 mstotal 跨度 msdecode mspacking ms释放合计 ms
真机基线 #145.06542.93812.71213.668未执行该清理
真机基线 #226.10025.0336.6078.494未执行该清理
真机基线 #326.42625.3768.2967.615未执行该清理
真机清理 #182.08476.35125.65420.4371.988
真机清理 #230.02428.9189.2347.4311.232
真机清理 #332.65031.6329.9157.7391.382

表4 真机阶段

真机和模拟器的对比很有意思:模拟器清理组首轮出现了 34.254 ms 的 release_pixelMap 长尾等待,而在畅享 100 Pro Max 真机上,三次 release_pixelMap 分别只用了 1.070 ms、0.773 ms 和 0.911 ms,五项释放合计均不超过 2 ms。真机清理组首轮 76.351 ms 的原生跨度里,宿主主线程 Running 为 18.767 ms、Sleeping 为 57.081 ms,主要耗时落在 decode(25.654 ms)与 packing(20.437 ms)两个由 OS_FFRT 线程池异步处理的阶段。图 7 是畅享 100 Pro Max 真机经 DevEco Studio 投屏(顶栏设备序列号已做不可逆像素马赛克脱敏)完成三次顺序压缩后的画面。同一套分段标记在不同硬件上呈现不同的慢阶段分布,正好体现了先做阶段归因再针对性优化的意义。

华为畅享 100 Pro Max 真机运行开启诊断组完成三次压缩

图7 真机实测

2.5 固定双环境配置并检查采样完整性

模拟器与真机双环境的配置汇总在表 5。

环境与工具项归因案例实际配置
Flutter / Dart3.41.9+ohos-1.0.2 / 3.11.5
DevEco Studio26.0.0.865
HarmonyOS SDK26.0.0.105 Release(API 26)
模拟器环境Mate X7,arm64,4 核 CPU、4 GB 内存,镜像 7.0.0.107 / 系统 OpenHarmony-7.0.0.105
实体真机环境华为畅享 100 Pro Max(型号 YEW-AL80),固件 7.0.0.100 / 系统 OpenHarmony-7.0.0.105,arm64-v8a,USB 直连
构建模式debug(模拟器无签名调试 HAP;真机配本地调试签名)
系统追踪参数app、sched,缓冲区 32768 KB
离线工具trace_streamer 4.3.7、analyze_trace.py
原版公开标签0.0.6_1-ohos-1.0.0(包内版本 0.0.6+1)
补丁与诊断示例feat/ohos-hitrace-attribution 分支(核心补丁见 feat/ohos-hitrace-minimal)

表5 实测环境

资源清理对照采样时,未并行执行其他构建和性能实验。两组均开启追踪与 PGC14Trace 日志,这些操作有开销,debug 数字也不代表 release 性能。

根据实采日志和分析结果整理的数据面板

图8 日志与汇总

图 8 汇集设备日志与两组区间统计,日志中的任务号用于对应具体请求。

基线共 75 个区间,每次 25 个;清理组每次多五个释放阶段,共 90 个。原件中全部配对,六个请求总区间的状态覆盖完整,未知时长为 0;解析器另有两条告警,见 4.3。

三、原理与核心代码

3.1 从公开 Dart 入口传入诊断选项

图 9 展示处理链路:Example 发起公开调用,插件添加阶段标记,设备采集 app+sched,主机解析求交。图片能力仍在插件中,Example 只负责输入和验收。

公开调用到系统追踪分析的处理流程

图9 归因流程

下面是 package/lib/flutter_native_image.dart 中加入可选参数后的完整方法。

  static Future<File> compressImage(String fileName,
      {int percentage = 70,
      int quality = 70,
      int targetWidth = 0,
      int targetHeight = 0,
      bool enableOhosTrace = false}) async {
    var file = await _channel.invokeMethod("compressImage", {
      'file': fileName,
      'quality': quality,
      'percentage': percentage,
      'targetWidth': targetWidth,
      'targetHeight': targetHeight,
      // 显式诊断才发点,默认调用不新增通道参数。
      if (enableOhosTrace) 'ohosTrace': true,
    });

    return new File(file);
  }

关闭时不发送 ohosTrace 键,开启才传 true。Dart 未增加异常拦截,PlatformException 仍交给调用方;Android、iOS 原生实现未改,也未作运行回归。

插件诊断入口与宿主关键配置

图10 插件配置

图 10 根据源码整理入口与配置。插件仍为 har,诊断不新增敏感权限;沙箱示例无需相册权限,真实相册授权由应用处理。

宿主 compatibleSdkVersion、targetSdkVersion 均为 26.0.0。

3.2 配对异步区间,并收尾未结束的标记

阶段里有异步等待,我用 startAsyncTrace、finishAsyncTrace 标记起止位置,两端的名称和任务号要一致。同名任务同时执行时,编号必须不同,避免把两个请求的记录混在一起;串行执行则可以复用编号。

开始方法负责分配编号、写入标记,并保存编号与名称的对应关系。每次压缩各用一份记录,互不影响。这里递增分配编号,到上限再从头计数;这六次请求没有触及回绕,也没有重复编号。

下面只摘录追踪辅助类中负责结束和收尾的两个完整方法。代码中的 active 保存尚未结束的标记,LEVEL 指定输出级别;追踪和日志接口均来自 PerformanceAnalysisKit。

其余实现见公开复现工程中的 ohos/src/main/ets/components/plugin/CompressionTrace.ets。

end(id: number): void {
  const name = this.active.get(id);
  // 没有开始记录,或已经结束,就不用再处理。
  if (name === undefined) return;
  try {
    // 用开始时的名称和编号,告诉系统这一段结束了。
    hiTraceMeter.finishAsyncTrace(LEVEL, name, id);
  } catch (_error) {
    // 打点失败只记告警,别盖掉图片处理本身的结果。
    hilog.warn(0, 'PGC14Trace', 'trace_end_unavailable');
  } finally {
    // 清掉本地记录,避免下次收尾时重复结束。
    this.active.delete(id);
  }
}

close(): void {
  const ids = Array.from(this.active.keys());
  // 后开始的先结束,让内层阶段先于外层 total 收尾。
  for (let index = ids.length - 1; index >= 0; index--) {
    this.end(ids[index]);
  }
}

正常走完一个阶段就调用 end();图片处理途中抛错时,外层收尾逻辑会调用 close(),把剩下的标记逐个收尾。系统结束接口失败时,本地记录虽然已清掉,追踪文件里仍可能缺少结束事件,所以采集后还要检查起止是否配对。

追踪名称仅含固定前缀、阶段名,附加参数留空;日志只有阶段和任务号,不记录图片路径、内容或摄影位置。

3.3 在真实调用边界添加阶段标记

插件收到开启追踪的选项后,先记录本次请求的总区间,再依次给打开文件、创建图像源、读取尺寸、解码、缩放、JPEG 编码、写盘和属性复制这八个阶段打点。

标记紧贴每次原生调用的起止位置,不往处理流程里插入额外等待。这样既能看到整次请求花了多久,也能继续查看其中某一步。

清理版另记录五个对象释放,成功时清理完再返回路径。失败区间可能包含错误处理与清理,不能视为 API 的纯成功耗时。

属性复制阶段再为十六项 EXIF 属性分别记录子区间。程序逐项读取、写入,无论成功或失败都会结束当前标记,全部处理完后释放两个 EXIF 图像源。

输入 PNG 没有摄影 EXIF,读取缺失属性时会进入异常分支,再继续处理下一项。因此,这些子区间主要反映查询及失败返回,不代表十六项都复制成功;父区间还包含两个 EXIF 对象的释放。

资源释放和文件关闭函数遇到异常时会记日志,并继续清理其他资源;单次底层释放仍可能失败。

3.4 对齐时基,再计算调度交集

app 记录业务标记,sched 提供调度依据。两者写入同一 .ftrace,由 trace_streamer 4.3.7 导出 SQLite,再交给只读 analyze_trace.py。

脚本先独立解析原文起止事件,与数据库核对。完整时间转换和配对函数如下。

EVENT = re.compile(r'\s(?P<time>\d+\.\d+):\s+tracing_mark_write:\s+(?P<kind>[SF])\|(?P<pid>\d+)\|(?P<name>[^|]+)\|(?P<cookie>\d+)')


def nanoseconds(value: str) -> int:
    whole, fraction = value.split('.', 1)
    return int(whole) * 1_000_000_000 + int(fraction.ljust(9, '0')[:9])


def read_spans(path: Path, pid: int, prefix: str) -> tuple[list[dict], list[str]]:
    pending: dict[tuple[str, int], int] = {}
    spans: list[dict] = []
    warnings: list[str] = []
    for number, line in enumerate(path.read_text().splitlines(), 1):
        event = EVENT.search(line)
        if event is None or int(event['pid']) != pid:
            continue
        name = event['name'].removeprefix('H:')
        if not name.startswith(prefix):
            continue
        key = (name, int(event['cookie']))
        timestamp = nanoseconds(event['time'])
        if event['kind'] == 'S':
            if key in pending:
                warnings.append(f'重复开始,原开始被保留:line={number}, key={key}')
            else:
                pending[key] = timestamp
        elif key not in pending:
            warnings.append(f'缺少开始:line={number}, key={key}')
        else:
            start = pending.pop(key)
            if timestamp < start:
                warnings.append(f'结束早于开始:line={number}, key={key}')
                continue
            spans.append({'name': name, 'task_id': key[1], 'start_ns': start,
                          'end_ns': timestamp, 'wall_ns': timestamp - start})
    for key in pending:
        warnings.append(f'缺少结束:key={key}')
    return sorted(spans, key=lambda item: item['start_ns']), warnings

原件时间戳有六位小数,即 1 μs 分辨率。数据库的部分端点与原始文本换算值相差 1 ns。若文本出现重复开始、缺少开始、结束早于开始或尾部未闭合区间,解析函数会逐条记录告警,避免残缺记录直接混入后续统计。

分析脚本按原件实际精度核对两边的起止时间,同时保留开始、结束时间的纳秒偏差,避免把舍入尾差当成时钟域错位,也不为了对齐而改写调度数据。

只有全部业务区间在原件精度下对齐,且存在目标进程的调度记录,脚本才计算 CPU 交集。如果缺少这些条件,结果为 null 并附告警;没有数据不能写成 0 ms。

业务窗口为 [ s t a r t , e n d ] [start,end] [start,end],调度切片起点、长度为 t i t_i ti​、 d i d_i di​,交集为:

Overlap = ∑ max ⁡ ( 0 , min ⁡ ( end , t i + d i ) − max ⁡ ( start , t i ) ) \text{Overlap}=\sum\max\bigl(0,\min(\text{end},t_i+d_i)-\max(\text{start},t_i)\bigr) Overlap=∑max(0,min(end,ti​+di​)−max(start,ti​))

这个求交会截断跨过业务边界的调度切片。某个线程在阶段开始前已经运行,或在阶段结束后继续执行其他任务,都只计入窗口内的部分。不过,时间重叠只建立了时间关系,并没有建立任务因果关系;同一进程中的其他工作落在窗口内,也会被统计。

对主线程状态,脚本先求区间并集得到覆盖长度,再用墙钟跨度减去覆盖长度,单列为未知时长(unknown)。状态重叠也单独报告。

这样,缺失切片不会被当成休眠,重复区间也不会被当成额外运行。

进程总 CPU 还可能大于墙钟跨度。例如清理组第 2 次缩放跨度为 6.281 ms,该窗口内 FFRT 线程与主线程的运行交集合计达到 6.590 ms。多核允许它们同时运行,这不矛盾;但这些运行时间只能称为窗口内进程线程的累计值,不能全部归给缩放函数,更不能当作单核利用率。

四、踩坑与解释边界

4.1 保留方向不一致的历史数据

开发分支 master 的三组、每组三十次历史测量中,清理版中位数第一组较长,后两组较短,不能稳定归因为清理影响。

这些样本与公开标签的六次请求分别保存,表 2、表 3 只用后者,不跨版本合并。一次释放长尾也不足以推出普遍规律。

4.2 将工具失败与插件失败分开

采集中遇到的工具错误如下。

现象核查原因处理方式
页面完成,脚本仍等到超时hdc shell 参数中的日志正则含未转义的竖线,被远端 shell 解释为管道按 PID 取回日志,在主机 Python 中过滤
安装返回 9568288模拟器存储空间不足清理两个已归档的旧自建实验应用,另开 patched-02a 采样
解析器报告两条 invalid_data原件末尾的时钟同步标记不被该分支识别用独立副本验证,正式分析保留原件
构建报 00306003当前使用的 Hvigor 不接受中文工程路径排除缓存后复制到 ASCII 路径构建

表6 工具排障

未转义的 TRACE_BATCH|PGC14Trace 导致首轮漏读实际完成回执;安装失败的 patched-02 则没有测量样本。两者均不计入正式结果,也不算插件失败。

修复后另开 patched-02a,保留失败记录。复现时也应使用新运行标识,避免覆盖原件。

4.3 核实解析告警,不修改正式原件

两份原件的写入/留存计数为 49481/49481、50245/50245,未见缓冲覆写;数据库却各报两条 tracing_mark_write invalid_data。

核查发现文件末尾有 trace_event_clock_sync,如下。

hitrace-27773   (  27773) [002] ....  8220.948103: tracing_mark_write: trace_event_clock_sync: realtime_ts=1794259562118
hitrace-27773   (  27773) [002] ....  8220.948110: tracing_mark_write: trace_event_clock_sync: parent_ts=8220.948242

这两行不符合常规 B|pid|...、S|pid|... 格式。我仅在诊断副本删除它们重跑,错误归零,本篇业务区间的名称、任务号、起点和时长均不变。

五、结论与可直接复用的建议

5.1 这次扩展解决了什么

回到图片处理页面,我原先只能知道一次压缩多久返回,现在能把等待定位到解码、编码、属性复制或资源释放。六次请求里,五次最长的独立阶段是解码,另一次出现了 34.254 ms 的像素对象释放等待,后续排查因此有了具体方向。

这项能力适合已经能完成图片处理、却不知道耗时落在哪里的 Flutter 鸿蒙项目。插件负责提供阶段标记,系统调度记录帮助判断窗口内线程的运行情况;这些结果不直接证明页面流畅,也不代表压缩速度已经提高。

5.2 在项目里直接使用

💡 需要这项能力时,可以直接依赖已经写好的 flutter_native_image 扩展版,继续使用原有图片压缩入口。下面固定为本文使用的实现,在项目的 pubspec.yaml 中加入依赖即可,无需自己编写鸿蒙阶段标记。

dependencies:
  flutter_native_image:
    git:
      url: https://atomgit.com/abaoge/flutter_native_image.git
      ref: 4c876b9ce0fe29354e88bc95ee5ff0f93168d2cc

这个提交来自公开的 feat/ohos-hitrace-attribution 分支,仓库根目录就是插件。已有同名依赖时替换原来的配置,再运行 flutter pub get,用鸿蒙 Flutter 工具链重新构建应用,让原生实现一起更新。

下面是业务侧调用的一个完整函数。传入应用能够读取的本地图片路径,等待返回后即可取得输出文件,交给自己的预览、保存或上传流程。

import 'dart:io';
import 'package:flutter_native_image/flutter_native_image.dart';

Future<File> compressPhoto(String imagePath) {
  return FlutterNativeImage.compressImage(
    imagePath,
    percentage: 70,
    quality: 70,
    // 平时保持关闭;需要排查这次压缩的等待时,再改为 true。
    enableOhosTrace: false,
  );
}

缩放比例和编码质量是两项不同的参数,示例都取 70。业务中按图片用途选择参数,并实际比较输入、输出大小;本文的网格 PNG 转为 JPEG 后反而变大,不能把“调用压缩接口”理解成文件一定缩小。

页面自行处理加载提示、重复点击和失败状态。插件返回文件后,后续预览或上传仍由应用负责;如果读取相册图片,也需要应用完成相应授权和路径准备。

5.3 按需使用诊断能力

日常压缩保持默认关闭。需要排查某次等待时,将调用中的 enableOhosTrace 改为 true,插件便会为这次请求记录阶段起止。开启诊断不会自动启动整机采集,也不会上传图片或追踪记录。

已有 HiTrace 采集流程的项目可以直接查看这些阶段。需要用到配套分析工具时,仓库使用说明提供了完整命令和同款 Example。

如果编译时提示没有诊断参数,先检查是否仍依赖原版包。更新依赖后要重新构建、安装应用,单独刷新 Flutter 页面不会替换 HAP 中的原生插件。关闭追踪也只停止新增打点,顺序修正和资源清理仍然执行。

这里提供的是本文运行过的标签派生版本,保留 16 项 EXIF 属性。面向社区仓库 CPF-Flutter/flutter_native_image 主分支整理的核心改动保留了该分支的 20 项属性与裁剪修改,两份代码应分开辨认;依赖先固定提交,升级时再查看相应变更说明。

我在表 5 所列的 Mate X7 模拟器与华为畅享 100 Pro Max 真机双环境运行过这份实现。Android、iOS 原有代码保留,但这次没有做其他平台的运行回归;业务中的大照片、复杂 EXIF 和异常输入仍需按实际场景处理。

小结

需要排查图片处理等待时,可以直接在现有压缩调用上开启诊断选项,沿着具体阶段继续查。无论是在模拟器揪出首轮像素对象释放长尾,还是在畅享 100 Pro Max 真机确认耗时主要落在解码与编码阶段,接入仍是熟悉的 Flutter 图片接口,阶段记录都由鸿蒙插件按需完成 🎉。

参考资料

Logo

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

更多推荐