下载沙箱图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { fileIo } from '@kit.CoreFileKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@ComponentV2
struct FileCopy {
@Local
list: Resource[] = [
$r("app.media.001"),
$r("app.media.002"),
$r("app.media.003"),
$r("app.media.004"),
$r("app.media.005"),
$r("app.media.006"),
$r("app.media.007"),
$r("app.media.008"),
$r("app.media.009"),
$r("app.media.010")
]


// 保存沙箱图片到相册
async saveImgToAssets() {
try {
let index = 0
while (index < this.list.length) {
let context = getContext();// 获取当前应用的上下文对象,用于访问系统资源。
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);//通过上下文获取相册访问助手对象
// Creating a Media File
let uri = await phAccessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');//创建一个新的相册资源
// Open the created media file and read the local file and convert it to ArrayBuffer for easy filling.
let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE);//打开刚刚创建的相册资源文件,模式为读写。
let buffer = getContext(this).resourceManager.getMediaContentSync(this.list[index].id);//从沙箱中读取图片资源,返回一个 ArrayBuffer,表示图片的二进制数据。
// Write the read ArrayBuffer to the new media file.
let writeLen = await fileIo.write(file.fd, buffer.buffer);
//调用 fileIo.write 方法,将读取到的图片数据(buffer.buffer)写入到相册文件中。
// file.fd:文件描述符,用于标识打开的文件。
await fileIo.close(file);
index++
}
promptAction.showToast({ message: '下载成功' })

} catch (err) {
AlertDialog.show({ message: err.message })
}
}

build() {
Column({ space: 10 }) {
Row() {
SaveButton()
.onClick((event, result: SaveButtonOnClickResult) => { // result是权限是否开启的结果
if (result === SaveButtonOnClickResult.SUCCESS) {
this.saveImgToAssets()
}
})
}
.justifyContent(FlexAlign.Center)
.width('100%')
GridRow({ columns: 2 }) {
ForEach(this.list, (item: string) => {
GridCol() {
Image(item)
.height(150)
.height(150)
.borderRadius(4)
}
.margin({
top: 10
})
})
}

}

}
}

SaveButton组件

安全控件SaveButton

1
2
3
4
5
6
SaveButton()
.onClick((event, result: SaveButtonOnClickResult) => { // result是权限是否开启的结果
if (result === SaveButtonOnClickResult.SUCCESS) {
this.saveImgToAssets()
}
})

SaveButton组件是用于请求保存文件的权限,再点击该组件后会临时获取存储权限,而不需要权限弹框授权确认。
点击事件的参数中第一个是获取点击的位置、显示区域等数据的对象,而第二个参数则是获取存储权限的授权结果,授权时长为10秒,即触发点击后,可以在10秒之内不限制次数的调用特定媒体库接口,超出10秒的调用会鉴权失败

异步事件处理函数saveImgToAssets

首先我们要明确为什么处理保存图片时间需要使用异步函数。
异步函数的核心思想在于防止程序在等待某个操作完成时陷入长时间的等待状态,将耗时包含不确定成分的长耗时操作抛出到主线程之外,防止主线程被阻塞,提高程序的响应速度用户体验

而在读取沙箱文件并保存到手机内的时候有多步耗时操作,所以我们需要进行异步编程。

沙箱

首先解释一下什么是沙箱。
在鸿蒙(HarmonyOS)开发中,沙箱机制(Sandbox Mechanism)是保障系统安全性和应用隔离性的核心技术之一。它通过严格的资源隔离和权限控制,确保每个应用在独立的环境中运行,防止恶意行为或错误操作对其他应用或系统造成影响。以下从核心原理技术实现应用场景三个方面展开说明。

2

核心原理
  • 应用隔离
    鸿蒙为每个应用分配独立的运行环境,包括进程、文件系统、内存和权限等资源。应用间无法直接访问彼此的数据或资源,必须通过系统提供的安全接口进行通信。这种隔离机制有效防止了数据泄露或恶意代码扩散。

  • 最小权限原则
    应用在安装或运行时,需明确声明所需权限(如访问摄像头、存储等),用户可动态授权。鸿蒙基于“最小权限”模型,仅授予应用完成功能所必需的权限,降低越权操作风险。

  • 资源访问控制
    系统通过内核层和框架层的双重防护,对硬件资源(如传感器、网络)和软件资源(如数据文件)进行细粒度管控。例如,应用无法直接读写其他应用私有目录下的文件。

