入门指南:HarmonyOS 沙箱文件管理与离线包加载机制解析

95 阅读8分钟

前言

本文将深入分析一个基于 HarmonyOS 的沙箱文件管理系统和离线包加载实现,该实现允许应用在沙箱环境中管理、解压和加载离线资源包。在鸿蒙(HarmonyOS)应用开发过程中,了解如何查看和管理应用的沙箱文件是开发者必备的技能之一。沙箱文件包含了应用的私有数据、缓存、数据库等重要信息,掌握这些文件的访问方法对于调试和问题排查至关重要

应用沙箱简介

关于应用沙箱如果不了解的可以访问华为官方文档应用沙箱目录

确保模拟器已启动并在模拟器上安装你要检查的应用 在 DevEco Studio 可以通过点击底部工具栏的 Device File Explorer查看应用沙箱

image.png

根据你的应用包名,如下方图片展示就可以找到自己的应用沙箱路径

image.png

应用沙箱是 HarmonyOS 提供的一种安全机制,用于隔离应用的文件和数据。每个应用都有自己的沙箱目录,其他应用无法访问。沙箱目录通常包含以下重要目录:

  1. base/:基础 hap 资源
  2. files/:应用私有文件
  3. cache/:缓存文件
  4. database/:数据库文件
  5. haps/:应用包资源
  6. preferences/:首选项文件

沙箱文件工具类:OfflinePackage

本文介绍的OfflinePackage类是基于HarmonyOS开发的离线资源管理工具(以ZIP文件示例),主要提供以下核心功能:

  1. 添加 ZIP 文件:支持将rawfile目录下的资源包写入应用沙箱
  2. 文件状态检测:智能判断目标文件是否存在
  3. 资源解压处理:支持zlib解压标准ZIP文件
  4. 动态内容读取:可直接读取解压后的HTML等文件内容
  5. 资源清理:支持删除已存在的ZIP包
  6. 删除文件:从沙箱目录移除指定文件

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文件不存在')
}
  1. fs.accessSync(zipPath):这个函数用于检查当前用户是否有权限访问zipPath指定的文件。如果文件存在并且可访问,则返回true;否则,抛出错误。
  2. 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();

使用示例

在演示前,需要提前准备好资源文件

image.png

创建 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 组件加载沙箱文件内容

  1. 加载沙箱 Web 资源:从应用沙箱目录加载 HTML 文件
  2. 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 资源文件
PixPin_2025-05-16_14-22-02.gif

通过上述代码解析,OfflinePackage类在 HarmonyOS 应用开发中提供了文件存在性检查、添加 ZIP 文件、删除 ZIP 文件、解压 ZIP 文件和读取文件内容等功能,将 Web 应用资源打包到应用中,实现离线访问,帮助开发者更好地管理和操作沙箱文件。这些功能的实现不仅确保了文件操作的安全性和正确性,还提升了应用的性能和用户体验。