【OpenHarmony/HarmonyOs 】CheckMe 存储分析与容量校准实践:statvfs 多路径探测和 Preferences 持久化

本文基于 CheckMe 项目的存储分析模块展开,聚焦存储容量读取、分区统计、多路径探测和用户校准,适合用于讲解 HarmonyOS 第三方应用如何在权限边界内做可信存储展示。

存储信息看起来简单:总容量、已使用、可用空间、使用率。

但在 HarmonyOS 第三方应用里,存储读取并不等于“直接拿到整机物理容量”。很多时候,应用只能读取到某个路径或分区的统计信息,这就会出现一个问题:

系统设置里显示 256GB,但应用 API 读出来的可能是某个分区容量。

CheckMe 为了解决这个问题,设计了两层方案:

  • 使用 statvfs 做多路径探测
  • 允许用户手动校准整机容量,并持久化保存

这篇文章就专门拆解这个模块。💾


一、为什么存储容量读取需要工程化

存储分析页面需要展示:

  • 总容量
  • 已使用
  • 可用空间
  • 使用率
  • 数据来源说明

页面代码中有明确注释:

/** StorageInfo.totalStorage / usedStorage / availableStorage 已为 GB 数值,非字节 */
private formatStorageGb(gb: number | undefined): string {
  if (gb === undefined || gb < 0) {
    return '--';
  }
  return gb.toFixed(2) + ' GB';
}

这里强调了一个点:服务层已经把字节转换成 GB,页面只负责格式化展示。

这就是工程化分层:

StorageInfoReader:读取底层字节数据
DeviceInfoService:转换成业务模型
StorageAnalysis:负责页面展示

二、页面展示:不要隐藏数据来源

StorageAnalysis.ets 展示分区容量:

this.InfoRow('总容量', this.formatStorageGb(this.storage.totalStorage))
this.InfoRow('已使用', this.formatStorageGb(this.storage.usedStorage))
this.InfoRow('可用', this.formatStorageGb(this.storage.availableStorage))
this.InfoRow('使用率', this.formatPercent(this.storage.usagePercent))

同时,如果有数据来源说明,也会展示出来:

if (this.storage.storageSourceLabel !== undefined &&
    this.storage.storageSourceLabel !== '') {
  Text('数据来源:' + this.storage.storageSourceLabel)
    .fontSize(12)
    .fontColor($r('app.color.text_tertiary'))
    .width('100%')
}

这非常重要。

存储数据可能来自:

  • 系统分区统计
  • 应用沙箱路径
  • 用户手动校准
  • 多路径探测结果

如果不说明来源,用户看到和系统设置不一致时,会以为应用有 bug。


三、statvfs 多路径探测

底层读取在 StorageInfoReader.ts 中:

import statvfs from '@ohos.file.statvfs';

private static readonly STATVFS_CANDIDATE_PATHS: string[] = [
  '/data/storage/el2/base',
  '/data/storage/el1/base',
  '/data',
  '/storage/emulated/0',
  '/data/storage'
];

项目不会只读一个路径,而是尝试多个候选路径。

原因是:不同路径可能落在不同分区或沙箱视角下。如果只读一个路径,结果可能偏小。


四、选择 total 最大的合法结果

多路径探测逻辑如下:

public async getDataPartitionStat(): Promise<{ total: number; free: number }> {
  let bestTotal: number = 0;
  let bestFree: number = 0;
  const paths: string[] = StorageInfoReader.STATVFS_CANDIDATE_PATHS;

  for (let i: number = 0; i < paths.length; i++) {
    const p: string = paths[i];
    try {
      const total: number = await statvfs.getTotalSize(p);
      const free: number = await statvfs.getFreeSize(p);

      if (total > 0 && free >= 0 && free <= total) {
        if (total > bestTotal) {
          bestTotal = total;
          bestFree = free;
        }
      }
    } catch (_e) {
      // ignore
    }
  }

  return { total: bestTotal, free: bestFree };
}

这段代码有几个稳健点:

  • 单个路径失败不影响整体
  • total 必须大于 0
  • free 不能小于 0
  • free 不能大于 total
  • 在合法结果中取 total 最大的一组

这比直接读 /data 更稳。


五、为什么还需要用户校准

即使做了多路径探测,第三方应用读到的也可能仍然不是系统设置里的整机容量。

所以项目提供了容量校准能力。