技术实现
  • 进程与数据沙箱

    独立进程空间:每个应用运行在独立的进程中,由系统调度资源。

    私有文件存储:应用数据默认存储在沙箱目录(如 /data/app/包名/),其他应用无权限访问。公共数据需通过用户授权或系统服务(如媒体库)共享。

  • 权限动态管理
    鸿蒙支持权限的动态申请与撤销。例如,当应用需要使用定位功能时,需弹窗请求用户授权,用户可随时在设置中关闭权限。

  • 分布式安全
    在鸿蒙的分布式架构中,跨设备调用需通过安全通道完成。设备间通信需要双向认证,数据加密传输,确保分布式场景下的沙箱隔离性。

  • 微内核架构支持
    鸿蒙采用微内核设计,将核心功能(如进程调度、权限管理)与系统服务分离,减少攻击面。即使某个服务被攻击,沙箱机制也能限制其影响范围。

应用场景
  • 场景示例

    金融类应用:保护用户敏感数据(如支付信息)不被恶意应用窃取。

    多设备协同:在分布式场景中,确保智能家居设备间的指令传递安全。

    IoT 设备:防止资源受限的终端设备因单个应用崩溃导致系统瘫痪。

  • 开发者优势

    降低开发复杂度:开发者无需自行实现安全隔离逻辑,专注于业务功能。

    增强用户信任:严格的权限控制可提升用户对应用安全性的信心。

上下文对象

在鸿蒙(HarmonyOS)应用开发中,上下文对象(Context) 是连接应用与系统服务的核心桥梁,它封装了应用运行环境的关键信息,并提供了访问资源、权限、组件管理等功能的能力。以下是对上下文对象的详细解析:


上下文对象的核心作用
  1. 标识应用身份

    • 包含应用的 包名沙箱路径权限状态 等信息,用于系统验证操作合法性。
  2. 访问系统服务

    • 通过上下文可获取系统级服务(如相册服务 photoAccessHelper),这些服务需基于应用身份进行权限控制。
  3. 管理资源与组件

    • 支持访问私有资源(如 $r("app.media.xxx"))、启动其他 Ability(页面)等操作。

代码中的上下文体现与解析
  1. 获取相册访问助手(photoAccessHelper

    1
    2
    let context = getContext(); // 获取当前上下文
    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);

    getContext() 的作用:

    • 返回当前 Ability 或 UI 组件的上下文,用于标识操作来源。
    • 相册服务需要基于上下文验证权限(如 WRITE_IMAGEVIDEO)。
  2. 访问沙箱内资源(resourceManager

    1
    let buffer = getContext().resourceManager.getMediaContentSync(this.list[index].id);

    资源隔离性:

    • resourceManager 仅能访问当前应用的沙箱资源(如 /data/app/包名/resources/)。

    • $r("app.media.001")由系统自动解析为沙箱内路径,避免硬编码。

整体代码逻辑解析

  1. 初始化循环与上下文获取

    1
    2
    3
    let index = 0
    while (index < this.list.length) {
    let context = getContext(); // [!code focus]
    • 作用:
      通过 getContext() 获取当前应用的上下文对象,用于后续访问系统服务和资源 管理器。

    • 沙箱关联:
      上下文对象隐含了当前应用的沙箱路径(如 /data/app/包名/),确保资源访问 限制在隔离环境中。

  2. 创建相册资源文件

    1
    2
    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    let uri = await phAccessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
    • 关键对象:
      phAccessHelper:相册操作代理对象,需通过上下文验证应用权限。
      createAsset:在系统相册中创建空白图片文件,返回 uri 标识新文件位置。

    • 权限控制:
      需要 ohos.permission.WRITE_IMAGEVIDEO 权限,否则操作会被系统拦截。

  3. 沙箱资源读取

    1
    let buffer = getContext().resourceManager.getMediaContentSync(this.list[index].id); // [!code focus]s
    • 资源路径:
      this.list[index].id 对应 $r(“app.media.xxx”),实际指向沙箱目录 /data/app/包名/resources/。

    • 数据封闭性:
      通过 resourceManager 直接读取二进制数据(ArrayBuffer),不暴露实际文件路径。

  4. 文件写入操作

    1
    2
    3
    let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE);
    let writeLen = await fileIo.write(file.fd, buffer.buffer);
    await fileIo.close(file);
    • 关键步骤:

      fileIo.open:以读写模式打开相册文件,获取文件描述符 file.fd。

      fileIo.write:将沙箱图片二进制数据写入目标文件。

      fileIo.close:显式关闭文件,释放系统资源。

    • 安全设计:

      file.fd 是系统分配的文件句柄,其他应用无法通过此句柄操作同一文件。

      写入操作通过系统服务代理,避免直接操作相册文件系统。

  5. 完成提示与异常处理

    1
    2
    3
    4
    promptAction.showToast({ message: '下载成功' })
    } catch (err) {
    AlertDialog.show({ message: err.message })
    }
    • 用户反馈:
      showToast 显示操作成功的轻量提示,避免打断用户。

    • 错误隔离:
      通过 try-catch 捕获沙箱内外的操作异常(如权限拒绝、文件不存在),错误信息仅影响当前应用。

