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