HarmonyOS 相机拍摄治理实战:权限、预览、拍照与异常恢复
HarmonyOS 相机拍摄治理实战:权限、预览、拍照与异常恢复
相机能力常用于头像、证件、报修照片、路线封面。它的风险点也很集中:没有权限时页面黑屏,预览会话没有释放导致再次进入失败,拍照文件为空,压缩后上传失败。相机治理不是只把系统相机拉起来,而是把权限、预览、拍照、输出文件和异常恢复做成闭环。

本文会讲清楚:相机权限如何解释,预览会话如何管理,拍照输出如何校验,页面退出和异常时如何释放资源。
1. 相机场景先分清入口
头像、工单照片、路线封面、扫码识别对相机的要求不同。头像重视裁剪,工单重视原图证据,路线封面重视压缩和尺寸。

| 场景 | 重点 | 失败兜底 |
|---|---|---|
| 头像 | 裁剪和压缩 | 从相册选择 |
| 工单照片 | 清晰度和原图 | 重新拍摄 |
| 路线封面 | 尺寸和比例 | 使用默认封面 |
| 扫码 | 实时预览 | 手动输入 |
2. Camera Kit 资料边界和目录
HarmonyOS 提供相机能力和媒体能力,应用需要处理权限、生命周期和资源释放。
| 资料入口 | 工程落点 |
|---|---|
| Camera Kit | 相机能力整体说明 |
| 相机开发指导 | 设备打开、会话和输出 |
| 应用权限 | 相机权限申请和说明 |
entry/src/main/ets/common/camera/
CameraGate.ets
PreviewSession.ets
CaptureTask.ets
ImageOutputGuard.ets
CameraRecovery.ets
3. CameraGate 判断权限和入口
权限模块只给出下一步,不直接打开相机。
export type CameraScene = 'avatar' | 'work_order' | 'route_cover'
export type CameraGateState = 'ALLOW' | 'NEED_PERMISSION' | 'UNAVAILABLE'
export interface CameraGateDecision {
state: CameraGateState
reason: string
fallbackAction: string
}
export class CameraGate {
decide(scene: CameraScene, permissionGranted: boolean, deviceReady: boolean): CameraGateDecision {
if (!deviceReady) {
return { state: 'UNAVAILABLE', reason: '当前设备相机不可用', fallbackAction: '从相册选择' }
}
if (!permissionGranted) {
return { state: 'NEED_PERMISSION', reason: `需要相机权限用于${scene === 'avatar' ? '拍摄头像' : '拍摄业务照片'}`, fallbackAction: '去授权' }
}
return { state: 'ALLOW', reason: '可以进入拍摄', fallbackAction: '继续' }
}
}
权限拒绝时要给替代入口,例如相册选择或手动上传。
4. PreviewSession 管理预览生命周期
预览会话必须跟页面生命周期绑定。页面退出、应用切后台或拍照结束,都要释放资源。
export type PreviewState = 'IDLE' | 'STARTING' | 'RUNNING' | 'STOPPED' | 'FAILED'
export class PreviewSession {
private state: PreviewState = 'IDLE'
async start(surfaceId: string): Promise<PreviewState> {
if (surfaceId.length === 0) {
this.state = 'FAILED'
return this.state
}
this.state = 'STARTING'
// 实际项目中这里创建相机会话并绑定预览输出。
this.state = 'RUNNING'
return this.state
}
async stop(): Promise<void> {
this.state = 'STOPPED'
}
}
不要让预览会话成为页面里的临时变量,否则异常路径很容易漏释放。
5. CaptureTask 输出拍照结果
拍照结果要校验 URI、大小、格式和时间。只要输出为空,就不能进入上传流程。
export interface CapturedImage {
uri: string
mimeType: 'image/jpeg' | 'image/png'
sizeBytes: number
width: number
height: number
capturedAt: number
}
export class CaptureTask {
async capture(scene: CameraScene): Promise<CapturedImage> {
return {
uri: `file://cache/${scene}_${Date.now()}.jpg`,
mimeType: 'image/jpeg',
sizeBytes: 2 * 1024 * 1024,
width: 1920,
height: 1080,
capturedAt: Date.now()
}
}
}
拍照任务只负责生成图片,不负责上传和业务提交。
6. ImageOutputGuard 校验图片边界
不同场景允许的大小不同。头像可以小一些,工单照片可以更大。