上传手机相册图片

首先由于鸿蒙系统的安全设置,软件的沙箱环境并不能直接访问手机相册,只能获取到选中图片复制的缓存数据。
所以我们需要先获取该图片的路径然后将图片复制到缓存目录,再进行上传操作


获取图片路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Button('上传图片')
.onClick(()=>{
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions()
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
photoSelectOptions.maxSelectNumber = 1

const photoPicker = new photoAccessHelper.PhotoViewPicker()
photoPicker.select(photoSelectOptions)
.then((rep)=>{
promptAction.showToast({
message:rep.photoUris.toString()
})
})
})

这个点击事件中主要分为两块功能,分别是设定PhotoViewPicker对象的参数PhotoSelectOptions,以及创建图库选择器实例,调用PhotoViewPicker.select接口拉起图库界面进行文件选择。文件选择成功后,返回PhotoSelectResult结果集。

PhotoSelectOptions

PhotoViewMIMETypes.IMAGE_TYPE表示过滤选择媒体文件类型为IMAGE类型。
maxSelectNumber表示最大选择数量,这里设置为1,表示只能选择一张图片。

PhotoViewPicker

PhotoViewPicker可以利用select方法拉起图库界面进行文件选择。
而得益于鸿蒙星河版的安全机制,软件只能获取渠道你所选择的图片的路径,并不能直接访问你的图库,这还是相当开创性的安全机制。


图片复制到缓存目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private getPhoto() {
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;

const photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(photoSelectOptions)
.then((rep) => {
promptAction.showToast({
message: rep.photoUris.toString()
});
const uri = rep.photoUris[0]
const context = getContext()
const fileType = 'jpg'
const fileName = `${Date.now()}${Math.floor(Math.random()*100)}.${fileType}`
const copyFilePath = context.cacheDir + '/' + fileName
const file = fs.openSync(uri,fs.OpenMode.READ_ONLY)
fs.copyFileSync(file.fd,copyFilePath)
promptAction.showToast({
message:copyFilePath
})
});

}

在这段代码中,我们在select方法的异步回调函数then中继续编写实现通过uri将系统相册图片复制到缓存目录的代码。

首先定义一系列的常量以便后续使用,主要包括以下几个:

  1. uri:通过PhotoViewPickerselect方法获取到的图片uri
  2. context:通过getContext()方法获取到的应用上下文。
  3. fileType:图片的文件类型,这里设置为jpg
  4. fileName:生成的文件名,使用当前时间戳和随机数拼接而成。
  5. copyFilePath:图片复制后的目标路径,即缓存目录下的文件名。
  6. file:通过fs.openSync方法打开图片文件,并设置为只读模式。

