HarmonyOS 文件操作不用愁:沙箱机制解析 + 可抄代码,零基础上手

138 阅读8分钟

✨家人们记得点个账号关注,会持续发布大前端领域技术文章💕 🍃

一、应用沙箱:鸿蒙应用的 “安全文件柜”​

1. 核心简介​

应用沙箱是鸿蒙系统为保障数据安全设计的隔离存储机制,每个应用都会获得专属的 “沙箱目录”,实现三大核心目标:​

  • 数据隔离:应用仅能访问自身文件和少量必需系统文件,避免被其他应用篡改或读取​
  • 安全防护:防止恶意路径穿越,保护用户隐私数据​
  • 规范存储:明确不同类型文件的存储路径,避免文件混乱​

简单说,沙箱就像给应用分配了一个独立的 “文件柜”,柜子里的空间分为两部分:​

  • 应用文件目录(可读写,随应用卸载清理)​
  • 系统文件目录(只读,提供应用运行必需资源)​

若需访问照片、文档等用户文件,需通过系统 API + 用户授权才能操作,进一步强化安全性。​

img

编辑

2. 为什么必须学?​

  • 开发必备:下载文件(音乐、PDF、头像)、缓存数据(接口缓存、图片缓存)都需要指定正确路径​
  • 避坑关键:不同目录的清理规则不同(如临时目录退出即删),选错路径会导致数据丢失​
  • 面试加分:文件操作是鸿蒙开发高频考点,沙箱机制是核心知识点

二、核心目录详解:不同文件该存哪?​

鸿蒙应用沙箱的应用文件目录包含 4 个核心子目录,按功能精准划分:​​

目录名​核心作用​生命周期​典型使用场景​
cache​应用缓存目录​超配额 / 系统空间不足时清理;卸载清理​接口数据缓存、图片缓存、临时下载文件​
preferences​用户首选项存储​卸载清理​应用配置(如主题、字体大小、登录状态)​
temp​临时文件目录​应用退出后清理​数据库临时文件、日志缓存、安装包缓存​
files​长期保存文件目录​卸载清理​离线数据、用户下载的永久文件(如文档)​

developer.huawei.com/consumer/cn…

img

编辑

🤔关键思考:上传文件该存哪?​

答案:优先选 cache 或 temp 目录!上传文件多为临时使用,上传完成后可按需删除,避免占用长期存储。​

三、实战核心:文件操作 API 详解​

鸿蒙提供 @ohos.file.fs(文件管理)和 @ohos.request(上传下载)核心 API,以下是高频场景的完整实战代码(含同步 / 异步方案)。​

1. 核心语法说明​

  • 上下文(Context):获取应用目录的关键,通过 getContext(this) 获取​
  • 核心流程:打开文件 → 操作文件(读 / 写)→ 关闭文件(异步需手动关闭)​
  • 注意事项:实战优先用异步 API(Promise/callback),避免阻塞主线程导致 UI 卡顿;同步 API 仅用于演示可读性​
context上下文    1-退出app、2-获取应用的相关信息
- 窗口 this.text
- 界面 getContext(this)  


import { fileIo as fs } from '@kit.CoreFileKit';

// 一 获取文件实例/对象(打开文件或目录 )     参数1-打开的文件沙箱路径、参数2-操作模式
const file = fs.openSync(沙箱目录/磁盘文件, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); 

// 二 操作writeSync/readSync/unlinkSync()   留心删除文件咱们不需要写文件路径,而是写 文件实例/对象的唯一标识  file.fd 数值型
fs.writeSync(file.fd, "hello, world");

2. 基础文件操作示例(创建 / 读取 / 删除)

import { fileIo as fs } from '@kit.CoreFileKit'

@Entry
@Component
struct Index {
  private filePath:string = getContext(this).cacheDir+'/jz.txt'

