HarmonyOS 鸿蒙Next原生应用占用空间管理

发布于 1周前 作者 songsunli 来自 鸿蒙OS

HarmonyOS 鸿蒙Next原生应用占用空间管理
<markdown _ngcontent-amc-c237="" class="markdownPreContainer">

使用接口

接口

接口能力

storageStatistics.getCurrentBundleStats

获取当前应用的存储空间大小 (包含缓存文件,安装文件等)

statvfs.getFreeSize

查询设备剩余可用空间

statvfs.getTotalSize

获取设备总空间 (排除系统占用空间)

场景一:计算应用缓存大小,并进行清理

1.可以通过storageStatistics.getCurrentBundleStats接口获取缓存大小。

应用的缓存文件因为级别和加密类型不同,会保存在以下目录中。例如:app级别,加密类型为el1的缓存会保存到/data/storage/el1/base/cache目录下。

/data/storage/el1/base/cache

/data/storage/el1/base/haps/entry/cache

/data/storage/el2/base/cache

/data/storage/el2/base/haps/entry/cache

这四个目录可以通过以下接口获取,其中el1与el2因为分区不同,需要切换加密等级才能获取到:

let moduleContext: common.Context;
moduleContext = this.context.createModuleContext('entry');
console.log('moduleContext + el2: '+moduleContext.cacheDir);
console.log('UIAbilityContext + el2: '+this.context.cacheDir);
moduleContext.area = contextConstant.AreaMode.EL1;
console.log('moduleContext + el1: '+moduleContext.cacheDir);
this.context.area = contextConstant.AreaMode.EL1;
console.log('UIAbilityContext + el1: '+this.context.cacheDir);
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

storageStatistics.getCurrentBundleStats((err: BusinessError, bundleStats: storageStatistics.BundleStats) => {
  if (err) {
    console.error(`Invoke getCurrentBundleStats failed, code is ${err.code}, message is ${err.message}`);
  } else {
    console.info(`Invoke getCurrentBundleStats succeeded, cacheSize is ${bundleStats.cacheSize}`);
  }
});
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

2.清除缓存,缓存会保存在上述四个文件夹中,可以递归删除

webview缓存(/data/storage/el2/base/cache/web/Cache)包含于在上述文件夹中,也可以调用WebviewController.removeCache单独清理web缓存。

//通过Context.cacheDir获取所有缓存路径
let cacheDir : string[] =[];
let moduleContext: common.Context;
moduleContext = getContext().createModuleContext('entry');
cacheDir.push(moduleContext.cacheDir);
cacheDir.push(getContext().cacheDir);
moduleContext.area = contextConstant.AreaMode.EL1;
getContext().area = contextConstant.AreaMode.EL1;
cacheDir.push(moduleContext.cacheDir);
cacheDir.push(getContext().cacheDir);