接下来,我们使用fs.copyFileSync方法将打开的图片文件复制到目标路径copyFilePath中。

最后,我们使用showToast方法显示复制后的图片路径copyFilePath,以便我们可以确认图片是否成功复制到缓存目录。

1

从最终获取到的缓存图片路径可以看出,图片已经成功复制到缓存目录中。
/data/storage/el2/base/haps/entry/cache/174004556592888.jpg

  1. data: 代表应用文件目录。
  2. storage: 代表本应用持久化文件目录。
  3. el2: 代表不同文件加密类型。

    1. EL1(Encryption Level 1):
      保护设备上的所有文件的基础安全能力。在设备开机后,不需要用户先完成身份认证即可访问EL1保护的文件。如无特殊必要,不推荐使用该方式。
      如果直接窃取设备存储介质上的密文,攻击者无法脱机进行解密

    2. EL2(Encryption Level 2):
      在EL1的基础上,增加首次认证后的文件保护能力。设备开机后,用户在通过首次认证后,通过EL2能力保护的文件才能被访问。此后只要设备没有关机,通过EL2能力保护的文件一直可被访问。推荐应用默认使用该方式。
      如果在关机后丢失手机,则攻击者无法读取通过EL2能力保护的文件。

    3. EL3(Encryption Level 3):
      与EL4整体能力类似,但和EL4的区别是,在锁屏下可创建新的文件,但无法读取。如无特殊必要,无需使用该方式。

    4. EL4(Encryption Level 4):
      在EL2的基础上,增加设备锁屏时的文件保护能力。在用户锁屏时,通过EL4能力保护的数据将无法被访问。如无特殊必要,无需使用该方式。
      如果设备在锁屏状态下被盗,攻击者无法读取通过EL4能力保护的文件。

    5. EL5(Encryption Level 5):
      在EL2的基础上,增加设备锁屏时的文件保护能力。在用户锁屏后,满足一定条件时,通过EL5能力保护的数据将无法被访问,但可以继续创建和读写新的文件。如无特殊必要,无需使用该方式。
      默认情况下不会生成EL5的相关目录,需要配置访问E类加密数据库的相关权限,详见E类加密数据库的使用。

  4. base:通过ApplicationContext可以获取distributedfiles目录或base下的files、cache、preferences、temp等目录的应用文件路径,应用全局信息可以存放在这些目录下。

而在这个路径中我们可以看到该文件是储存在cache目录下的,这个目录下的就是缓存文件。

3

用网络请求上传图片

对于文件的传递我们不能直接用http模块,而是要用request模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private getPhotoAndUploadImg() {
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;

const photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(photoSelectOptions)
.then((rep) => {
promptAction.showToast({
message: rep.photoUris.toString()
});
const uri = rep.photoUris[0]
const context = getContext()
const fileType = 'jpg'
const fileName = `${Date.now()}${Math.floor(Math.random() * 100)}.${fileType}`
const copyFilePath = context.cacheDir + '/' + fileName
const file = fs.openSync(uri, fs.OpenMode.READ_ONLY)
fs.copyFileSync(file.fd, copyFilePath)
promptAction.showToast({
message: copyFilePath
})

// 上传文件
let files: Array<request.File> = [
// internal://cache/ 固定的,后面跟上 咱们上一步传的文件名即可
// name 和接口文档的要求对上
{ filename: fileName, type: fileType, name: 'img', uri: `internal://cache/${fileName}` }
]

let uploadConfig: request.UploadConfig = {
url: 'https://hmajax.itheima.net/api/uploadimg',
method: http.RequestMethod.POST,
header: {
// 和接口文档的要求的格式对象
contentType: 'multipart/form-data',
},
files,
data: []
}
console.log('debug1 发送请求')
request.uploadFile(context, uploadConfig)
.then((res => {
console.log('debug1 进入异步函数then')
// 这里可以获取到响应的内容
res.on('headerReceive', (value) => {
AlertDialog.show({
message:JSON.stringify(value)
})

})
}))
});
}

