✨家人们记得点个账号关注,会持续发布大前端领域技术文章💕 🍃
一、应用沙箱:鸿蒙应用的 “安全文件柜”
1. 核心简介
应用沙箱是鸿蒙系统为保障数据安全设计的隔离存储机制,每个应用都会获得专属的 “沙箱目录”,实现三大核心目标:
- 数据隔离:应用仅能访问自身文件和少量必需系统文件,避免被其他应用篡改或读取
- 安全防护:防止恶意路径穿越,保护用户隐私数据
- 规范存储:明确不同类型文件的存储路径,避免文件混乱
简单说,沙箱就像给应用分配了一个独立的 “文件柜”,柜子里的空间分为两部分:
- 应用文件目录(可读写,随应用卸载清理)
- 系统文件目录(只读,提供应用运行必需资源)
若需访问照片、文档等用户文件,需通过系统 API + 用户授权才能操作,进一步强化安全性。
编辑
2. 为什么必须学?
- 开发必备:下载文件(音乐、PDF、头像)、缓存数据(接口缓存、图片缓存)都需要指定正确路径
- 避坑关键:不同目录的清理规则不同(如临时目录退出即删),选错路径会导致数据丢失
- 面试加分:文件操作是鸿蒙开发高频考点,沙箱机制是核心知识点
二、核心目录详解:不同文件该存哪?
鸿蒙应用沙箱的应用文件目录包含 4 个核心子目录,按功能精准划分:
| 目录名 | 核心作用 | 生命周期 | 典型使用场景 |
|---|---|---|---|
| cache | 应用缓存目录 | 超配额 / 系统空间不足时清理;卸载清理 | 接口数据缓存、图片缓存、临时下载文件 |
| preferences | 用户首选项存储 | 卸载清理 | 应用配置(如主题、字体大小、登录状态) |
| temp | 临时文件目录 | 应用退出后清理 | 数据库临时文件、日志缓存、安装包缓存 |
| files | 长期保存文件目录 | 卸载清理 | 离线数据、用户下载的永久文件(如文档) |
developer.huawei.com/consumer/cn…
编辑
🤔关键思考:上传文件该存哪?
答案:优先选 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)
})
}
}
}
四、避坑指南(新手必看)
- 路径错误:必须通过 getContext(this).xxxDir 获取沙箱路径,禁止硬编码(如 /data/data/...),否则会导致权限报错
- 资源泄露:异步打开文件后,必须调用 fs.close() 关闭文件描述符,否则会占用系统资源
- 目录选择:临时文件存 temp,缓存存 cache,永久文件存 files,避免数据被误清理
- 权限问题:访问用户文件(如相册)需先申请权限(ohos.permission.READ_MEDIA 等),否则会报错
- 线程安全:文件操作属于 IO 密集型任务,若需处理大文件(如视频),建议结合之前讲的 TaskPool/Worker 放到子线程执行,避免阻塞主线程
五、周边面试题
谈谈你对沙箱的理解
应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录”。
- 对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。
- 应用沙箱限制了应用可见的数据范围。在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的系统文件(应用运行必需的少量系统文件)。因此,本应用的文件也不为其他应用可见,从而保护了应用文件的安全。
- 应用可以在“应用文件目录”下保存和处理自己的应用文件;系统文件及其目录对于应用是只读的;而应用若需访问用户文件,则需要通过特定API同时经过用户的相应授权才能进行。
沙箱目录和缓存清除机制
- cache 应用缓存目录(应用cache目录大小超过配额或者系统空间达到一定条件,自动触发清理该目录下文件;随应用卸载而清理)
- preferences 用户首选项(随应用卸载而清理)
- temp 应用临时文件路径(应用退出后即清理。保存应用的临时生成的数据,主要包括数据库缓存、图片缓存、临时日志文件、以及下载的应用安装包文件等。此路径下存储使用后即可删除的数据)
- files 默认长期保存的文件路径;随应用卸载而清理。
沙箱文件存媒体库
编辑
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ᵒᵛᵉᵧₒᵤ❤