  @State content:string = ''
  build() {
    Column() {
      Button('创建文件').onClick(() => {
          // 1 创建文件对象
          const file = fs.openSync(getContext(this).cacheDir+'/jz.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
          // 2 写入文件
          const result = fs.writeSync(file.fd, 'hello baidu')
          console.log('返回结果:', result)  // 返回实际写入的数据长度(单位:字节)
      })


      Text('读取内容:' + this.content)
      Button('读取文件 & 展示').onClick(async () => {
          // # 方案1
          // 1 读取
          // const file = fs.openSync(getContext(this).cacheDir+'/jz.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
          // 2 修改响应式数据
          // const result = fs.readText(getContext(this).cacheDir+'/jz.txt')
          // console.log('查看结果1:', await result)

          // # 方案2
          // let arrayBuffer = new ArrayBuffer(1024);
          // let readOptions: ReadOptions = {
          //   offset: 0,
          //   length: arrayBuffer.byteLength
          // };
          // let readLen = fs.readSync(this.file.fd, arrayBuffer, readOptions);
          // let buf = buffer.from(arrayBuffer, 0, readLen);
          // console.info("查看结果2: " + buf.toString());
      })

      Button('删除文件').onClick(() => {
          fs.unlinkSync(this.filePath)
      })

    }
  }
}

3. 封装通用文件工具类(企业级实战)​

重复写文件操作代码效率低,封装 FileUtil 工具类,支持复用:

import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit'

export type FsTestType = string | number
export type FsType = fs.File  // 给fs.File类型起一个别名 方便我使用

class FileUtil {

  private file: fs.File | undefined = undefined

  // const res = await fileUtil.open()
  open(filePath:string):Promise<fs.File | undefined> {
    return new Promise((resolve, reject) => {
      fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE).then((file: fs.File) => {
        console.info("file fd: " + file.fd);
        this.file = file
        resolve(file)
        // fs.closeSync(file);
      }).catch((err: BusinessError) => {
        console.error("open file failed with error message: " + err.message + ", error code: " + err.code);
        reject(undefined)
      });
    })
  }

  // const res = await fileUitl.write(fn, 'hello')
  write(content:string ):Promise<number>  { // number返回写入的字节长度
    return new Promise((resolve, reject) => {
      fs.write(this.file?.fd, content, (err: BusinessError, writeLen: number) => {
        if (err) {
          console.error("write data to file failed with error message:" + err.message + ", error code: " + err.code);
          reject(0)
        } else {
          console.info("write data to file succeed and size is:" + writeLen);
          resolve(writeLen)
        }
        // fs.closeSync(this.file);
      });
    })
  }

  read(filePath:string)  {
    const result = fs.readText(filePath)
    // console.log('查看结果1:', await result)
    return result  // 默认数据读出来就是promise 默认的
  }

  // const res:boolean = fileUitl.unlink(路径)
  unlink(filePath:string):Promise<boolean> {
    return new Promise((resolve, reject) => {
      fs.unlink(filePath).then(() => {
        console.info("remove file succeed");
        resolve(true)
      }).catch((err: BusinessError) => {
        reject(false)
        console.error("remove file failed with error message: " + err.message + ", error code: " + err.code);
      });
    })
  }

}

export const fileUtil = new FileUtil()

使用

import { fileUtil } from '../utils/FileUtil'

@Entry
@Component
struct Index {
  private filePath:string = getContext(this).cacheDir + '/jx3.txt'

  build() {
    Column() {
      Button('创建文件').onClick(async () => {
        await fileUtil.open(this.filePath)  // 切记切记切记创建就得放着 避免删除后 不能生成新的fd
        const writeLen = await fileUtil.write('hello sljz')
        console.log('写入状态:', writeLen)
      })

      Button('读取文件').onClick(async () => {
        const content = await fileUtil.read(this.filePath)
        console.log('写入状态:', content)
      })

      Button('删除文件').onClick(async () => {
        const bool = await fileUtil.unlink(this.filePath)
        console.log('删除状态:', bool)
      })

    }
  }
}

