@ohos-cpf/3rdloop 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/vendor/Server/Routes/controllers/LoopEngineController.js +36 -3
- package/vendor/Server/Routes/controllers/OrchestratorController.js +193 -9
- package/vendor/Server/Skills/flutter-build-test/SKILL.md +252 -0
- package/vendor/Server/Skills/flutter-build-test/assets/BUILDENV_TEMPLATE.md +42 -0
- package/vendor/Server/Skills/flutter-build-test/assets/BUILD_REPORT_TEMPLATE.md +64 -0
- package/vendor/Server/Skills/flutter-build-test/assets/README_SECTION_TEMPLATE.md +78 -0
- package/vendor/Server/Skills/flutter-build-test/references/BUILD_TROUBLESHOOTING.md +128 -0
- package/vendor/Server/Skills/flutter-build-test/references/DOC_UPDATE_GUIDE.md +119 -0
- package/vendor/Server/Skills/flutter-build-test/references/FLVM_GUIDE.md +89 -0
- package/vendor/Server/Skills/flutter-build-test/scripts/build-matrix.cjs +374 -0
- package/vendor/Server/Skills/flutter-build-test/scripts/locate-example.cjs +189 -0
- package/vendor/Server/Skills/flutter-build-test/scripts/update-buildenv.cjs +171 -0
- package/vendor/Server/Skills/flutter-code-use/SKILL.md +316 -0
- package/vendor/Server/Skills/flutter-code-use/references/event-channel.md +440 -0
- package/vendor/Server/Skills/flutter-code-use/references/federated.md +295 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-binding-translate.md +130 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-compile-from-source.md +169 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-fetch-at-build.md +161 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-prebuilt-bundle.md +175 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-rhttp-guide.md +235 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-rust-cross-compile.md +514 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi.md +220 -0
- package/vendor/Server/Skills/flutter-code-use/references/method-channel.md +643 -0
- package/vendor/Server/Skills/flutter-code-use/references/monorepo.md +188 -0
- package/vendor/Server/Skills/flutter-code-use/references/ohos-api-pitfalls.md +717 -0
- package/vendor/Server/Skills/flutter-code-use/references/platform-view.md +448 -0
- package/vendor/Server/Skills/flutter-code-use/references/pure-dart.md +180 -0
- package/vendor/Server/Skills/flutter-code-use/references/texture.md +459 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/SKILL.md +270 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/assets/PAGE_TEMPLATES.md +544 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/references/CODE_STANDARDS.md +328 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/references/DEMO_DOC_PARSING.md +126 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/references/EXAMPLES.md +629 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/scripts/validate-flutter-demo.cjs +268 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/SKILL.md +227 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/assets/DEMO_DOC_TEMPLATE.md +78 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/COVERAGE_REPORT_PARSING.md +174 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/EXAMPLES.md +162 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/MCP_TOOL_GUIDE.md +123 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/OUTPUT_FORMAT.md +190 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/QUALITY_CHECKLIST.md +83 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/scripts/validate-skill.cjs +259 -0
- package/vendor/Server/Skills/flutter-library-demo-coverage/SKILL.md +175 -0
- package/vendor/VERSION +3 -3
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# OHOS API 常见陷阱
|
|
2
|
+
|
|
3
|
+
> 本文件汇集 HarmonyOS API 在 Flutter 插件适配中容易踩坑的场景。这些问题通常不会导致编译失败,但会导致运行时功能异常(无声、卡死、回环、行为不符合预期等),是真机调试中最常见的问题来源。
|
|
4
|
+
> **使用时机**:在 `flutter-code-use` 主 Skill 编码阶段,如果插件涉及以下场景,必须阅读对应章节并遵循规则。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 音频 — SoundPool
|
|
9
|
+
|
|
10
|
+
### 1.1 SoundPool.play() 必须显式传入 PlayParameters 和音量
|
|
11
|
+
|
|
12
|
+
**陷阱**:SoundPool.play() 不传 PlayParameters 或不设置 leftVolume/rightVolume 时,默认音量可能为 0,表现为"播放成功、回调正常触发,但设备无声音输出"。这个问题极难排查,因为所有 API 调用层面都返回成功。
|
|
13
|
+
|
|
14
|
+
**正确做法**:
|
|
15
|
+
|
|
16
|
+
```ets
|
|
17
|
+
import { media } from '@kit.MediaKit';
|
|
18
|
+
import { audio } from '@kit.AudioKit';
|
|
19
|
+
|
|
20
|
+
// 创建 SoundPool 时指定音频流类型
|
|
21
|
+
const audioRendererInfo: audio.AudioRendererInfo = {
|
|
22
|
+
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
|
|
23
|
+
rendererFlags: 0
|
|
24
|
+
};
|
|
25
|
+
const soundPool = await media.createSoundPool(1, audioRendererInfo);
|
|
26
|
+
|
|
27
|
+
// 播放时必须显式设置音量
|
|
28
|
+
const playParams: media.PlayParameters = {
|
|
29
|
+
loop: 0, // 不循环
|
|
30
|
+
rate: 1, // 正常速度
|
|
31
|
+
leftVolume: 1.0, // 左声道音量(0.0-1.0)
|
|
32
|
+
rightVolume: 1.0, // 右声道音量(0.0-1.0)
|
|
33
|
+
priority: 0 // 正常优先级
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// 使用 callback 形式(见 1.2)
|
|
37
|
+
soundPool.play(soundId, playParams, (error, streamId) => {
|
|
38
|
+
if (error) {
|
|
39
|
+
hilog.error(LOG_DOMAIN, LOG_TAG, 'play failed: %{public}s', error.message);
|
|
40
|
+
} else {
|
|
41
|
+
hilog.debug(LOG_DOMAIN, LOG_TAG, 'play streamId: %{public}d', streamId);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**禁止**:`soundPool.play(soundId)` — 不传 PlayParameters 会导致静音播放。
|
|
47
|
+
|
|
48
|
+
### 1.2 SoundPool API 优先使用 callback 形式
|
|
49
|
+
|
|
50
|
+
**陷阱**:SoundPool 的 Promise 形式(`await soundPool.play(...)`)和 callback 形式在 HarmonyOS 上行为存在差异。Promise 形式可能出现播放不稳定的情况。
|
|
51
|
+
|
|
52
|
+
**正确做法**:优先使用 callback 形式调用 SoundPool.play()、SoundPool.load() 等方法:
|
|
53
|
+
|
|
54
|
+
```ets
|
|
55
|
+
// ✅ 推荐:callback 形式
|
|
56
|
+
soundPool.play(soundId, playParams, (error, streamId) => { ... });
|
|
57
|
+
|
|
58
|
+
// ⚠️ 不推荐:Promise 形式(可能不稳定)
|
|
59
|
+
// const streamId = await soundPool.play(soundId, playParams);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### 1.3 `load()` 成功不等于“已经可以播放”
|
|
64
|
+
|
|
65
|
+
**陷阱**:`soundPool.load(...)` 的 callback / Promise 只表示“已经拿到了 `soundId`”,不表示底层资源已经完成解码和装载。
|
|
66
|
+
如果插件把 `load()` 成功直接当成“已加载完成”,随后立刻 `play()`,常见现象就是:
|
|
67
|
+
|
|
68
|
+
- `soundID(...) has not been loaded completely`
|
|
69
|
+
- `play sound failed`
|
|
70
|
+
- Dart 方法返回成功,但设备没有声音
|
|
71
|
+
|
|
72
|
+
**正确做法**:必须监听 `on('loadComplete')`,只有收到该回调后,才把对应音效标记为真正可播。
|
|
73
|
+
|
|
74
|
+
```ets
|
|
75
|
+
private loadedFlags: Map<string, boolean> = new Map();
|
|
76
|
+
private soundNamesById: Map<number, string> = new Map();
|
|
77
|
+
|
|
78
|
+
soundPool.on('loadComplete', (soundId: number) => {
|
|
79
|
+
const soundName = this.soundNamesById.get(soundId);
|
|
80
|
+
if (soundName !== undefined) {
|
|
81
|
+
this.loadedFlags.set(soundName, true);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
soundPool.load(fd, offset, length, (error, soundId) => {
|
|
86
|
+
if (!error) {
|
|
87
|
+
this.soundNamesById.set(soundId, 'shutter');
|
|
88
|
+
// 这里只是拿到 soundId,还不能立刻 play()
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 2. 音频 — AVPlayer 状态机
|
|
97
|
+
|
|
98
|
+
### 2.1 prepare 阶段不能直接调用 play/seek
|
|
99
|
+
|
|
100
|
+
**陷阱**:AVPlayer 有严格的状态机(`idle → initialized → prepared → playing ↔ paused → stopped → released`)。在 `prepare()` 尚未完成时直接调用 `play()` 或 `seek()`,不会抛异常但会静默失败或导致状态不一致。
|
|
101
|
+
|
|
102
|
+
**正确做法**:在 `prepare` 阶段收到的 `play/seek` 请求需要排队/缓存,待 `prepared` 状态回调后再执行:
|
|
103
|
+
|
|
104
|
+
```ets
|
|
105
|
+
private pendingPlay: boolean = false;
|
|
106
|
+
private pendingSeekMs: number = -1;
|
|
107
|
+
private isPrepared: boolean = false;
|
|
108
|
+
|
|
109
|
+
// 监听状态变化
|
|
110
|
+
avPlayer.on('stateChange', (state: string) => {
|
|
111
|
+
if (state === 'prepared') {
|
|
112
|
+
this.isPrepared = true;
|
|
113
|
+
if (this.pendingSeekMs >= 0) {
|
|
114
|
+
avPlayer.seek(this.pendingSeekMs);
|
|
115
|
+
this.pendingSeekMs = -1;
|
|
116
|
+
}
|
|
117
|
+
if (this.pendingPlay) {
|
|
118
|
+
avPlayer.play();
|
|
119
|
+
this.pendingPlay = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// play/seek 方法先检查状态
|
|
125
|
+
play(): void {
|
|
126
|
+
if (this.isPrepared) {
|
|
127
|
+
this.avPlayer.play();
|
|
128
|
+
} else {
|
|
129
|
+
this.pendingPlay = true; // 缓存请求
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 2.2 Dart 层 onError 不应再调用平台 stop()
|
|
135
|
+
|
|
136
|
+
**陷阱**:Dart 层收到 `player.onError` 回调后如果再主动调用平台 `stop()`,会把一次底层错误放大成新的停止请求,引发 `preload → seek(0) → stop → play → stop` 的回环,用户看到的表现是"点击播放无反应"。
|
|
137
|
+
|
|
138
|
+
**正确做法**:Dart 层 `_onError` 回调中只做本地状态更新(设为 STOPPED、复位位置),不要再通过 MethodChannel 调用原生 `stop()`。
|
|
139
|
+
|
|
140
|
+
### 2.3 player.onError 载荷结构必须与 Dart 端一致
|
|
141
|
+
|
|
142
|
+
**陷阱**:OHOS 原生 `player.onError` 回传的错误载荷结构如果和 Dart 预期不一致(如多了/少了字段),Dart 侧解析失败后可能触发二次异常,掩盖真正错误来源。
|
|
143
|
+
|
|
144
|
+
**正确做法**:与 Android/iOS 端对齐错误回调的 Map 结构,确保 Dart 层 `_onError` 可以正常解析。
|
|
145
|
+
|
|
146
|
+
### 2.4 audioRendererInfo 必须在 prepare 前设置
|
|
147
|
+
|
|
148
|
+
创建 AVPlayer 后,如果涉及音频播放,建议在 `prepare()` 之前设置 `audioRendererInfo`,减少 AVPlayer 初始化期的不确定性。
|
|
149
|
+
|
|
150
|
+
### 2.5 后台播放语义必须生成后台能力闭环
|
|
151
|
+
|
|
152
|
+
如果插件原 API 含有后台播放、锁屏播放、保活播放或 `stayAwake` 等语义,生成 OHOS AVPlayer 代码时不能只生成前台 `AVPlayer.play()`,必须同步生成后台播放能力闭环。
|
|
153
|
+
|
|
154
|
+
代码侧至少包含:
|
|
155
|
+
|
|
156
|
+
- 实现 `AbilityAware` 获取真实 `UIAbilityContext`
|
|
157
|
+
- 播放开始前启动音频后台任务并创建/激活 `AVSession`
|
|
158
|
+
- 暂停、停止、释放、销毁时停止后台任务并释放 `AVSession`
|
|
159
|
+
- `module.json5` 声明对应后台运行权限和音频后台模式
|
|
160
|
+
|
|
161
|
+
原则:有后台播放语义,就生成后台能力;没有后台能力,就不要实现成看似支持后台播放。
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 3. UI 提示 — Toast vs CustomDialog
|
|
166
|
+
|
|
167
|
+
### 3.1 优先使用系统 promptAction.showToast() 而非 CustomDialog
|
|
168
|
+
|
|
169
|
+
**陷阱**:为了保留样式控制(fontSize、背景色、文字色等),Agent 可能选用 `CustomDialog` / `openCustomDialog` 来实现 Toast 功能。这会引入不必要的弹窗生命周期管理(timer、dialog id、@Builder),而且行为与系统轻提示不一致。
|
|
170
|
+
|
|
171
|
+
**正确做法**:
|
|
172
|
+
|
|
173
|
+
```ets
|
|
174
|
+
import { promptAction } from '@kit.ArkUI';
|
|
175
|
+
|
|
176
|
+
// 系统 Toast — 简洁且行为一致
|
|
177
|
+
promptAction.showToast({
|
|
178
|
+
message: msg,
|
|
179
|
+
duration: length === 1 ? 5000 : 2000, // LONG=5s, SHORT=2s
|
|
180
|
+
alignment: Alignment.Bottom, // 位置
|
|
181
|
+
backgroundColor: bgColor, // 背景色
|
|
182
|
+
textColor: textColor, // 文字色
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**决策规则**:
|
|
187
|
+
- 如果原插件只需要显示文本+时长+位置 → 用 `promptAction.showToast()`
|
|
188
|
+
- 如果原插件需要自定义字号、富文本、主动关闭等 `showToast()` 不支持的能力 → 才用 `CustomDialog`
|
|
189
|
+
|
|
190
|
+
### 3.2 系统 API 能力边界受 compatibleSdkVersion 约束
|
|
191
|
+
|
|
192
|
+
**陷阱**:`promptAction.showToast()` 的可选参数(如 `backgroundColor`、`textColor`、`alignment`)在不同 API Level 下支持程度不同。编码前必须核对当前工程 `compatibleSdkVersion` 对 API 的支持范围。
|
|
193
|
+
|
|
194
|
+
**正确做法**:
|
|
195
|
+
1. 查看 `example/ohos/build-profile.json5` 中的 `compatibleSdkVersion` 值
|
|
196
|
+
2. 通过 `sub-doc-search` 或 `read_file` .d.ts 确认 API 参数的 `@since` 版本
|
|
197
|
+
3. 对当前 SDK 版本不支持的参数,保留参数接收但做降级处理(忽略该值),不要伪造实现
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 4. 异步 API — callback vs Promise
|
|
202
|
+
|
|
203
|
+
### 4.1 部分 OHOS API 的 callback 和 Promise 形式行为不一致
|
|
204
|
+
|
|
205
|
+
**陷阱**:HarmonyOS 部分 API(如 SoundPool、部分文件操作)的 Promise 形式和 callback 形式在错误处理、时序、回调触发上可能存在差异。官方文档示例通常使用 callback 形式。
|
|
206
|
+
|
|
207
|
+
**正确做法**:
|
|
208
|
+
- 优先参考官方文档示例的调用形式
|
|
209
|
+
- 如果官方示例使用 callback 形式,优先使用 callback
|
|
210
|
+
- 如果 Promise 形式出现不稳定行为,切换到 callback 形式
|
|
211
|
+
- 通过 `sub-doc-search` 搜索具体 API 的官方示例确认推荐用法
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 5. MethodChannel 参数安全
|
|
216
|
+
|
|
217
|
+
### 5.1 call.args 可能为 null
|
|
218
|
+
|
|
219
|
+
**陷阱**:部分 MethodChannel 调用不携带参数(如生命周期回调、无参查询方法),此时 `call.args` 为 null。如果代码直接把 `call.args` 当作非空 Map 使用(如 `call.args.get("key")`),会触发 `Cannot read property get of null` 运行时崩溃。
|
|
220
|
+
|
|
221
|
+
**正确做法**:
|
|
222
|
+
|
|
223
|
+
```ets
|
|
224
|
+
onMethodCall(call: MethodCall, result: MethodResult): void {
|
|
225
|
+
switch (call.method) {
|
|
226
|
+
case "methodWithArgs": {
|
|
227
|
+
// 先检查 args 是否存在
|
|
228
|
+
const args = call.args as Map<string, Object> | null;
|
|
229
|
+
if (args === null || args === undefined) {
|
|
230
|
+
result.error("INVALID_ARGS", "Arguments are required", null);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const value = args.get("key") as string;
|
|
234
|
+
// ...
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "methodWithoutArgs": {
|
|
238
|
+
// 无参方法不要访问 call.args
|
|
239
|
+
result.success("ok");
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 6. 传感器 — SensorResponse 必须属性访问
|
|
249
|
+
|
|
250
|
+
**陷阱**:OHOS `SensorServiceKit` 回调数据通过具名属性提供(`alpha/beta/gamma`、`x/y/z`),不是数组。`data[0]` 返回 `undefined`,界面数值不变。
|
|
251
|
+
|
|
252
|
+
```ets
|
|
253
|
+
sensor.on(sensor.SensorId.ORIENTATION, (data: sensor.OrientationResponse) => {
|
|
254
|
+
const heading = data.alpha; // ✅ 属性访问
|
|
255
|
+
// const heading = data[0]; // ❌ undefined
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
适用所有 SensorResponse 子类:ORIENTATION(`alpha/beta/gamma`)、ACCELEROMETER/GYROSCOPE/MAGNETIC_FIELD(`x/y/z`)。
|
|
260
|
+
|
|
261
|
+
### 6.2 后台禁止调用传感器
|
|
262
|
+
|
|
263
|
+
**陷阱**:只在 `onCancel()`、`onDetachedFromEngine()` 等销毁路径里停止传感器还不够。若应用退到后台后可能继续收到传感器回调,造成后台耗电和隐私风险。
|
|
264
|
+
|
|
265
|
+
**正确做法**:
|
|
266
|
+
- 维护应用/窗口是否处于前台可用状态,只有在前台时才使用传感器
|
|
267
|
+
- 应用退后台、窗口失焦、Ability detach 时立即 `sensor.off(...)`
|
|
268
|
+
- 回到前台后恢复 `sensor.on(...)`
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 7. hilog — 浮点数用 `%{public}s` + `String()`
|
|
273
|
+
|
|
274
|
+
**陷阱**:`%{public}f` 在部分 SDK 版本下输出空字符串或乱码。
|
|
275
|
+
|
|
276
|
+
```ets
|
|
277
|
+
hilog.info(LOG_DOMAIN, LOG_TAG, 'val=%{public}s', String(floatVal)); // ✅
|
|
278
|
+
// hilog.info(LOG_DOMAIN, LOG_TAG, 'val=%{public}f', floatVal); // ❌
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 8. bundleManager — 读 metadata 必须含 `GET_BUNDLE_INFO_WITH_HAP_MODULE`
|
|
284
|
+
|
|
285
|
+
**陷阱**:metadata 挂在 `HapModuleInfo` 下。只用 `GET_BUNDLE_INFO_WITH_APPLICATION | GET_BUNDLE_INFO_WITH_METADATA` 时 `hapModulesInfo` 为空数组,读不到任何 metadata。
|
|
286
|
+
|
|
287
|
+
```ets
|
|
288
|
+
// ✅ 必须包含 GET_BUNDLE_INFO_WITH_HAP_MODULE
|
|
289
|
+
const bundleInfo = await bundleManager.getBundleInfoForSelf(
|
|
290
|
+
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE |
|
|
291
|
+
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_METADATA
|
|
292
|
+
);
|
|
293
|
+
for (const hapModule of bundleInfo.hapModulesInfo) {
|
|
294
|
+
for (const meta of hapModule.metadata) {
|
|
295
|
+
if (meta.name === 'my_channel_key') { /* meta.value */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 9. 平台敏感默认模式
|
|
301
|
+
|
|
302
|
+
### 不要把 default / platformDefault / auto 留给 OHOS 运行时决定
|
|
303
|
+
|
|
304
|
+
平台敏感能力(浏览器/WebView、分享、Picker、权限页、设置页、扫码/拍照/录音、文件选择等)在 OHOS 分支不要保留 `default` / `platformDefault` / `auto` / `system`。必须显式选择:外部应用、应用内页面、系统面板、系统 Picker、自定义页面、PlatformView/XComponent、Ability 跳转等;需要页面路由、headers、options、容器或权限前置流程时同步补齐。
|
|
305
|
+
|
|
306
|
+
`url_launcher` 普通 http/https URL:默认意图按外部浏览器处理,不要保留 `LaunchMode.platformDefault`:
|
|
307
|
+
|
|
308
|
+
```dart
|
|
309
|
+
final mode = Platform.operatingSystem == 'ohos' &&
|
|
310
|
+
requestedMode == LaunchMode.platformDefault
|
|
311
|
+
? LaunchMode.externalApplication
|
|
312
|
+
: requestedMode;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
如明确要求应用内 WebView:补 OHOS WebView 页面/路由,并传 `harmony_browser_page`:
|
|
316
|
+
|
|
317
|
+
```dart
|
|
318
|
+
WebViewConfiguration(
|
|
319
|
+
headers: {'harmony_browser_page': 'pages/BrowserPage'},
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
禁止因为能编译就依赖运行时默认映射。
|
|
324
|
+
|
|
325
|
+
## 10. 加密与安全 — cryptoFramework
|
|
326
|
+
|
|
327
|
+
### 10.1 算法字符串格式必须符合 OHOS 规范
|
|
328
|
+
|
|
329
|
+
**陷阱**:OHOS cryptoFramework 的 `createSign`/`createVerify`/`createCipher` 等方法对算法字符串有特定格式要求,不能沿用 Android/iOS 格式。
|
|
330
|
+
|
|
331
|
+
| 操作类型 | Android/iOS 格式 | OHOS 格式 |
|
|
332
|
+
|---------|-----------------|-----------|
|
|
333
|
+
| 签名验签 | `SHA1withRSA` | `RSA{keySize}|PKCS1|SHA1` |
|
|
334
|
+
| 加解密 | `RSA/ECB/PKCS1Padding` | `RSA|PKCS1` |
|
|
335
|
+
|
|
336
|
+
**强制检查**:
|
|
337
|
+
- 编码前通过 `sub-doc-search` 搜索 `cryptoFramework` + 方法名确认格式
|
|
338
|
+
- 密钥长度必须从实际密钥推断或 Dart 参数获取
|
|
339
|
+
|
|
340
|
+
**禁止**:直接沿用其他平台的算法字符串格式。
|
|
341
|
+
|
|
342
|
+
### 10.2 非标准 RSA 操作不支持
|
|
343
|
+
|
|
344
|
+
**陷阱**:部分 RSA 用法在其他平台可行但 OHOS 不支持:
|
|
345
|
+
|
|
346
|
+
| 操作 | Android | iOS | OHOS |
|
|
347
|
+
|------|---------|-----|------|
|
|
348
|
+
| 公钥解密 | ✓ | ✗ | ✗ |
|
|
349
|
+
| 私钥加密 | ✓ | ✗ | ✗ |
|
|
350
|
+
|
|
351
|
+
OHOS `Cipher.init(DECRYPT_MODE, PubKey)` 不支持,会报 init failed。
|
|
352
|
+
|
|
353
|
+
**处理方式**:发现不支持的操作 → `result.notImplemented()` + 写入 `not_implemented` + `risk_items`(severity: high)
|
|
354
|
+
|
|
355
|
+
**禁止**:伪造实现或静默返回空值。
|
|
356
|
+
|
|
357
|
+
### 10.3 字符串与二进制转换必须用 TextEncoder/TextDecoder
|
|
358
|
+
|
|
359
|
+
**陷阱**:使用 `charCodeAt`/`String.fromCharCode` 处理 UTF-8 多字节文本(汉字、emoji)会截断或乱码。`charCodeAt()` 只返回 0-255 单字节值。
|
|
360
|
+
|
|
361
|
+
**正确做法**:
|
|
362
|
+
|
|
363
|
+
```ets
|
|
364
|
+
import { util } from '@kit.ArkTS';
|
|
365
|
+
|
|
366
|
+
// 编码:字符串 → Uint8Array
|
|
367
|
+
const encoder = new util.TextEncoder();
|
|
368
|
+
const bytes = encoder.encodeInto(str);
|
|
369
|
+
|
|
370
|
+
// 解码:Uint8Array → 字符串
|
|
371
|
+
const decoder = new util.TextDecoder('utf-8');
|
|
372
|
+
const str = decoder.decodeToString(bytes);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**禁止**:对可能含多字节字符的文本使用 `charCodeAt` 或 `String.fromCharCode`。
|
|
376
|
+
|
|
377
|
+
**适用场景**:RSA 加密、签名验签、哈希计算、网络传输等所有文本与二进制互转场景。
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 11. Window / WindowStage 生命周期
|
|
382
|
+
|
|
383
|
+
### 11.1 不能只在 attach 时初始化一次 `mainWindow`
|
|
384
|
+
|
|
385
|
+
**陷阱**:依赖 `window.Window` 的插件(如状态栏、导航栏、全屏、避让区)如果只在 `onAttachedToAbility()` 里获取一次 `mainWindow`,很容易因为 `windowStage` 还没准备好而拿到 `null`。
|
|
386
|
+
|
|
387
|
+
**正确做法**:
|
|
388
|
+
- 保留 `UIAbility` 引用,但不要把 `mainWindow` 当成启动时一次拿好、后续永久可用的静态资源
|
|
389
|
+
- 业务方法调用时再懒获取窗口,优先走 `FlutterManager -> windowStage -> mainWindow`
|
|
390
|
+
- 若主路径暂时拿不到,再回退 `window.getLastWindow(context)`。依赖窗口的方法,统一先确保主窗口可用,再执行实际业务逻辑
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 12. Share Kit — AbilityAware 与 UTD 类型
|
|
395
|
+
|
|
396
|
+
### 12.0 Share Kit 必须通过 AbilityAware 获取 UIAbilityContext
|
|
397
|
+
|
|
398
|
+
**陷阱**:`ShareController.show()` 的第一个参数是 `common.UIAbilityContext`。Agent 可能错误认为"HAR 模块无法获取 UIAbilityContext",从而将所有分享方法标记为 `notImplemented()`。
|
|
399
|
+
|
|
400
|
+
**正确做法**:HAR 插件可以通过 `AbilityAware` 接口获取真实 `UIAbilityContext`。插件类必须 `implements AbilityAware`,在 `onAttachedToAbility(binding)` 中通过 `binding.getAbility().context` 获取:
|
|
401
|
+
|
|
402
|
+
```ets
|
|
403
|
+
import {
|
|
404
|
+
FlutterPlugin, FlutterPluginBinding, MethodCall, MethodCallHandler,
|
|
405
|
+
MethodChannel, MethodResult, AbilityAware, AbilityPluginBinding,
|
|
406
|
+
} from '@ohos/flutter_ohos';
|
|
407
|
+
import { common } from '@kit.AbilityKit';
|
|
408
|
+
import { systemShare } from '@kit.ShareKit';
|
|
409
|
+
|
|
410
|
+
export default class SharePlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
|
|
411
|
+
private context: common.UIAbilityContext | null = null;
|
|
412
|
+
|
|
413
|
+
onAttachedToAbility(binding: AbilityPluginBinding): void {
|
|
414
|
+
const ability = binding.getAbility();
|
|
415
|
+
if (ability !== null && ability !== undefined) {
|
|
416
|
+
this.context = ability.context as common.UIAbilityContext;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
onDetachedFromAbility(): void {
|
|
421
|
+
this.context = null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async showSharePanel(msg: string, result: MethodResult): Promise<void> {
|
|
425
|
+
if (this.context === null) {
|
|
426
|
+
result.error('NO_CONTEXT', 'UIAbilityContext not available', null);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const shareData = new systemShare.SharedData({
|
|
430
|
+
utd: 'general.text',
|
|
431
|
+
content: msg,
|
|
432
|
+
});
|
|
433
|
+
const controller = new systemShare.ShareController(shareData);
|
|
434
|
+
await controller.show(this.context, {
|
|
435
|
+
selectionMode: systemShare.SelectionMode.SINGLE,
|
|
436
|
+
previewMode: systemShare.SharePreviewMode.DEFAULT,
|
|
437
|
+
});
|
|
438
|
+
result.success('SUCCESS');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**禁止**:
|
|
444
|
+
- 以"HAR 模块无法获取 UIAbilityContext"为由放弃实现分享功能
|
|
445
|
+
- 把 `binding.getApplicationContext()` 强转为 `UIAbilityContext`
|
|
446
|
+
|
|
447
|
+
### 12.1 UTD 类型字符串必须使用正确值
|
|
448
|
+
|
|
449
|
+
**陷阱**:Share Kit 的 `SharedRecord` 要求 `utd` 字段为 HarmonyOS 统一数据类型(UTD)标识符。常见错误是使用 `'general.plain-text'`,这不是合法的 UTD 值,会导致 `Invalid record` / `WriteToWantParams failed` 运行时错误。
|
|
450
|
+
|
|
451
|
+
**正确 UTD 值对照**:
|
|
452
|
+
|
|
453
|
+
| 数据类型 | 正确 UTD 值 | 常见错误值 |
|
|
454
|
+
|---------|------------|-----------|
|
|
455
|
+
| 纯文本 | `'general.text'` | `'general.plain-text'` ❌ |
|
|
456
|
+
| JPEG 图片 | `'general.jpeg'` | `'image/jpeg'` ❌ |
|
|
457
|
+
| PNG 图片 | `'general.png'` | `'image/png'` ❌ |
|
|
458
|
+
| GIF 图片 | `'general.gif'` | — |
|
|
459
|
+
| WebP 图片 | `'general.webp'` | — |
|
|
460
|
+
| BMP 图片 | `'general.bmp'` | — |
|
|
461
|
+
| 通用图片 | `'general.image'` | `'image/*'` ❌ |
|
|
462
|
+
| PDF 文件 | `'com.adobe.pdf'` | `'application/pdf'` ❌ |
|
|
463
|
+
|
|
464
|
+
**禁止**:使用 MIME 类型格式(`image/jpeg`、`text/plain`)作为 UTD 值——OHOS UTD 体系与 MIME 不同。
|
|
465
|
+
|
|
466
|
+
### 12.2 SharedRecord 必须用内联对象字面量构造
|
|
467
|
+
|
|
468
|
+
**陷阱**:使用 `as systemShare.SharedRecord` 类型断言构造 SharedRecord 对象会跳过运行时类型检查,导致对象内部结构不完整,系统分享时抛出 `Invalid record` 或 `Parameter error`。
|
|
469
|
+
|
|
470
|
+
```ets
|
|
471
|
+
// ❌ 禁止:类型断言
|
|
472
|
+
const record = { utd: 'general.text', content: msg } as systemShare.SharedRecord;
|
|
473
|
+
|
|
474
|
+
// ✅ 正确:通过构造函数或 addRecord 内联对象
|
|
475
|
+
const shareData = new systemShare.SharedData({
|
|
476
|
+
utd: 'general.text',
|
|
477
|
+
content: msg,
|
|
478
|
+
});
|
|
479
|
+
// 或
|
|
480
|
+
shareData.addRecord({
|
|
481
|
+
utd: 'general.text',
|
|
482
|
+
content: msg,
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 12.3 ArkTS 对象字面量禁止给属性赋 undefined
|
|
487
|
+
|
|
488
|
+
**陷阱**:ArkTS 对 `undefined` 在对象字面量中的处理与 TypeScript 不同。`{ title: undefined }` 可能导致属性存在但值非法,引发 Share Kit 内部校验失败。
|
|
489
|
+
|
|
490
|
+
**正确做法**:当可选属性值不存在时,用条件分支构造不同的对象,而不是赋值 `undefined`:
|
|
491
|
+
|
|
492
|
+
```ets
|
|
493
|
+
// ❌ 禁止
|
|
494
|
+
const shareData = new systemShare.SharedData({
|
|
495
|
+
utd: 'general.text',
|
|
496
|
+
content: message,
|
|
497
|
+
title: title ?? undefined, // 不要这样写
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ✅ 正确:条件分支
|
|
501
|
+
let shareData: systemShare.SharedData;
|
|
502
|
+
if (title) {
|
|
503
|
+
shareData = new systemShare.SharedData({
|
|
504
|
+
utd: 'general.text',
|
|
505
|
+
content: message,
|
|
506
|
+
title: title,
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
shareData = new systemShare.SharedData({
|
|
510
|
+
utd: 'general.text',
|
|
511
|
+
content: message,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### 12.4 HAR 模块中使用 uniformTypeDescriptor 的注意事项
|
|
517
|
+
|
|
518
|
+
**注意**:`import { uniformTypeDescriptor as utd } from '@kit.ArkData'` 在大多数 HAR 模块中可以正常工作。如果编译或运行时出现模块找不到的错误,回退为直接使用 UTD 字符串常量:
|
|
519
|
+
|
|
520
|
+
| 用途 | `uniformTypeDescriptor` 枚举 | 等价字符串常量 |
|
|
521
|
+
|------|-------------------------------|---------------|
|
|
522
|
+
| 纯文本 | `utd.UniformDataType.TEXT` | `'general.text'` |
|
|
523
|
+
| 通用图片 | `utd.UniformDataType.IMAGE` | `'general.image'` |
|
|
524
|
+
| 超链接 | `utd.UniformDataType.HYPERLINK` | `'general.hyperlink'` |
|
|
525
|
+
|
|
526
|
+
优先使用 `uniformTypeDescriptor` 枚举(类型安全、IDE 补全);仅在枚举不可用时回退到字符串常量。
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## 13. MediaKit — AVFileDescriptor 与 AVTranscoder
|
|
531
|
+
|
|
532
|
+
### 13.1 AVFileDescriptor 必须包含 offset 和 length
|
|
533
|
+
|
|
534
|
+
**陷阱**:`AVFileDescriptor` 的 `.d.ts` 声明中 `offset` 和 `length` 标记为可选(`?`),但 `AVTranscoder`、`AVMetadataExtractor`、`AVImageGenerator` 等组件的运行时实现**强制要求**这两个字段。仅传 `{ fd }` 会报 `can not find offset property` / `can not find length property`,随后触发 `prepare()` 参数校验失败(errCode 401)。
|
|
535
|
+
|
|
536
|
+
**正确做法**:
|
|
537
|
+
|
|
538
|
+
```ets
|
|
539
|
+
import { fileIo } from '@kit.CoreFileKit';
|
|
540
|
+
|
|
541
|
+
// 获取文件大小
|
|
542
|
+
const stat = fileIo.statSync(filePath);
|
|
543
|
+
const fileSize = stat.size;
|
|
544
|
+
|
|
545
|
+
// 打开文件
|
|
546
|
+
const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
|
|
547
|
+
|
|
548
|
+
// ✅ 必须包含 offset 和 length
|
|
549
|
+
metadataExtractor.fdSrc = { fd: file.fd, offset: 0, length: fileSize };
|
|
550
|
+
transcoder.fdSrc = { fd: file.fd, offset: 0, length: fileSize };
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**禁止**:
|
|
554
|
+
```ets
|
|
555
|
+
// ❌ 运行时必定报错
|
|
556
|
+
metadataExtractor.fdSrc = { fd: file.fd };
|
|
557
|
+
transcoder.fdSrc = { fd: file.fd };
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**适用范围**:所有使用 `AVFileDescriptor` 类型的 `fdSrc` 赋值场景,包括 `AVPlayer`、`AVMetadataExtractor`、`AVImageGenerator`、`AVTranscoder`。
|
|
561
|
+
|
|
562
|
+
### 13.2 AVTranscoderConfig 视频尺寸必须为偶数且在合法范围内
|
|
563
|
+
|
|
564
|
+
**陷阱**:`AVTranscoderConfig` 的 `videoFrameWidth` 合法范围为 [240-3840],`videoFrameHeight` 合法范围为 [240-2160]。此外,H.264/AVC 和 H.265/HEVC 编码器要求宽高**必须为偶数**。不满足任一条件都会导致 `prepare()` 报 `Parameter verification failed`(errCode 401),且错误信息中不会指明具体哪个参数有问题。
|
|
565
|
+
|
|
566
|
+
**常见触发场景**:
|
|
567
|
+
- 按比例缩放后用 `Math.floor()` 得到奇数(如 1920×0.4444 = 853)
|
|
568
|
+
- 原始视频尺寸极小或元数据解析失败返回 0,直接传入导致低于最小值 240
|
|
569
|
+
|
|
570
|
+
**正确做法**:
|
|
571
|
+
|
|
572
|
+
```ets
|
|
573
|
+
// 缩放后确保偶数 + 合法范围
|
|
574
|
+
let width = Math.floor(originalWidth * ratio);
|
|
575
|
+
let height = Math.floor(originalHeight * ratio);
|
|
576
|
+
|
|
577
|
+
// 向下取偶数(视频编码器要求)
|
|
578
|
+
width = width - (width % 2);
|
|
579
|
+
height = height - (height % 2);
|
|
580
|
+
|
|
581
|
+
// 钳制到 AVTranscoderConfig 合法范围
|
|
582
|
+
width = Math.max(240, Math.min(3840, width));
|
|
583
|
+
height = Math.max(240, Math.min(2160, height));
|
|
584
|
+
|
|
585
|
+
const config: media.AVTranscoderConfig = {
|
|
586
|
+
fileFormat: media.ContainerFormatType.CFT_MPEG_4,
|
|
587
|
+
videoFrameWidth: width,
|
|
588
|
+
videoFrameHeight: height,
|
|
589
|
+
// ...
|
|
590
|
+
};
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**元数据无效时的降级策略**:当 `AVMetadataExtractor.fetchMetadata()` 返回的 `videoWidth`/`videoHeight` 为空或为 0 时,不要设置 `videoFrameWidth`/`videoFrameHeight`(均为可选字段),让系统使用源视频的原始尺寸:
|
|
594
|
+
|
|
595
|
+
```ets
|
|
596
|
+
const config: media.AVTranscoderConfig = {
|
|
597
|
+
fileFormat: media.ContainerFormatType.CFT_MPEG_4,
|
|
598
|
+
audioBitrate: audioBitrate,
|
|
599
|
+
audioCodec: media.CodecMimeType.AUDIO_AAC,
|
|
600
|
+
};
|
|
601
|
+
// 仅在获取到有效尺寸时设置视频参数
|
|
602
|
+
if (originalWidth > 0 && originalHeight > 0) {
|
|
603
|
+
config.videoFrameWidth = adjustedWidth;
|
|
604
|
+
config.videoFrameHeight = adjustedHeight;
|
|
605
|
+
config.videoBitrate = videoBitrate;
|
|
606
|
+
config.videoCodec = media.CodecMimeType.VIDEO_AVC;
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### 13.3 AVTranscoderConfig 高版本字段的兼容性
|
|
611
|
+
|
|
612
|
+
**陷阱**:`enableBFrame` 字段从 API 20 起新增。设备系统低于 API 20 时,运行时会报 `can not find enableBFrame property` 警告。这是**非致命警告**,不会导致转码失败,但如果与其他参数错误叠加,容易误导排查方向。
|
|
613
|
+
|
|
614
|
+
**正确做法**:不需要在 `AVTranscoderConfig` 中显式设置 `enableBFrame`。如果需要使用该特性,应先检查设备 API 版本。
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## 14. Want 跳转 — canOpenLink 不可靠与 URI Scheme 差异
|
|
619
|
+
|
|
620
|
+
### 14.1 canOpenLink() 不能用作"应用是否安装"的判断
|
|
621
|
+
|
|
622
|
+
**陷阱**:`bundleManager.canOpenLink()` 用于检查是否有应用能处理给定 URI,但它在以下场景会**错误返回 false**:
|
|
623
|
+
- 目标应用已安装且注册了对应 scheme,但 skills 匹配规则与链接格式不完全一致
|
|
624
|
+
- `querySchemes` 配置了对应 scheme,但系统版本差异导致匹配逻辑变化
|
|
625
|
+
- 目标应用注册了 scheme 但未声明 host/path,某些链接格式不匹配
|
|
626
|
+
|
|
627
|
+
将 `canOpenLink()` 作为"应用未安装"的硬拦截会导致**已安装的应用无法被拉起**。
|
|
628
|
+
|
|
629
|
+
**正确做法**:
|
|
630
|
+
|
|
631
|
+
```ets
|
|
632
|
+
// ❌ 禁止:canOpenLink 硬拦截
|
|
633
|
+
if (!bundleManager.canOpenLink(link)) {
|
|
634
|
+
result.error('APPLICATION_NOT_INSTALLED', 'App not installed', null);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
await this.context.startAbility(want);
|
|
638
|
+
|
|
639
|
+
// ✅ 正确:直接尝试 startAbility,通过 catch 区分错误类型
|
|
640
|
+
try {
|
|
641
|
+
await this.context.startAbility(want);
|
|
642
|
+
result.success(true);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
const msg = (err as Error).message ?? '';
|
|
645
|
+
if (msg.includes('17700056') || msg.includes('not in the querySchemes')) {
|
|
646
|
+
result.error('APPLICATION_NOT_INSTALLED', 'Target app not installed', null);
|
|
647
|
+
} else {
|
|
648
|
+
result.error('OPEN_FAILED', 'Failed to open app', null);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 14.2 Android URI Scheme 不能直接复用到 HarmonyOS
|
|
654
|
+
|
|
655
|
+
**陷阱**:同一应用在 Android 和 HarmonyOS 上注册的 URI Scheme 可能完全不同。Agent 机械平移 Android 的 URI Scheme 会导致目标应用无法被拉起。
|
|
656
|
+
|
|
657
|
+
典型案例:
|
|
658
|
+
- QQ on Android 注册 `mqqwpa://`(用于好友聊天),但 QQ on HarmonyOS **未注册** `mqqwpa`
|
|
659
|
+
- QQ on HarmonyOS 只注册 `mqqapi`、`qqopenapi`、`wtloginmqq` 等
|
|
660
|
+
|
|
661
|
+
**正确做法**:
|
|
662
|
+
1. 通过 `hdc shell bm dump -n <bundleName> | grep '"scheme"'` 查看目标应用实际注册的 scheme
|
|
663
|
+
2. 根据实际注册的 scheme 调整 URI 格式
|
|
664
|
+
3. 在 Want 中添加 `bundleName` 以精确匹配目标应用
|
|
665
|
+
4. 将 scheme 列入 `module.json5` 的 `querySchemes`
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## 15. 图片处理 — PixelMap 与 ImagePacker
|
|
670
|
+
|
|
671
|
+
### 15.1 readPixelsToBuffer 返回原始像素,不是编码图片
|
|
672
|
+
|
|
673
|
+
**陷阱**:`PixelMap.readPixelsToBuffer()` 读取的是原始 BGRA/RGBA 像素字节(裸位图数据),**不是** PNG/JPEG 编码数据。将其直接传回 Dart 侧,`Image.memory()` 无法解码(缺少文件头和压缩),报 invalid image data。
|
|
674
|
+
|
|
675
|
+
这是与 Android `Bitmap.compress(format, quality, stream)` 的关键差异——Android 的 compress 直接输出编码后的图片流,而 OHOS 的 `readPixelsToBuffer` 只输出裸像素。
|
|
676
|
+
|
|
677
|
+
**正确做法**:使用 `image.createImagePacker()` + `packing()` 将 PixelMap 编码为 PNG/JPEG 格式后返回:
|
|
678
|
+
|
|
679
|
+
```ets
|
|
680
|
+
import { image } from '@kit.ImageKit';
|
|
681
|
+
|
|
682
|
+
async pixelMapToEncodedBytes(pixelMap: image.PixelMap): Promise<ArrayBuffer> {
|
|
683
|
+
const packer = image.createImagePacker();
|
|
684
|
+
const packOpts: image.PackingOption = {
|
|
685
|
+
format: 'image/png',
|
|
686
|
+
quality: 100
|
|
687
|
+
};
|
|
688
|
+
const encodedBuffer = await packer.packing(pixelMap, packOpts);
|
|
689
|
+
packer.release();
|
|
690
|
+
return encodedBuffer;
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**禁止**:
|
|
695
|
+
- `pixelMap.readPixelsToBuffer()` 的结果直接返回给 Dart 侧作为图片数据
|
|
696
|
+
- 假设 `readPixelsToBuffer` 等同于 Android 的 `Bitmap.compress()`
|
|
697
|
+
|
|
698
|
+
### 15.2 createImageSource 需要 ArrayBuffer,不是 Array<number>
|
|
699
|
+
|
|
700
|
+
**陷阱**:`image.createImageSource(buffer)` 的参数类型是 `ArrayBuffer`,必须通过 `Uint8Array.buffer` 获取。如果 Dart 侧传来的二进制数据经过 `.toList()` 转换,ETS 侧收到的是 `Array<number>`(无 `.buffer` 属性),直接调用 `createImageSource` 会失败。
|
|
701
|
+
|
|
702
|
+
**正确做法**:
|
|
703
|
+
```ets
|
|
704
|
+
// 防御性处理:确保拿到 ArrayBuffer
|
|
705
|
+
let buffer: ArrayBuffer;
|
|
706
|
+
if (rawData instanceof Uint8Array) {
|
|
707
|
+
buffer = rawData.buffer;
|
|
708
|
+
} else if (Array.isArray(rawData)) {
|
|
709
|
+
const u8 = new Uint8Array(rawData as number[]);
|
|
710
|
+
buffer = u8.buffer;
|
|
711
|
+
} else {
|
|
712
|
+
buffer = rawData as ArrayBuffer;
|
|
713
|
+
}
|
|
714
|
+
const imageSource = image.createImageSource(buffer);
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**禁止**:假设 Channel 传来的二进制数据一定是 `Uint8Array`,不做类型检查。
|