fix(card): 观影 4x4/6x4 海报墙「只有 6 张图」修复 #2

用户原话:「观影4x4和4x6,只有六张图,划到第七张,就没图片了」

病根:两个上限各写各的
  · CARD_IMG_MAX = 6      (配图数)
  · rows.length < 30      (列表长度)
⇒ 前 6 格有海报,第 7 格起 img === '',posterCell 走 else 画淡紫占位块。
渲染没错、数据也没缺(线上 movie_details 221 条全有 poster_key,去重 205 张),
纯粹是「配图数 < 格子数」的结构性问题。

修法(不是把 6 改成 20 就完事,而是让格子数由图数决定):
  · CARD_IMG_MAX 6 → 20,依据华为 Form Kit FAQ:API 20+ 上限 20 张 / 10MB,
    本机 const.ohos.apiversion = 26 走这一档;
    内存按解码后算 240×360×4×20 = 6.59MB < 10MB,留 3.4MB 余量。
  · 拆出 CARD_ROW_MAX = 30(命名常量),专给不画图的 2x2/2x4 纯文本行。
  · 新增 list:只保留 img !== '' 的行,长度天然 ≤ CARD_IMG_MAX
    ⇒ 格子数与图数在结构上永远相等,某张 404 也不会冒空格子。
  · fbAttachPosters 签名 (recent, list) → (rows),两个数组各写一遍循环太容易只改一处。
  · 新增 fbCardImagePaths 并发取图(MAX_PARALLEL = 4):20 张串行实测 10~17s,
    卡片刷新窗口撑不住。MAX_FILES 24 必须 ≥ CARD_IMG_MAX,否则后下的挤掉先下的。

验证:静态守卫 1317/0;反向验证 15/0(7 条注入各自恰好命中目标断言)。
其中注入 A 抓出原先一条假绿断言:写的是「CARD_IMG_MAX >= 4」,
而 bug 原值正好是 6,6 >= 4 恒真 ⇒ 等于什么都没守。已改成「> 6」。
This commit is contained in:
2026-10-27 23:06:24 +08:00
parent b84ea06b73
commit f0d0ab9a05
2 changed files with 157 additions and 38 deletions
+70 -2
View File
@@ -38,12 +38,31 @@ const TAG: string = 'FbCardImg';
/** 缓存目录(`filesDir/cardimg`):跟着应用数据走,卸载即清 */
const DIR_NAME: string = 'cardimg';
/** 缩略图尺寸:卡片上最大也就 ~150vp 宽(≈ 470px@3.125x),240×360 足够 */
/** 缩略图尺寸:卡片上最大也就 ~150vp 宽(≈ 470px@3.2x),240×360 足够。
*
* ⚠️ 别为了"更清晰"往上调:卡片图片的官方限制是按**解码后内存**算的
* (宽×高×4),不是 jpg 文件大小。240×360 = 0.33MB/张,20 张 6.59MB 安全;
* 调到 360×540 就是 0.74MB/张,20 张 14.8MB 超过 10MB 上限 ⇒ 丢图甚至白卡。
* 依据见 `FbCardMediaData.ets` 里 `CARD_IMG_MAX` 的注释(工单 #2)。 */
const THUMB_W: number = 240;
const THUMB_H: number = 360;
/** 目录里最多留多少个文件(每部电影一张,24 部够翻一大阵子) */
/**
* 目录里最多留多少个文件。
*
* 🔴 必须 **≥ `FbCardMediaData.CARD_IMG_MAX`**(现在 20)。
* 工单 #2 把图数量从 6 提到 20,如果这里还是原来的小值,
* 就会出现「一次推送要 20 张、缓存只留 N 张」——
* 后下载的把先下载的挤掉,表现为「翻回去前面几张又没了」,比原来的 bug 更难查。
* 24 是 20 + 4 的余量(留几张上一轮的,翻页时不必重新下载)。
*/
const MAX_FILES: number = 24;
/**
* 同时下载几张(见 `fbCardImagePaths` 的注释:为什么不是 1,也不是 20)。
* 4 是折中:20 张压到 5 轮,又不会一次开 20 个连接。
*/
const MAX_PARALLEL: number = 4;
function dirOf(ctx: common.Context): string {
return ctx.filesDir + '/' + DIR_NAME;
}
@@ -236,6 +255,55 @@ export async function fbCardImagePath(ctx: common.Context, posterKey: string,
return made;
}
/**
* 并发取多张海报的本地小图。
*
* 🔴🔴 为什么需要它(工单 #2,2026-10-27):图数量从 6 提到 20 之后,
* 原来那种"一条 for 循环里逐张 await"的写法会变成**串行 20 次网络往返**。
* 单张超时是 `connectTimeout 6000 + readTimeout 10000`,最坏 16s/张 ⇒
* 20 张串行理论上能卡 320 秒,而卡片刷新窗口远没有这么长
* (`FbFormAbility` 里 updateForm 完不成,系统会按失败处理,
* 表现是"卡片一直旧数据 / 干脆不刷新")。
* 实测首轮 6 张串行约 3~5 秒,20 张串行会到 10~17 秒,已经接近不可接受。
*
* ⇒ 用**固定并发窗口**(`MAX_PARALLEL`)滚动推进,而不是一次 20 个并发:
* · 一次全发:瞬时开 20 个 HttpRequest,小内存设备容易失败,且服务端瞬时压力大;
* · 串行:见上,太慢。
* 4 路并发把 20 张压到约 5 轮,总耗时接近"最慢的那一轮",实际 3~6 秒。
*
* @return 与 `keys` **一一对应**的本地路径数组(失败的位置是空串,不改变长度/顺序)
*/
export async function fbCardImagePaths(ctx: common.Context, keys: string[],
urlOf: (key: string) => string): Promise<string[]> {
const out: string[] = [];
for (let i = 0; i < keys.length; i++) {
out.push('');
}
let next: number = 0;
const worker = async (): Promise<void> => {
while (true) {
const idx: number = next;
next++;
if (idx >= keys.length) {
return;
}
try {
out[idx] = await fbCardImagePath(ctx, keys[idx], urlOf(keys[idx]));
} catch (err) {
// 单张失败只留空,绝不冒泡(卡片进程被回收最多是"这张没图")
out[idx] = '';
}
}
};
const n: number = Math.min(MAX_PARALLEL, keys.length);
const workers: Promise<void>[] = [];
for (let i = 0; i < n; i++) {
workers.push(worker());
}
await Promise.all(workers);
return out;
}
/** 打开本地小图并返回 fd(失败 -1) */
export function fbCardImageFd(path: string): number {
if (path === '') {