for (let i = 0; i < cacheDir.length; i++) { let cache = cacheDir[i]; let exist = fs.accessSync(cache); if (exist) { try { fs.rmdirSync(cache); } catch (err) { console.log(err); } } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

场景二:对文件夹中图片进行展示,并选择性清理(这里只对图片做了筛选,同理,还可以加上视频等)

1.制定图片过滤规则,过滤出对应格式(例如:jpg,png)的图片。

let listFileOption: ListFileOptions = {
  recursion: true,//列出子目录
  listNum: 0,// 列出文件名数量。可选,当设置0时,列出所有文件,默认为0。
  filter: {
    suffix: ['.jpg','.png'],//文件后缀
  }
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

2.将文件夹路径与listFile获取到的路径拼接起来,得到图片完整沙箱路径。

let path: string[] = [];
let fileNames = fs.listFileSync(dirPath, listFileOption)
for (let i = 0; i < fileNames.length; i++) {
  let filePath = dirPath + fileNames[i]
  console.log(filePath);
  path[i] = filePath
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

3.根据沙箱路径将图片转换为pixelmap。

function Picture(mediaPath: string): image.PixelMap {
  let options: image.SourceOptions = {
    sourceSize: {
      width: 100,
      height: 100
    },
    sourceDensity: 0
  }
  let decodingOptions: image.DecodingOptions = {}

//获取文件 let file = fs.openSync(mediaPath, fs.OpenMode.READ_ONLY); fs.copyFileSync(file.fd, getContext().filesDir + ‘/’ + file.name + ‘1’); //通过传入文件描述符来创建图片源实例 let imageSource: image.ImageSource = image.createImageSource(file.fd, options); let imageInfo: image.ImageInfo = imageSource.getImageInfoSync(); //这里只取图片中间一部分 if (imageInfo.size.height > imageInfo.size.width) { let a = (imageInfo.size.height - imageInfo.size.width) / 2 decodingOptions.desiredRegion = { size: { width: imageInfo.size.width, height: imageInfo.size.width }, x: 0, y: a } } else { let a = (imageInfo.size.width - imageInfo.size.height) / 2 decodingOptions.desiredRegion = { size: { width: imageInfo.size.height, height: imageInfo.size.height }, x: a, y: 0 } } let pixelMap: image.PixelMap = imageSource.createPixelMapSync(decodingOptions); return pixelMap } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

4.将图片展示出来,并删除指定图片,释放空间。

(1)单张图片删除。

定义长按手势,可以通过长按图片拉起菜单,选择删除指定图片。

[@Builder](/user/Builder)
MenuBuilder(path: string) {
  Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
    Text('删除')
      .fontSize(20)
      .width(100)
      .height(50)
      .textAlign(TextAlign.Center)
      .onClick(() => {
        try {
          fs.unlinkSync(path);
          let i = this.mediaPath.indexOf(path);
          this.mediaPath.splice(i, 1);
        } catch (err) {
          console.log(err);
        }
      })
  }.width(100)
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

使用Grid将图片排列展示,并通过bindContextMenu给image组件绑定菜单,长按拉起删除选择框,并实时展示删除后剩余图片。

Grid() {
  ForEach(this.mediaPath, (item: string) => {
    GridItem() {
      Image(picture(item))
        .bindContextMenu(this.MenuBuilder(item), ResponseType.LongPress)
        .objectFit(ImageFit.Fill)
        .autoResize(false)
    }
  })
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

(2)一键删除所有图片。

点击一键删除后,会拉起一个弹窗确认,这里弹窗使用的promptAction.openCustomDialog。

Button('一键删除')
  .onClick(() => {
    let uiContext = this.getUIContext();
    let promptAction = uiContext.getPromptAction();
    let contentNode = new ComponentContent(uiContext, wrapBuilder(BuildText), this.mediaPath);
    this.content = contentNode;
    try {
      promptAction.openCustomDialog(contentNode
        , {
          showInSubWindow: false,
          offset: { dx: 5, dy: 5 },
          onWillDisappear: () => {
            this.mediaPath = FindAll(getContext().filesDir)
          }
        }
      )
    } catch (error) {
      let message = (error as BusinessError).message;
      let code = (error as BusinessError).code;
      console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
    }
  })
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

当前弹窗组件返回的reason中,暂不支持按钮关闭,故需自行实现。

自定义一个变量,使用[@Watch](/user/Watch)去监测,一旦变量发生改变则触发回调关闭弹窗。

//自定义回调,当dialogState发生变化,关闭弹窗
[@LocalStorageLink](/user/LocalStorageLink)('CustomDialogInfo') [@Watch](/user/Watch)('onChange') dialogState: number = 0;
......
// 自定义回调,关闭弹窗
onChange() {
  try {
    this.uiContext.getPromptAction().closeCustomDialog(this.content);
  } catch (error) {
    let message = (error as BusinessError).message;
    let code = (error as BusinessError).code;
    console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
  }
}
......
[@Builder](/user/Builder)
function BuildText(params: string[]) {
  Column({ space: 10 }) {
    Text('请确认是否删除')
      .fontSize(30)
      .textAlign(TextAlign.Center)
      .fontWeight(FontWeight.Bold)
      .padding(8)
    Text('注意:一旦确认删除后,文件将不可恢复')
      .fontSize(20)
      .textAlign(TextAlign.Center)
      .padding({ left: 14, right: 14 })
    Flex({ justifyContent: FlexAlign.SpaceAround }) {
      Button('取消').onClick(() => {
        //改变dialogState,触发onChange回调,关闭弹窗
        let dialogInfo: SubscribedAbstractProperty<number> = storage.link('CustomDialogInfo');
        let dialogState = dialogInfo.get();
        console.log('取消', dialogInfo.get());
        dialogInfo.set(++dialogState);
      }).backgroundColor(0xffffff).fontColor(Color.Black)
      Button('确认')
        .onClick(() => {
          if (params!) {
            for (let i = 0; i < params.length; i++) {
              const filePath = params[i];
              try {
                fs.unlinkSync(filePath);
              } catch (err) {
                console.log(err);
              }
            }
          }
          //改变dialogState,触发onChange回调,关闭弹窗
          let dialogInfo: SubscribedAbstractProperty<number> = storage.link('CustomDialogInfo');
          let dialogState = dialogInfo.get();
          dialogInfo.set(++dialogState);
        }).backgroundColor(0xffffff).fontColor(Color.Black)
    }.margin({ bottom: 10 })
  }.backgroundColor('#FFF0F0F0')
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

常见问题

Q:为什么通过查询到设备总空间和设置中显示的不同?

A:statvfs.getTotalSizeSync获取到的是data分区大小,三方应用仅能查询到自己能够使用的空间大小,暂无查询手机总内存大小的接口。

Q:能够查询外卡空间大小吗?

A:暂不支持。

Q:为什么有些中文文件名的文件搜不到?

A:当前HarmonyOS文件系统仅支持utf-8格式,gbk格式的中文名的文件通过其它方式存入文件系统中,名称会乱码。

Q:为什么删除文件夹会报错:Directory not empty。

A:fs.rmdir是递归删除,会删除该文件夹以及子文件夹中的所有文件,但是当其子目录中有高权限的文件时,调用的接口无法删除此文件,导致无法删除此文件夹,报错:Directory not empty。

</markdown>
1 回复

HarmonyOS 鸿蒙Next原生应用占用空间管理,可通过系统API如storageStatistics.getCurrentBundleStats获取应用存储空间详情,包括缓存和安装文件等。同时,系统支持查询设备剩余可用空间及总空间(排除系统占用)。对于缓存管理,可指定清理策略,如通过Context.cacheDir获取缓存路径并清理。HarmonyOS NEXT还优化了云空间利用,提升数据安全性与空间管理效率。如果问题依旧没法解决请加我微信,我的微信是itying888。

回到顶部