StorageCalibrationStore 使用 Preferences 保存用户输入的整机容量:

const PREF_STORE_NAME = 'checkme_user_prefs';
const KEY_STORAGE_CALIBRATION_GB = 'storageTotalCalibrationGB';

保存逻辑:

static async saveCalibrationGb(gb: number): Promise<boolean> {
  const ctx: common.UIAbilityContext | null =
    StorageCalibrationStore.contextHolder;
  if (ctx === null) {
    return false;
  }

  try {
    const pref = await dataPreferences.getPreferences(ctx, PREF_STORE_NAME);
    await pref.put(KEY_STORAGE_CALIBRATION_GB, gb);
    await pref.flush();
    return true;
  } catch (e) {
    console.error('StorageCalibrationStore.saveCalibrationGb failed: ' + JSON.stringify(e));
    return false;
  }
}

Preferences 适合保存这种轻量配置:

  • 数字小
  • 读取频繁
  • 不需要复杂数据库
  • 和用户设置强相关

六、读取校准值

读取逻辑:

static async getCalibrationGb(): Promise<number | undefined> {
  const ctx: common.UIAbilityContext | null =
    StorageCalibrationStore.contextHolder;
  if (ctx === null) {
    return undefined;
  }

  try {
    const pref = await dataPreferences.getPreferences(ctx, PREF_STORE_NAME);
    const raw: dataPreferences.ValueType =
      await pref.get(KEY_STORAGE_CALIBRATION_GB, 0);

    if (typeof raw !== 'number') {
      return undefined;
    }
    if (raw <= 0) {
      return undefined;
    }
    return raw;
  } catch (_e) {
    return undefined;
  }
}

这里做了两层校验:

  • 类型必须是 number
  • 数值必须大于 0

这可以避免无效配置污染存储展示。


七、校准后的容量如何计算

存储读取时,如果用户提供了校准值,会按当前分区使用比例映射到校准总量:

if (paramTotalGB !== undefined && paramTotalGB > 0) {
  const calibratedBytes: number =
    Math.round(paramTotalGB * 1024 * 1024 * 1024);

  if (partitionTotal > 0) {
    const usageRatio: number = partitionUsed / partitionTotal;
    const usedScaled: number = Math.round(calibratedBytes * usageRatio);
    const freeScaled: number = Math.max(0, calibratedBytes - usedScaled);

    return {
      total: calibratedBytes,
      free: freeScaled
    };
  }
}

这个设计很有意思。

它不是简单把总容量改成用户输入值,然后直接套用原始 free。因为原始 free 可能来自分区视角,和整机容量不一致。

项目采用的是:

分区真实使用比例 -> 映射到用户校准总量

这样展示结果更接近系统设置中的整机容量。


八、存储分析页面的用户提示

页面底部有说明:

Text('说明:第三方应用仅能读取分区统计,与「设置」中整机存储可能略有差异。')
  .fontSize(12)
  .fontColor($r('app.color.text_tertiary'))
  .width('100%')

这句非常重要。

开发设备工具类应用时,不要假装自己能拿到所有系统级数据。第三方应用有权限边界,某些数据只能近似或通过用户校准。

把限制告诉用户,反而能提升可信度。


九、推荐扩展方向

如果继续完善存储分析,我建议加这些能力:

  • 存储来源标签更明确
  • 展示是否已校准
  • 提供重新校准入口
  • 显示校准时间
  • 支持恢复默认分区统计
  • 对异常输入做范围校验

比如:

interface StorageCalibrationInfo {
  calibratedTotalGb: number;
  calibratedAt: number;
  source: 'user_input' | 'statvfs';
}

这样用户能知道当前容量展示到底来自系统探测还是手动校准。


十、文章小结

CheckMe 的存储分析模块解决了一个很实际的问题:第三方应用读取存储容量时,系统 API 结果和用户认知可能不完全一致。

项目采用了两步方案:

  • 先用 statvfs 多路径探测,取最合理的分区统计
  • 再允许用户手动校准整机容量,并通过 Preferences 持久化

最终效果是:

  • 数据更稳定
  • 来源更透明
  • 用户可校准
  • 展示更接近系统设置
  • 不突破第三方应用权限边界

最后一句总结:设备工具类应用最难的不是显示数字,而是告诉用户这个数字从哪里来、为什么可信、什么时候需要校准。 📊


参考资料

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