【鸿蒙】鸿蒙 HAR 组件包中的资源隔离与图片动态获取:从资源模型到 ComponentSnapshot 的一次完整分析
一、背景:一个看似简单的图片问题
在鸿蒙开发中,如果一个业务模块最终以 HAR 组件包的形式交付,那么组件内部通常会携带自己的资源:
HAR
├── src/
├── ets/
└── resources/
└── base/
└── media/
└── poster.png
在 HAR 自己的页面中,可以非常自然地:
Image($r('app.media.poster'))
图片能够正常显示。
到这里,一切都很美好。
但业务继续往下走,问题出现了:
这张图片不是只用来显示,而是需要参与 PixelMap 图片合成。怎么办?
例如:
HAR 内置海报
+
动态二维码
+
用户信息
↓
最终合成图片
这时候我们需要的不再是一个 $r() 资源引用,而是真正的:
PixelMap
或者一个可以重新读取的图片文件。
于是问题从“怎么显示图片”变成了:
如何跨越 HAR 资源和图片处理 API 之间的边界?
二、先理解鸿蒙为什么要这样设计:HAR 不是一个独立应用
很多问题的根源,其实来自一个很容易被忽略的事实:
HAR 和 HAP 的定位完全不同。
可以简单理解成:
HAP
│
├── Ability
├── 应用生命周期
├── Context
├── 应用资源
└── 应用运行环境
而 HAR 更接近:
HAR
│
├── 代码
├── 组件
├── 页面
└── 资源
它是一个可被宿主应用集成的组件包。
因此:
宿主 HAP
│
├── EntryAbility
├── EntryAbilityContext
│
└── 集成 HAR
│
├── HAR代码
└── HAR资源
这里非常重要的一点是:
HAR 并没有因为被宿主加载,就自动拥有一个独立的 Ability 生命周期和独立的 EntryAbilityContext。
因此,当 HAR 代码拿到:
EntryAbilityContext
它本质上拿到的是:
宿主应用的运行上下文。
这就解释了我们最开始遇到的问题。
三、为什么宿主 Context 不应该被认为是 HAR 的 ResourceManager?
假设:
宿主 HAP
resources/
base/
media/
host.png
HAR:
HAR
resources/
base/
media/
poster.png
宿主给 HAR 一个:
EntryAbilityContext
那么:
context.resourceManager
天然对应的是宿主运行环境的资源管理体系。
而不是:
HAR/resourceManager
这种理解其实很符合鸿蒙组件化的设计思想。
因为如果 HAR 可以随意通过宿主 Context 去访问自己的所有内部资源,那么组件边界就会变得非常模糊:
宿主 Context
↓
任意 HAR
↓
任意资源
↓
任意模块
这会让模块资源管理变得非常混乱。
所以鸿蒙这里实际上体现的是一种比较典型的组件化思想:
组件可以携带资源,但组件资源的访问应该遵循组件自身的资源解析机制,而不是简单把宿主 Context 当成组件自己的 Context。
四、那为什么 $r() 又能工作?
这恰恰是整个问题最值得理解的地方。
我们写:
Image($r('app.media.poster'))
并不是:
读取一个普通文件
而是在告诉鸿蒙:
我要引用这个组件/模块声明的资源。
资源引用本身属于鸿蒙 UI/资源体系。
因此:
HAR资源
↓
$r()
↓
资源解析
↓
Image
↓
渲染系统
这个过程不要求我们手动拿着宿主 EntryAbilityContext 去:
filesDir
resourceManager
rawfile
逐层寻找。
这也是为什么:
Image($r('app.media.poster'))
可以正常工作。
而:
context.resourceManager.getMedia(...)
却不能简单理解成“当然应该能找到 HAR 资源”。
二者实际上处于不同的抽象层。
五、真正的问题:我们需要的是“资源”,还是“渲染结果”?
到了这里,可以把问题重新定义。
最开始我们一直在问:
怎么从 HAR 中把 poster.png 读出来?
其实这个问题本身就有点偏。
因为我们真正需要的是:
poster.png
↓
PixelMap
↓
图片合成
而鸿蒙已经帮我们完成了:
HAR Resource
↓
Image
那么为什么不继续利用 UI 系统已经提供的能力?
于是思路发生变化:
不要:
HAR Resource
↓
Context
↓
文件系统
↓
PixelMap
而是:
HAR Resource
↓
$r()
↓
Image
↓
ComponentSnapshot
↓
PixelMap
这就是最终方案的核心。
六、ComponentSnapshot 本质上是什么?
这里不能把 ComponentSnapshot 简单理解成:
“截一张屏。”
这个理解太浅。
更准确地说,它提供了一条从:
UI Component
到:
图像数据
的桥梁。
也就是:
声明式 UI
↓
布局
↓
渲染
↓
组件最终视觉结果
↓
PixelMap
因此:
const snapshot = uiContext.getComponentSnapshot()
const pixelMap = await snapshot.get(componentId)
本质上是在说:
我不再关心这个 Image 最初的资源来自哪个模块,我只关心这个组件最终渲染出来的图像。
这个思路非常重要。
因为此时:
HAR资源边界
已经不再重要了。
系统已经把它变成:
UI渲染结果
而 UI 渲染结果恰恰是 ComponentSnapshot 所能处理的对象。
七、这其实是一次“资源边界 → UI边界”的转换
把整个过程抽象一下:
模块边界
│
HAR Resource ────────┤
│
▼
Image
│
│
UI Render Boundary
│
▼
ComponentSnapshot
│
▼
PixelMap
我们实际上做了一件很有意思的事情:
没有突破 HAR 的资源边界,而是让资源正常进入 UI 渲染体系,再从 UI 渲染体系获得图像数据。
这也是为什么这个方案在架构上是成立的。
它不是:
“偷偷绕过鸿蒙的资源机制。”
而是:
利用鸿蒙已经公开提供的 UI 能力,把一个组件资源转换成标准图像数据。
八、但是,这个方案也不是“完美方案”
这里必须客观一点。
如果把:
Image → Snapshot → PNG → ImageSource → PixelMap
作为鸿蒙官方意义上的“资源读取 API”,那显然是不合适的。
因为它本质上是:
通过 UI 渲染链路间接获得资源数据。
所以它存在几个明显的缺点。
8.1 第一个缺点:存在一次额外的渲染转换
正常情况下:
图片资源
↓
PixelMap
理论上可以直接完成。
现在却变成:
图片资源
↓
Image
↓
布局
↓
渲染
↓
Snapshot
↓
PixelMap
中间多了一整套 UI 流程。
因此:
- 有额外开销
- 依赖 UI 组件存在
- 依赖 UIContext
- 依赖组件已经进入可 Snapshot 的状态
这显然不是最高效的资源读取方式。
九、第二个缺点:Image 尺寸会影响最终结果
这是实际使用中最容易踩坑的地方。
例如:
Image($r('app.media.poster'))
.width(100)
.height(100)
你不能简单认为:
HAR原图 = 1280 × 1280
那么 Snapshot 一定还能得到:
1280 × 1280
因为你实际上已经让这张图片进入了:
100 × 100
的 UI 渲染环境。
所以最终应该:
Image($r('app.media.poster'))
.width(1280)
.height(1280)
.opacity(1)
如果后续需要:
1280 × 1280
那么 Image 的布局尺寸也应该尽量满足这个要求。
十、为什么这和 Android 的思维不一样?
这是非常值得单独强调的一点。
Android 开发者很容易带着这样的经验:
ImageView尺寸
≠
Drawable原始尺寸
例如一个 Drawable 本身:
1280 × 1280
即使 ImageView 很小,底层 Drawable 的原始数据依然存在。
但这里我们的目标不是:
获取 Drawable 原始数据
而是:
获取 UI Component 的 Snapshot
Snapshot 本质上面对的是:
渲染后的组件。
因此:
Image尺寸
↓
布局尺寸
↓
渲染结果
↓
Snapshot尺寸
这就是为什么不能简单照搬 Android 的经验。
十一、第三个缺点:不能完全透明
既然 Image 只是“中转站”,第一反应通常是:
.opacity(0)
这样用户就看不到它了。
但是:
Image
↓
opacity = 0
↓
Snapshot
得到的自然也是:
完全透明
所以如果需要 Snapshot 获取实际图片内容:
.opacity(1)
才是安全的。
如果不希望用户看到,可以利用组件层级让 Image 被其他 UI 覆盖。
这也恰好说明:
ComponentSnapshot 获取的是组件最终视觉表现,而不是简单地读取 Image 背后的原始资源文件。
十二、第四个特点:Image 被遮挡并不等于资源消失
这里又是一个很有意思的地方。
如果:
Image
↓
其他 UI 覆盖
并不意味着:
Image资源不存在
只要这个 Image 组件仍然存在于组件树,并且能够正常被 Snapshot 获取,就可以继续:
ComponentSnapshot
↓
PixelMap
所以:
“用户看不到”与“组件不存在”是两个完全不同的问题。
这也是为什么我们最终不需要依靠:
opacity(0)
去隐藏 Image。
十三、为什么最终要把 PixelMap 再保存成文件?
其实严格来说:
HAR Image
↓
Snapshot
↓
PixelMap
已经解决问题了。
为什么还要:
PixelMap
↓
PNG
↓
PixelMap
多绕一圈?
原因是生命周期和业务解耦。
如果整个业务链路都依赖:
UIContext
那么会变成:
页面
↓
UIContext
↓
资源获取
↓
图片合成
↓
更多业务
这会导致 Context 在整个链路中到处传递。
而保存到:
context.filesDir/ai_images/
之后:
HAR资源阶段
↓
保存一次
↓
文件
↓
普通业务阶段
↓
PixelMap
资源准备和图片业务就被彻底拆开了。
十四、这也是为什么最终选择“静态无状态工具类”
最终没有:
class Manager {
private context: Context
}
也没有:
static context: Context
更没有单例长期保存:
UIContext
而是:
static async saveImageFromComponent(
uiContext: UIContext,
componentId: string,
fileName: string
)
和:
static async getPixelMap(
context: Context,
fileName: string
)
两个阶段各自传入自己真正需要的 Context。
这样:
save阶段
UIContext
↓
Snapshot
↓
文件
↓
UIContext生命周期结束
之后:
get阶段
EntryAbilityContext
↓
文件
↓
PixelMap
工具类本身不持有任何 Context。
这是比“单例缓存 Context”更加干净的设计。
十五、这个方案真正好在哪里?
如果从系统设计角度评价,这个方案最大的优点并不是:
“终于把图片保存出来了。”
而是:
1. 没有破坏 HAR 的资源封装
没有要求:
HAR
↓
直接暴露资源路径
也没有要求宿主:
把 HAR 资源复制一份出来
HAR 仍然可以保持自己的资源封装。
2. 没有要求宿主了解 HAR 内部资源结构
宿主不需要知道:
poster.png
qr_bg.png
xxx.png
这些东西具体放在哪里。
HAR 自己通过:
$r(...)
加载。
3. 图片业务和组件资源解耦
一旦转换成:
filesDir/ai_images/poster.png
后面的图片合成模块就不需要知道:
这张图来自 HAR
它只知道:
这里有一个 PNG
这对于组件化非常重要。
4. Context 生命周期不会被工具类污染
工具类不持有:
UIContext
EntryAbilityContext
所以不会因为一个图片工具类,把整个页面生命周期拖住。
十六、但是从“理想设计”来看,我们其实希望鸿蒙提供什么?
这才是这件事情真正值得讨论的地方。
如果鸿蒙提供一个明确的:
HAR Resource
↓
PixelMap
或者:
Resource
↓
ImageSource
↓
PixelMap
能力,那么整个过程可以变成:
HAR Resource
↓
ResourceManager
↓
ImageSource
↓
PixelMap
而不需要:
HAR Resource
↓
Image
↓
布局
↓
渲染
↓
Snapshot
↓
PixelMap
后者明显更重。
所以从 API 设计角度,我认为更理想的是:
鸿蒙应该让组件模块能够在不依赖宿主 AbilityContext 的情况下,对自身声明的资源进行标准化访问,并提供统一的 Resource → ImageSource/PixelMap 转换能力。
这样才能真正做到:
组件资源
↓
组件资源管理器
↓
PixelMap
而不是:
组件资源
↓
UI
↓
Snapshot
↓
PixelMap
十七、为什么鸿蒙可能没有直接把这条路做得这么简单?
这其实涉及一个系统设计上的取舍。
鸿蒙需要同时处理:
应用资源
模块资源
HAR资源
HSP资源
系统资源
Ability资源
如果所有资源都允许:
任意 Context
↓
任意资源
那么资源隔离、模块化、依赖关系都会变得非常复杂。
所以它更倾向于:
模块
↓
声明资源
↓
资源引用
↓
运行时解析
↓
UI/业务使用
这种方式。
它的好处是:
- 模块边界清晰
- 宿主与组件解耦
- 资源归属明确
- 更适合组件化
但缺点也很明显:
当开发者需要“资源原始数据”而不是“资源 UI 表现”时,API 会显得不够顺手。
这正是我们这次遇到的问题。
十八、所以这个方案应该怎么看?
我认为应该把它定义为:
一个合理的工程性解决方案,而不是理想的资源访问方案。
这两个概念一定要区分。
从工程实践看
Image
↓
ComponentSnapshot
↓
PixelMap
↓
文件
完全可以使用。
尤其适合:
- HAR 内置图片
- 海报模板
- 二维码背景
- 固定素材
- 需要转换成 PixelMap 的资源
- 不希望宿主感知 HAR 内部资源结构的场景
从系统 API 设计看
它并不理想。
因为:
资源读取
本来应该属于:
Resource API
结果我们借用了:
UI Snapshot API
来完成。
这相当于:
为了拿到厨房里的苹果,我们先让苹果做成一道菜,再从菜里把苹果捞出来。
能吃,甚至很好吃。
但显然不是最短路径。
十九、最终推荐的工程架构
因此,如果现在项目必须使用 HAR,而且当前 API 条件不允许直接获得 HAR 资源对应的 PixelMap,那么我建议采用:
┌───────────────────────────────┐
│ HAR │
│ │
│ resources/base/media │
│ ↓ │
│ Image($r(...)) │
└──────────────┬────────────────┘
│
│ UIContext
▼
ComponentSnapshot
│
▼
PixelMap
│
▼
ImagePacker
│
▼
filesDir/ai_images
│
│
┌───────▼────────┐
│ 业务图片层 │
│ │
│ getPixelMap() │
└───────┬────────┘
│
▼
图片合成
其中最重要的一条边界是:
资源获取边界
│
▼
HAR ──→ Image ──→ Snapshot ──→ 文件
│
│
业务处理边界 │
│ │
▼ ▼
PixelMap ←── 文件
前半段负责“把组件资源变成普通文件”,后半段负责“把普通文件当普通图片使用”。
这比让整个业务链路一直携带 UIContext 要合理得多。
二十、总结:真正需要理解的不是“怎么拿图片”
这次问题表面上是:
鸿蒙 HAR 里的图片怎么变成 PixelMap?
但深入以后其实是一个非常典型的鸿蒙组件化问题:
模块资源属于谁?Context 属于谁?UI 资源引用和文件资源访问之间是什么关系?
最终可以得到几个结论。
第一,HAR 是组件资源载体,不应该简单等同于宿主 HAP。
所以:
宿主 EntryAbilityContext
≠
HAR ResourceContext
不能想当然地认为宿主 Context 可以直接访问 HAR 内部所有资源。
第二,$r() 和 Context.resourceManager 解决的是不同层次的问题。
$r()
→ 资源引用/解析/UI使用
Context
→ 运行环境/文件/应用生命周期
不要把两者混成一个概念。
第三,ComponentSnapshot 提供了一条非常有价值的“UI → PixelMap”通道。
因此:
HAR Resource
→ Image
→ Snapshot
→ PixelMap
虽然不是资源访问的最优路径,但在当前组件化场景下是一条合理的工程路径。
第四,这个方案的核心不是“绕过 HAR”,而是“顺着鸿蒙 UI 体系把资源转换成标准图像数据”。
这也是它为什么能够工作。
第五,这个方案同时存在明显局限。
它依赖:
- UIContext
- Image 组件
- 正确的组件 ID
- 正确的 Image 尺寸
- 非透明渲染
- Snapshot 时机
- 一次额外的图像转换
所以它更适合作为:
HAR 资源向业务图片数据转换的适配层。
而不是整个项目通用的资源读取机制。
最后,如果未来鸿蒙提供:
HAR Resource
↓
ImageSource / PixelMap
这种不经过 UI 渲染层的直接资源数据访问能力,那么应该优先迁移到那条路径。
因为从系统设计角度看,最合理的终点始终应该是:
资源属于哪个模块
↓
由哪个模块的资源管理体系解析
↓
直接得到资源数据
而不是:
资源
↓
先画出来
↓
再截图
↓
再把截图当资源
我们现在的方案,是在鸿蒙现有组件化边界下找到的一条可行的桥;但这座桥解决的是工程问题,不代表它就是系统设计上的终点。
更多推荐




所有评论(0)