前言
本文将深入分析一个基于 HarmonyOS 的沙箱文件管理系统和离线包加载实现,该实现允许应用在沙箱环境中管理、解压和加载离线资源包。在鸿蒙(HarmonyOS)应用开发过程中,了解如何查看和管理应用的沙箱文件是开发者必备的技能之一。沙箱文件包含了应用的私有数据、缓存、数据库等重要信息,掌握这些文件的访问方法对于调试和问题排查至关重要
应用沙箱简介
关于应用沙箱如果不了解的可以访问华为官方文档应用沙箱目录
确保模拟器已启动并在模拟器上安装你要检查的应用 在 DevEco Studio 可以通过点击底部工具栏的 Device File Explorer查看应用沙箱
根据你的应用包名,如下方图片展示就可以找到自己的应用沙箱路径
应用沙箱是 HarmonyOS 提供的一种安全机制,用于隔离应用的文件和数据。每个应用都有自己的沙箱目录,其他应用无法访问。沙箱目录通常包含以下重要目录:
base/
:基础 hap 资源files/
:应用私有文件cache/
:缓存文件database/
:数据库文件haps/
:应用包资源preferences/
:首选项文件
沙箱文件工具类:OfflinePackage
本文介绍的OfflinePackage
类是基于HarmonyOS开发的离线资源管理工具(以ZIP文件示例),主要提供以下核心功能:
- 添加 ZIP 文件:支持将rawfile目录下的资源包写入应用沙箱
- 文件状态检测:智能判断目标文件是否存在
- 资源解压处理:支持zlib解压标准ZIP文件
- 动态内容读取:可直接读取解压后的HTML等文件内容
- 资源清理:支持删除已存在的ZIP包
- 删除文件:从沙箱目录移除指定文件
HarmonyOS 通过context.filesDir
获取应用的沙箱目录路径,这是应用私有的文件存储区域,其他应用无法访问。
let boxPath = context.filesDir; // 获取沙箱路径
文件状态检测(isExist)
isExist(context: Context, dynDir: string, dynFileName: string): boolean {
let boxPath = context.filesDir; // 获取沙箱路径
let unzipPath = boxPath + "/" + dynDir; // 动态目录
let zipPath = boxPath + "/" + dynDir + "/" + dynFileName; // 动态文件名
// 判断沙箱目录是否存在
if (fs.accessSync(unzipPath)) {
if (fs.accessSync(zipPath)) {
console.log('沙箱目录下存在zip文件');
return true;
}
}
return false; // 如果沙箱目录不存在或不包含zip文件,返回false
}
- 采用同步访问检测机制
- 使用
fs.accessSync
进行双重路径验证 - 返回布尔值表示文件存在状态
资源写入(addZip)
// 获取资源文件内容
let uint8Array: Uint8Array = context.resourceManager.getRawFileContentSync(fileUri);
let bf = buffer.from(uint8Array).buffer;
// 创建目录和文件
fs.mkdirSync(unzipPath);
const fsOpen = fs.openSync(zipPath, fs.OpenMode.READ_WRITE | fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC)
// 写入数据
let destFile = fs.writeSync(fsOpen.fd, bf);
fs.close(destFile)
- 使用ResourceManager获取原始文件
- 通过Buffer进行二进制数据转换
- 采用TRUNC模式确保文件覆盖写入
- 支持自动创建目标目录
ZIP解压(unzipFile)
const options: zlib.Options = {
level: zlib.CompressLevel.DEFAULT_COMPRESSION
};
zlib.decompressFile(zipPath, unzipPath, options, (err) => {
if (err) {
console.error(`Error ${err.code}: ${err.message}`);
return;
}
// 解压后文件处理...
});
使用zlib
模块的decompressFile
方法进行异步解压,
- 配置标准压缩级别参数
- 使用异步回调处理解压结果,避免阻塞主线程,提升应用性能。
- 内置解压后文件验证机制
- 自动读取验证文件内容
文件内容读取 (readFile)
const fd = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const size = fs.statSync(fd).size;
const bufferRes = new ArrayBuffer(size);
return buffer.from(bufferRes).toString();
文件读取流程包括:打开文件、获取大小、创建缓冲区、读取内容和转换格式。
- 同步读取保证数据完整性
- 精确计算文件大小分配缓冲区
- 自动转换Buffer为字符串
删除文件 (deleteZip)
// 删除沙箱目录下zip文件
if (fs.accessSync(zipPath)) {
fs.unlinkSync(zipPath)
console.log('删除沙箱目录下zip文件成功')
} else {
console.log('沙箱目录下zip文件不存在')
}
fs.accessSync(zipPath)
:这个函数用于检查当前用户是否有权限访问zipPath
指定的文件。如果文件存在并且可访问,则返回true
;否则,抛出错误。fs.unlinkSync(zipPath)
:这个函数用于同步删除指定的文件。如果文件被成功删除,则不会有返回值;如果出现错误,则会抛出错误。
完整代码
import { zlib, BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit'; // 导入文件模块
import { buffer } from '@kit.ArkTS';
class OfflinePackage {
// 判断沙箱目录是否存在zip文件
isExist(context: Context, dynDir: string, dynFileName: string): boolean {
let boxPath = context.filesDir; // 获取沙箱路径
let unzipPath = boxPath + "/" + dynDir; // 动态目录
let zipPath = boxPath + "/" + dynDir + "/" + dynFileName; // 动态文件名
// 判断沙箱目录是否存在
if (fs.accessSync(unzipPath)) {
console.log('沙箱目录存在');
// 判断沙箱目录下是否存在zip文件
if (fs.accessSync(zipPath)) {
console.log('沙箱目录下存在zip文件');
return true; // 如果沙箱目录存在且包含zip文件,返回true
}
}
return false; // 如果沙箱目录不存在或不包含zip文件,返回false
}
// 添加zip文件到沙箱目录
addZip(context: Context, dynDir: string, dynFileName: string, directory?: string) {
let boxPath = context.filesDir; // 获取沙箱路径
let unzipPath = boxPath + "/" + dynDir; // 动态目录
let zipPath = boxPath + "/" + dynDir + "/" + dynFileName; // 动态文件名
let fileUri = directory + "/" + dynFileName; // 获取zip文件路径
// 使用上下文的 resourceManager 获取文件 Uint8Array 数据
let uint8Array: Uint8Array = context.resourceManager.getRawFileContentSync(fileUri);
// 将获取的 unit8Array 数据 转换为 ArrayBuffer
let bf = buffer.from(uint8Array).buffer;
// 创建沙箱目录
if (fs.accessSync(unzipPath)) {
console.log('沙箱目录存在');
} else {
try {
fs.mkdirSync(unzipPath);
} catch (e) {
console.error('创建目录失败: ' + e);
return;
}
}
// 在创建的沙箱路径下创建待拷贝数据的空文件
const fsOpen =
fs.openSync(zipPath, fs.OpenMode.READ_WRITE | fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC)
// 写入数据
let destFile = fs.writeSync(fsOpen.fd, bf);
// 写入完毕后关闭文件,因为在上一步文件在开启中
fs.close(destFile)
}
// 删除沙箱里的zip文件
deleteZip(context: Context, dynDir: string, dynFileName: string) {
let boxPath = context.filesDir; // 获取沙箱路径
let unzipPath = boxPath + "/" + dynDir; // 动态目录
let zipPath = boxPath + "/" + dynDir + "/" + dynFileName; // 动态文件名
// 删除沙箱目录下zip文件
if (fs.accessSync(zipPath)) {
fs.unlinkSync(zipPath)
console.log('删除沙箱目录下zip文件成功')
} else {
console.log('沙箱目录下zip文件不存在')
}
}
// 解压沙箱里的zip文件
unzipFile(context: Context, dynDir: string, dynFileName: string) {
if (!this.isExist(context, dynDir,dynFileName)) {
return console.log('沙箱目录下zip文件不存在')
}
let boxPath = context.filesDir; // 获取沙箱路径
let unzipPath = boxPath + "/" + dynDir; // 动态目录
let zipPath = boxPath + "/" + dynDir + "/" + dynFileName; // 动态文件名
// 配置解压选项
let options: zlib.Options = {
// 设置压缩级别(此处使用默认压缩级别)
level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION
};
zlib.decompressFile(zipPath, unzipPath, options, (errData: BusinessError) => {
if (errData !== null) {
console.error('记录错误代码和消息', `errData is errCode:${errData.code} message:${errData.message}`);
}
// 解压成功后,打开输出目录下的'test.html'文件,以只读模式
let fileTarget = fs.openSync(unzipPath + "/test.html", fs.OpenMode.READ_ONLY);
// 获取文件的大小
let size = fs.statSync(fileTarget.fd).size;
// 创建一个与文件大小相同的数据缓冲区
let bufferRes = new ArrayBuffer(size);
let readLen =fs.readSync(fileTarget.fd,bufferRes)
// 关闭文件
fs.closeSync(fileTarget);
let result = buffer.from(bufferRes).toString()
})
}
// 读取沙箱路径下指定文件内容
readFile(context: Context, dynDir: string, dynFileName: string): string {
let boxPath = context.filesDir;
let htmlPath = boxPath + "/" + dynDir + "/" + dynFileName;
let textHtml = ''
if (fs.accessSync(htmlPath)) {
let fileTarget = fs.openSync(htmlPath, fs.OpenMode.READ_ONLY);
let size = fs.statSync(fileTarget.fd).size;
let bufferRes = new ArrayBuffer(size);
let readLen =fs.readSync(fileTarget.fd,bufferRes)
let hotIndexContent = buffer.from(bufferRes).toString()
textHtml = hotIndexContent;
}
return textHtml
}
}
export const offlinePackage = new OfflinePackage();
使用示例
在演示前,需要提前准备好资源文件
创建 SandboxFileOperationPage
组件提供操作界面,展示如何使用OfflinePackage
功能:
import { offlinePackage } from "utils" // 初始化实例
import { common } from "@kit.AbilityKit"
@Component
export struct SandboxFileOperationPage {
context = getContext(this) as common.UIAbilityContext
@Builder
OperationRow(title: string, buttonText: string, onClick: () => void) {
Row() {
Text(title)
.fontSize(15)
.fontWeight(500)
.fontColor($r('sys.color.black'));
Button(buttonText)
.fontSize(15)
.fontColor($r('sys.color.white'))
.onClick(() => {
onClick();
});
}
.width('80%')
.justifyContent(FlexAlign.SpaceBetween);
}
build() {
Navigation() {
Column({ space: 20 }) {
this.OperationRow('添加沙箱文件', '添加', () => {
offlinePackage.addZip(this.context, 'webSources', 'test.zip', 'zip');
});
this.OperationRow('读取沙箱文件是否存在', '读取', () => {
if (offlinePackage.isExist(this.context, 'webSources', 'test.zip')) {
console.log('沙箱目录存在zip文件');
} else {
console.log('沙箱目录不存在zip文件');
}
});
this.OperationRow('删除沙箱下的zip文件', '删除', () => {
offlinePackage.deleteZip(this.context, 'webSources', 'test.zip')
});
this.OperationRow('删除沙箱下的html文件', '删除', () => {
offlinePackage.deleteZip(this.context, 'webSources', 'test.html')
});
this.OperationRow('解压沙箱文件', '解压', () => {
offlinePackage.unzipFile(this.context, 'webSources', 'test.zip')
});
this.OperationRow('读取沙箱路径下文件', '读取文件', () => {
const textHtml = offlinePackage.readFile(this.context, 'webSources', 'test.html')
console.log('读取沙箱路径下文件 text: ' + textHtml)
});
}
}
.title('沙箱文件')
.titleMode(NavigationTitleMode.Mini)
.mode(NavigationMode.Stack)
}
}
context = getContext(this) as common.UIAbilityContext
获取当前Ability上下文- 通过
@Builder
装饰器创建可复用的操作行模板 webSources
是自定义的沙箱目录名
功能上可自行测试,接下来是创建 WebLoadOfflinePackagePage
组件加载沙箱文件内容
- 加载沙箱 Web 资源:从应用沙箱目录加载 HTML 文件
- JavaScript 与原生交互:实现 Web 与原生代码的双向通信
@Component
export struct WebLoadOfflinePackagePage {
context = getContext(this) as common.UIAbilityContext
controller: webview.WebviewController = new webview.WebviewController();
private webController: web_webview.WebviewController = new web_webview.WebviewController();
url = 'file://' + this.context.filesDir + '/webSources/test.html';
build() {
Navigation() {
Column() {
Web({
src: this.url, // 设置 Web 组件的 HTML 文件路径
controller: this.webController // 绑定 Web 组件的控制器
})
.darkMode(WebDarkMode.Auto)
.fileAccess(true)
.javaScriptProxy({
// 定义 JavaScript 调用原生方法的功能
object: {
openCamera: () => this.openGallery(), // 当 JavaScript 调用 openCamera 时,执行原生方法 openGallery
},
name: 'webJSCallObject', // 定义 JavaScript 中调用原生方法的对象名称
methodList: ['openCamera', 'dbConnection'], // 定义 JavaScript 可以调用的原生方法列表
controller: this.webController // 绑定 Web 组件的控制器,用于管理 JavaScript 和原生代码的交互
} as JavaScriptProxy)
}
}
.title('web加载沙箱文件')
.titleMode(NavigationTitleMode.Mini)
.mode(NavigationMode.Stack)
}
关键配置说明:
-
src
:指定要加载的 HTML 文件路径,这里使用沙箱中的文件路径 -
controller
:Webview 控制器,用于管理 Webview 行为 -
fileAccess(true)
:启用本地文件访问权限,允许 Webview 访问设备文件
沙箱文件路径构建:url = 'file://' + this.context.filesDir + '/webSources/test.html';
路径构建要点:
- 使用
file://
协议访问本地文件 context.filesDir
获取应用沙箱目录- 路径指向先前解压的 Web 资源文件
通过上述代码解析,OfflinePackage
类在 HarmonyOS 应用开发中提供了文件存在性检查、添加 ZIP 文件、删除 ZIP 文件、解压 ZIP 文件和读取文件内容等功能,将 Web 应用资源打包到应用中,实现离线访问,帮助开发者更好地管理和操作沙箱文件。这些功能的实现不仅确保了文件操作的安全性和正确性,还提升了应用的性能和用户体验。