在将图片复制到缓存目录后,我们需要将图片上传到服务器。
这里我们使用request.uploadFile方法来实现图片上传的功能。
首先,我们定义了一个UploadConfig对象,其中包含了上传文件的相关配置信息,包括上传的URL、请求方法、请求头、文件列表和数据列表。
然后,我们使用request.uploadFile方法来上传文件,该方法返回一个Promise对象,我们可以在then方法中处理上传成功的响应。
then方法中,我们可以通过res.on方法来监听上传的进度和响应的内容。

对于返回的数据我们先将他打印出来进行结构分析。

4

可以看到返回的是一个对象,其中包含了上传的进度和响应的内容。

而这其中最重要的部分含显然是body,也就是我们需要的返回数据

其中message字段是我们的上传状态,这里是上传成功。
data字段是我们的上传数据,这里是我们上传的图片的路径。

由此我们可以定义接口

1
2
3
4
5
6
7
8
9
10
interface UploadResponse {
body: string
}
interface ResponseBody {
message: string
data: ResponseUrl
}
interface ResponseUrl{
url:string
}

再利用接口获取数据即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

request.uploadFile(context, uploadConfig)
.then((res => {
console.log('debug1 进入异步函数then')
// 这里可以获取到响应的内容
res.on('headerReceive', (value) => {
AlertDialog.show({
message:JSON.stringify(value)
})
const uploadRes = value as UploadResponse
// 通过 JSON.parse 转为对应的数据
const responseBody = JSON.parse(uploadRes.body) as ResponseBody
// 获取咱们要的响应的 URL 即可
console.log('debug1 responseBody:', responseBody.data.url)
})
}))

渲染到页面上

这就很简单啦,价格状态变量就解决了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';
import fs from '@ohos.file.fs';
import { request } from '@kit.BasicServicesKit';
import { http } from '@kit.NetworkKit';

interface UploadResponse {
body: string
}

interface ResponseBody {
message: string
data: ResponseUrl
}

interface ResponseUrl {
url: string
}

@Entry
@ComponentV2
struct PhotoPicker {
@Local imgSrc: string = ''

build() {
Column() {
Image(this.imgSrc)
.width(100)
Button('上传图片')
.onClick(() => {
this.getPhotoAndUploadImg();
})
}
.width('100%')
.height('100%')
}

private getPhotoAndUploadImg() {
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;

const photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(photoSelectOptions)
.then((rep) => {
promptAction.showToast({
message: rep.photoUris.toString()
});
const uri = rep.photoUris[0]
const context = getContext()
const fileType = 'jpg'
const fileName = `${Date.now()}${Math.floor(Math.random() * 100)}.${fileType}`
const copyFilePath = context.cacheDir + '/' + fileName
const file = fs.openSync(uri, fs.OpenMode.READ_ONLY)
fs.copyFileSync(file.fd, copyFilePath)
promptAction.showToast({
message: copyFilePath
})

// 上传文件
let files: Array<request.File> = [
// internal://cache/ 固定的,后面跟上 咱们上一步传的文件名即可
// name 和接口文档的要求对上
{
filename: fileName,
type: fileType,
name: 'img',
uri: `internal://cache/${fileName}`
}
]

let uploadConfig: request.UploadConfig = {
url: 'https://hmajax.itheima.net/api/uploadimg',
method: http.RequestMethod.POST,
header: {
// 和接口文档的要求的格式对象
contentType: 'multipart/form-data',
},
files,
data: []
}
console.log('debug1 发送请求')
request.uploadFile(context, uploadConfig)
.then((res => {
console.log('debug1 进入异步函数then')
// 这里可以获取到响应的内容
res.on('headerReceive', (value) => {
AlertDialog.show({
message: JSON.stringify(value)
})
const uploadRes = value as UploadResponse
// 通过 JSON.parse 转为对应的数据
const responseBody = JSON.parse(uploadRes.body) as ResponseBody
// 获取咱们要的响应的 URL 即可
console.log('debug1 responseBody:', responseBody.data.url)

this.imgSrc = responseBody.data.url
})
}))
});

}
}

5