四、避坑指南(新手必看)​

  1. 路径错误:必须通过 getContext(this).xxxDir 获取沙箱路径,禁止硬编码(如 /data/data/...),否则会导致权限报错​
  2. 资源泄露:异步打开文件后,必须调用 fs.close() 关闭文件描述符,否则会占用系统资源​
  3. 目录选择:临时文件存 temp,缓存存 cache,永久文件存 files,避免数据被误清理​
  4. 权限问题:访问用户文件(如相册)需先申请权限(ohos.permission.READ_MEDIA 等),否则会报错​
  5. 线程安全:文件操作属于 IO 密集型任务,若需处理大文件(如视频),建议结合之前讲的 TaskPool/Worker 放到子线程执行,避免阻塞主线程

五、周边面试题

 谈谈你对沙箱的理解

应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录”。

  • 对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。
  • 应用沙箱限制了应用可见的数据范围。在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的系统文件(应用运行必需的少量系统文件)。因此,本应用的文件也不为其他应用可见,从而保护了应用文件的安全。
  • 应用可以在“应用文件目录”下保存和处理自己的应用文件;系统文件及其目录对于应用是只读的;而应用若需访问用户文件,则需要通过特定API同时经过用户的相应授权才能进行。

沙箱目录和缓存清除机制

  • cache 应用缓存目录(应用cache目录大小超过配额或者系统空间达到一定条件,自动触发清理该目录下文件;随应用卸载而清理)
  • preferences 用户首选项(随应用卸载而清理)
  • temp 应用临时文件路径(应用退出后即清理。保存应用的临时生成的数据,主要包括数据库缓存、图片缓存、临时日志文件、以及下载的应用安装包文件等。此路径下存储使用后即可删除的数据)
  • files 默认长期保存的文件路径;随应用卸载而清理。

 沙箱文件存媒体库

tmp00002.zhaodashen.cn/jd.png

编辑

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';

async function savePhotoToGallery(context: common.UIAbilityContext, url:string) {
  let helper = photoAccessHelper.getPhotoAccessHelper(context);
  try {
    // onClick触发后10秒内通过createAsset接口创建图片文件,10秒后createAsset权限收回。
    let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
    // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制
    let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    // 写到媒体库文件中
    const source = image.createImageSource(url) // 图片对象
    const packer = image.createImagePacker() // 创建打包对象
    const buffer = await packer.packing(source, {
      quality: 100,
      format: 'image/jpeg'
    })
    await fileIo.write(file.fd, buffer);
    await fileIo.close(file.fd);
    promptAction.showToast({ message: '已保存至相册!' });
  }
  catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`);
  }
}

@Entry
@Component
struct Index {
  build() {
    Row() {
      Column({ space: 10 }) {
        // $r('app.media.startIcon')需要替换为开发者所需的图像资源文件
        Image($r('app.media.startIcon'))
          .height(400)
          .width('100%')

        SaveButton()
          .padding({top: 12, bottom: 12, left: 24, right: 24})
          .onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {
            if (result === SaveButtonOnClickResult.SUCCESS) {
              const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
              // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片
              // =======================
              try {
                // 需要手动将 url 替换为真实服务器的 HTTP 协议地址
                request.downloadFile(
                  getContext(),
                  {
                    url: 'http://tmp00002.zhaodashen.cn/jd.png',
                    filePath: context.filesDir + '/hello3.jpg'
                  }
                ).then((data: request.DownloadTask) => {
                  let downloadTask: request.DownloadTask = data;

                  downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
                    console.log('下载中:', receivedSize, totalSize)
                  })

                  downloadTask.on('complete', () => {
                    console.log('下载完成') // 保存到媒体库中✅
                    savePhotoToGallery(context, context.filesDir + '/hello3.jpg');
                  })
                }).catch((err: BusinessError) => {
                  console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
                })
              } catch (err) {
                console.error(`Failed to request the download. err: ${JSON.stringify(err)}`);
              }
              // =======================
            } else {
              promptAction.showToast({ message: '设置权限失败!' })
            }
          })
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

鸿蒙开发者班级

✨家人们点个juejin账号关注,会持续发布大前端领域技术文章💕 🍃

✨家人们点个juejin账号关注,会持续发布大前端领域技术文章💕 🍃

✨家人们点个juejin账号关注,会持续发布大前端领域技术文章💕 🍃

    ^_^ 点关注、不迷路、主播带你学技术 (๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