export class ImageOutputGuard {
accept(image: CapturedImage, scene: CameraScene): string[] {
const errors: string[] = []
const maxSize = scene === 'work_order' ? 8 * 1024 * 1024 : 3 * 1024 * 1024
if (!image.uri.startsWith('file://')) errors.push('图片 URI 不合法')
if (image.sizeBytes <= 0 || image.sizeBytes > maxSize) errors.push('图片大小超出限制')
if (image.width < 300 || image.height < 300) errors.push('图片分辨率过低')
return errors
}
}
校验失败时应让用户重拍或重新选择,不要把坏文件交给上传模块。
7. CameraRecovery 处理异常恢复
相机占用、权限变化、页面切后台都可能中断拍摄。
export interface CameraRecoveryPlan {
message: string
action: 'retry' | 'choose_album' | 'close'
}
export class CameraRecovery {
plan(errorCode: 'PERMISSION_LOST' | 'DEVICE_BUSY' | 'OUTPUT_EMPTY'): CameraRecoveryPlan {
const map: Record<string, CameraRecoveryPlan> = {
PERMISSION_LOST: { message: '相机权限已关闭,请重新授权', action: 'close' },
DEVICE_BUSY: { message: '相机被占用,请稍后重试', action: 'retry' },
OUTPUT_EMPTY: { message: '照片保存失败,请重新拍摄', action: 'retry' }
}
return map[errorCode]
}
}
异常恢复要给出明确动作,不能只弹一个失败提示。
8. 页面状态构建
export interface CameraViewState {
title: string
shutterEnabled: boolean
actionText: string
}
export function buildCameraView(preview: PreviewState): CameraViewState {
if (preview === 'RUNNING') return { title: '请对准拍摄对象', shutterEnabled: true, actionText: '拍照' }
if (preview === 'FAILED') return { title: '预览启动失败', shutterEnabled: false, actionText: '重试' }
return { title: '正在启动相机', shutterEnabled: false, actionText: '等待' }
}
按钮状态要跟预览状态绑定,预览未就绪时不要允许拍照。
9. 相机验收动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 未授权 | 打开拍照页 | 展示用途并引导授权 |
| 预览失败 | 模拟设备占用 | 提示重试或相册 |
| 拍照成功 | 点击快门 | 输出 URI、大小、分辨率合法 |
| 页面退出 | 拍照页返回 | 释放会话资源 |
| 大图上传 | 工单照片超限 | 提示压缩或重拍 |
export function assertCapturedImage(image: CapturedImage): void {
if (!image.uri || image.sizeBytes <= 0) throw new Error('拍照输出文件无效')
if (!['image/jpeg', 'image/png'].includes(image.mimeType)) throw new Error('图片格式不支持')
}
10. 相机异常排查表
相机问题要从权限、设备、会话、输出四层定位。只看拍照按钮日志通常不够。
| 现象 | 优先查看 | 处理建议 |
|---|---|---|
| 黑屏 | 预览会话状态 | 失败后释放并重建 |
| 拍照无文件 | 输出 URI 和大小 | 阻止上传并提示重拍 |
| 再次进入失败 | 资源释放 | 页面退出必须 stop |
| 权限拒绝 | 权限文案 | 给相册兜底 |
| 上传失败 | 图片大小格式 | 拍照后先校验 |
拍摄链路建议记录会话状态变化。尤其是“第一次能打开,第二次黑屏”这类问题,通常不是拍照失败,而是上一次预览会话没有释放。
export interface CameraStateRecord {
scene: CameraScene
from: PreviewState
to: PreviewState
reason: string
at: number
}
export class CameraStateLog {
private readonly records: CameraStateRecord[] = []
append(scene: CameraScene, from: PreviewState, to: PreviewState, reason: string): void {
this.records.push({ scene, from, to, reason, at: Date.now() })
}
recent(): CameraStateRecord[] {
return this.records.slice(-20)
}
}
有了状态记录,开发可以确认页面退出时是否走到 STOPPED,也能区分设备占用和输出文件异常。
相机异常恢复复现场景:给读者一组可执行核验
相机能力要验证权限拒绝、预览失败、拍照失败和资源释放。能拍一张照片,不代表页面生命周期安全。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface CameraReplayCase {
cameraId: any
previewReady: any
captureResult: any
released: any
}
const replay83: CameraReplayCase = {
cameraId: 'sample',
previewReady: 'sample',
captureResult: 'sample',
released: 'sample',
}
function assertReplay83(item: CameraReplayCase): void {
if (item.captureResult === 'success' && !item.released) throw new Error('拍照后资源未释放')
}
这组核验把相机预览、拍照和资源释放放在一起,适合发现页面退出后的设备占用问题。
相机生命周期回放表:把文章方法变成可复现动作
相机页面要重点验证资源释放。进入预览后返回、拍照后返回、权限弹窗后返回、切后台后返回,这四类动作都可能暴露资源占用问题。
| 回放动作 | 核验方式 |
|---|---|
| 预览退出释放 | 准备输入、执行操作、记录结果、给出结论 |
| 拍照后释放 | 准备输入、执行操作、记录结果、给出结论 |
| 拒绝授权不残留 | 准备输入、执行操作、记录结果、给出结论 |
| 切后台恢复正常 | 准备输入、执行操作、记录结果、给出结论 |
相机能力的重点是生命周期。读者应反复执行进入预览、拍照、返回、切后台、再进入这几步,并观察相机资源是否被释放、预览是否重新初始化、失败后按钮是否恢复可点。拍照成功只是链路的一部分,资源释放和异常恢复才决定页面是否稳定。
相机页面的落地边界:不要把边界留给读者猜
相机页面要把预览、拍照、裁剪、上传拆开。预览失败不应该影响相册选择,拍照失败不应该丢失页面状态,上传失败不应该占用相机资源。读者落地时把这些阶段分开,问题定位会清楚很多。
| 落地项 | 处理要求 |
|---|---|
| 预览只管理设备资源 | 需要有明确输入、处理边界和失败兜底 |
| 拍照只产出本地文件 | 需要有明确输入、处理边界和失败兜底 |
| 裁剪只处理图片尺寸 | 需要有明确输入、处理边界和失败兜底 |
| 上传只处理网络队列 | 需要有明确输入、处理边界和失败兜底 |
这类边界写清楚后,读者不需要猜哪些逻辑属于页面、哪些属于服务、哪些属于发布前验收。文章的价值也会从“讲了一个功能”变成“给了一套可迁移的工程判断”。
11. 小结:相机能力要重视释放和兜底
相机治理的关键是把权限、预览、拍照、输出校验和异常恢复分开。页面只展示状态,底层模块负责资源生命周期,失败时提供相册或重试入口。
更多推荐


所有评论(0)