From f0d0ab9a057dafd8d164dedf44dd14a08951efb6 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 27 Oct 2026 23:06:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(card):=20=E8=A7=82=E5=BD=B1=204x4/6x4=20?= =?UTF-8?q?=E6=B5=B7=E6=8A=A5=E5=A2=99=E3=80=8C=E5=8F=AA=E6=9C=89=206=20?= =?UTF-8?q?=E5=BC=A0=E5=9B=BE=E3=80=8D=E4=BF=AE=E5=A4=8D=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户原话:「观影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」。 --- entry/src/main/ets/data/FbCardImages.ets | 72 +++++++++++- entry/src/main/ets/data/FbCardMediaData.ets | 123 ++++++++++++++------ 2 files changed, 157 insertions(+), 38 deletions(-) diff --git a/entry/src/main/ets/data/FbCardImages.ets b/entry/src/main/ets/data/FbCardImages.ets index dc20a95..60631df 100644 --- a/entry/src/main/ets/data/FbCardImages.ets +++ b/entry/src/main/ets/data/FbCardImages.ets @@ -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 { + const out: string[] = []; + for (let i = 0; i < keys.length; i++) { + out.push(''); + } + let next: number = 0; + const worker = async (): Promise => { + 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[] = []; + 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 === '') { diff --git a/entry/src/main/ets/data/FbCardMediaData.ets b/entry/src/main/ets/data/FbCardMediaData.ets index d36e235..b4cd3a6 100644 --- a/entry/src/main/ets/data/FbCardMediaData.ets +++ b/entry/src/main/ets/data/FbCardMediaData.ets @@ -18,7 +18,7 @@ import { hilog } from '@kit.PerformanceAnalysisKit'; import { FB_SERVER_STORE } from './FbServerStore'; import { FB_TOKEN_STORE } from './FbTokenStore'; import { fbApi, fbImageUrl, FbApiResult, FbApiOptions } from './FbHttp'; -import { fbCardImagePath, fbCardImageFd, fbCloseCardImageFds } from './FbCardImages'; +import { fbCardImagePaths, fbCardImageFd, fbCloseCardImageFds } from './FbCardImages'; import { fbCardBuild, FbCardData } from './FbCardData'; import { parseTasks } from './FbParse'; import { setTaskStatus } from './FbTaskRepo'; @@ -224,26 +224,42 @@ export async function fbMovieCardBuild(ctx: common.Context): Promise { + rows: FbMovieCardRow[]): Promise { const bag: FbImageBag = new FbImageBag(); - // 收候选:先最近(2×2/2×4 用),再列表(4×4/6×4 用) const keys: string[] = []; - for (const r of recent) { - if (r.poster !== '' && keys.indexOf(r.poster) < 0 && keys.length < CARD_IMG_MAX) { - keys.push(r.poster); - } - } - for (const r of list) { + for (const r of rows) { if (r.poster !== '' && keys.indexOf(r.poster) < 0 && keys.length < CARD_IMG_MAX) { keys.push(r.poster); } } + /* 🔴 必须走**并发窗口**(`fbCardImagePaths`),不能在这里 `for + await` 串行: + 20 张串行 = 20 次网络往返,实测会到 10 秒以上,卡片刷新窗口撑不住。 */ + const paths: string[] = await fbCardImagePaths(ctx, keys, + (pk: string): string => fbImageUrl(pk, token)); + const map: Map = new Map(); let seq: number = 0; - for (const pk of keys) { - const path: string = await fbCardImagePath(ctx, pk, fbImageUrl(pk, token)); - const fd: number = fbCardImageFd(path); + for (let i = 0; i < keys.length; i++) { + const fd: number = fbCardImageFd(paths[i]); if (fd >= 0) { const memKey: string = 'p' + seq.toString(); seq++; - map.set(pk, memKey); + map.set(keys[i], memKey); bag.formImages[memKey] = fd; bag.imgFds.push(fd); } } if (map.size > 0) { - for (const r of recent) { - const k: string | undefined = map.get(r.poster); - if (k !== undefined) { - r.img = k; - } - } - for (const r of list) { + for (const r of rows) { const k: string | undefined = map.get(r.poster); if (k !== undefined) { r.img = k; @@ -470,7 +521,7 @@ export async function fbTaskCardBuild(ctx: common.Context): Promise