From 35a636ef4247a504d4ea331925582908b55695f1 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 5 Nov 2026 13:00:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(ncm):=20=E7=A6=BB=E7=BA=BF=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=94=B9=E5=8D=95=E8=B0=83=E9=92=9F=E8=8A=82=E5=A5=8F?= =?UTF-8?q?=E9=97=B8=EF=BC=8C=E6=96=AD=E7=BD=91=E4=B8=8D=E5=86=8D=E6=AF=8F?= =?UTF-8?q?=E4=B8=AA=20tick=20=E9=87=8D=E8=AF=95=EF=BC=88M10=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(真机日志 19000100_000000_7534.log):meow_ui_time_ms() 在设备上返回坏值—— 低 32 位是「开机秒数×1000」(无毫秒),高 32 位每次调用随机(REPORT §42)。 旧守卫 `!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)` 因此恒真:worker 一收工,下一个 tick 立刻再重试;真机 74s 内 10 次 (间隔 5-7s 只是失败请求本身的连接超时)。第二条链是 else 分支清零重试时刻, online/offline 一抖动就再试。 修复: - 新增 app/logic/retry_ctl.{h,c}:节奏闸只做差值,时钟由调用方给可信单调钟 (home.c 改传 core 的 ncm_monotonic_ms());回退不下溢(重定基准、不重试); busy 单飞;在线/离线抖动不重置基准;首次进离线立即探测一次,之后严格 ≥30s。 - home_tick 不再使用入参 now(真机坏值无效化);新增 home_retry_reset()。 - 静默重试不再走完整 home_reload:新增 home_reload_ex(silent) 不预读盘 (旧实现每轮在 UI 线程同步读 playlists.bin)、home_apply_ex(keep_scroll) 保留滚动位置、worker 记 g_home_changed 决定是否需要重绘——旧实现每轮 home_apply() 复位滚动+整帧重绘,是「歌单列表滑动特别卡」的直接来源。 - worker 线程 pthread_detach(旧代码每轮泄漏线程栈)。 --- patch_v2/ncm_app/app/logic/retry_ctl.c | 30 +++++++ patch_v2/ncm_app/app/logic/retry_ctl.h | 42 ++++++++++ patch_v2/ncm_app/app/ncm_app_internal.h | 1 + patch_v2/ncm_app/app/ui/home.c | 103 +++++++++++++++++------- 4 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 patch_v2/ncm_app/app/logic/retry_ctl.c create mode 100644 patch_v2/ncm_app/app/logic/retry_ctl.h diff --git a/patch_v2/ncm_app/app/logic/retry_ctl.c b/patch_v2/ncm_app/app/logic/retry_ctl.c new file mode 100644 index 0000000..a0e4117 --- /dev/null +++ b/patch_v2/ncm_app/app/logic/retry_ctl.c @@ -0,0 +1,30 @@ +/* logic/retry_ctl.c — 见 retry_ctl.h 头注释(M10 离线重试节奏闸)。 */ +#include "retry_ctl.h" + +void ncm_retry_ctl_init(ncm_retry_ctl *c) +{ + if (!c) return; + c->last_ms = 0; + c->have_last = 0; +} + +int ncm_retry_ctl_poll(ncm_retry_ctl *c, int offline, int busy, int64_t now_ms, + int64_t interval_ms) +{ + if (!c || interval_ms <= 0) return 0; + if (!offline) return 0; /* 在线:不动基准 —— 抖动不得加速重试 */ + if (busy) return 0; /* 单飞:上一个 worker 还在跑 */ + + if (!c->have_last) { /* 首次进离线:立即探测一次 */ + c->have_last = 1; + c->last_ms = now_ms; + return 1; + } + if (now_ms < c->last_ms) { /* 时钟回退/异常:绝不让差值下溢成「巨大已超时」 */ + c->last_ms = now_ms; /* 重定基准 → 仍需重新等满 interval */ + return 0; + } + if (now_ms - c->last_ms < interval_ms) return 0; + c->last_ms = now_ms; + return 1; +} diff --git a/patch_v2/ncm_app/app/logic/retry_ctl.h b/patch_v2/ncm_app/app/logic/retry_ctl.h new file mode 100644 index 0000000..2041b0c --- /dev/null +++ b/patch_v2/ncm_app/app/logic/retry_ctl.h @@ -0,0 +1,42 @@ +/* logic/retry_ctl.h — 离线自动重试节奏闸(M10:断网重试风暴的修复点)。 + * + * 背景(真机 19000100_000000_7534.log + launcher meow_20261105_122541.log): + * M9 的 30s 守卫写成 `if (!thr_on && (!last || now - last >= 30000))`,节奏完全依赖 + * meow_ui_time_ms() 的 64 位差值。真机实测该值「低 32 位 = 整秒×1000(无毫秒)、高 32 位是垃圾」 + * (见 REPORT §42),于是 now-last 几乎恒为巨大正数 → 每个 UI tick 都判「该重试」, + * 断网时把「30s 一次」退化成「每个 tick 一次」的重试风暴。 + * + * 本模块把节奏判定收敛成三条硬约束(纯逻辑、零依赖,宿主编译可直接驱动): + * 1) 时钟由调用方供给**可信单调钟**(home.c 传 core 的 ncm_monotonic_ms(),不再用 SDK 的 + * meow_ui_time_ms());本模块只做差值,不取时间。 + * 2) 时钟回退(now < last)绝不下溢成「巨大已超时」:按「刚试过」处理并重定基准, + * 下一次尝试仍需重新等满 interval。 + * 3) 单飞:busy(上一个 worker 未收工)期间任何时钟跳变都不发新尝试。 + * 在线/离线抖动不重置基准 —— 抖动无法加速重试(旧实现 else 分支清零 g_offline_retry_ms, + * 状态一抖就立刻再试,是风暴的第二条触发链)。 + */ +#ifndef NCM_RETRY_CTL_H +#define NCM_RETRY_CTL_H + +#include + +typedef struct { + int64_t last_ms; /* 上次发起尝试的单调毫秒时刻(have_last=0 时无意义) */ + int have_last; /* 1=至少尝试过一次 */ +} ncm_retry_ctl; + +void ncm_retry_ctl_init(ncm_retry_ctl *c); + +/* 每个 UI tick 调一次。返回 1=现在应当发起一次重试(内部已记账:调用方随即起 worker)。 + * offline=当前是否离线;busy=是否已有 worker 在跑(单飞);now_ms=单调毫秒; + * interval_ms=两次尝试的最小间隔(M10 定 30000)。 + * + * 语义: + * - offline=0:返回 0,且**不动**基准(在线期间与状态抖动都不重置节奏)。 + * - busy=1:返回 0。 + * - 首次观察到离线:返回 1(进离线先立即探测一次,恢复后由会话门切回在线)。 + * - 之后:仅当 now-last >= interval 才返回 1;now #include #include @@ -36,6 +42,13 @@ static int g_home_thr_on; static volatile int g_home_gen; static int g_home_acked; static volatile int g_home_loading; +static int g_home_silent; /* 1=本次加载是后台静默刷新(M10 离线重试) */ +static int g_home_changed; /* 1=本次加载改了可见数据/离线态(决定是否重绘) */ + +/* M10:离线自动重试节奏闸。时钟用 core 单调钟(ncm_monotonic_ms),不用 SDK meow_ui_time_ms + * ——真机后者高 32 位是垃圾,任何 `now - last >= N` 都会变成「恒真」(REPORT §42)。 */ +#define HOME_OFFLINE_RETRY_MS 30000 +static ncm_retry_ctl g_retry; static const char *tab_label(int tab) { return tab ? "我收藏的" : "我创建的"; } @@ -234,12 +247,17 @@ static void home_draw_footer(void *ctx) ncm_draw_footer(r_cont, foot); } -/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。 */ -static void home_apply(void) +/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。 + * keep_scroll=1:后台静默刷新用——保留用户当前滚动位置(数量变化由 SDK 钳位); + * 0=复位到顶部(进页/切 tab/手动刷新,与原行为一致)。 + * M10:离线重试不再每轮把列表拽回顶部(旧代码每轮调本函数复位滚动 + 整帧重绘, + * 重试风暴时表现为「歌单列表滑动特别卡」)。 */ +static void home_apply_ex(int keep_scroll) { if (g_list) { + int keep = keep_scroll ? meow_listview_scroll(g_list) : 0; meow_listview_set_count(g_list, tab_count(g_tab)); - meow_listview_set_scroll(g_list, 0); + meow_listview_set_scroll(g_list, keep); meow_listview_set_sel(g_list, -1); } if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab); @@ -248,6 +266,8 @@ static void home_apply(void) meow_ui_paint_full(); } +static void home_apply(void) { home_apply_ex(0); } + /* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 * §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存; * 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */ @@ -256,6 +276,8 @@ static void *home_worker(void *arg) (void)arg; int rc; int from_cache = 0; + int off_before = appstate_is_offline(); + int cnt_before = g_st.playlist_count; if (!g_st.logged_in) rc = NCM_ERR_AUTH; else { /* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。 @@ -273,44 +295,65 @@ static void *home_worker(void *arg) pthread_mutex_lock(&g_home_mu); g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */ g_home_loading = 0; + /* M10:本次加载是否产生「看得见的变化」——离线态翻转 / 歌单数量变化 / 仍处空态 + * (空态文案随 load_err、离线提示变化)。无变化则静默重试不重绘,不打断滑动。 */ + g_home_changed = (appstate_is_offline() != off_before) || + (g_st.playlist_count != cnt_before) || + (g_st.playlist_count == 0); g_home_gen++; pthread_mutex_unlock(&g_home_mu); return NULL; } -void home_reload(void) +/* silent=0:进页/手动刷新(读盘缓存秒开 + 复位滚动);1:后台静默刷新(不动界面)。 */ +static void home_reload_ex(int silent) { - if (g_host_mock) { g_load_err = 0; home_apply(); return; } - if (g_home_thr_on) return; - /* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */ - int64_t ts = 0; - int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0); - g_load_err = 0; - if (!had_cache) { - g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */ - g_home_loading = 1; + if (g_host_mock) { if (!silent) { g_load_err = 0; home_apply(); } return; } + if (g_home_thr_on) return; /* 单飞:同一时刻只一个 worker */ + if (!silent) { + /* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */ + int64_t ts = 0; + int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0); + g_load_err = 0; + if (!had_cache) { + g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */ + g_home_loading = 1; + } } + /* M10 静默刷新:界面已有数据 → 不预读盘(旧实现每轮都在 UI 线程同步读 playlists.bin)、 + * 不复位滚动、不重绘;等 worker 完成后由 home_tick 按 g_home_changed 决定是否重绘。 */ + g_home_silent = silent; g_home_thr_on = 1; if (ncm_thread_create(&g_home_thr, home_worker, NULL) != 0) { g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO; + return; } - home_apply(); + pthread_detach(g_home_thr); /* 只靠 gen 代次收工、不 join:线程栈即刻回收(旧代码每轮泄漏) */ + if (!silent) home_apply(); } +void home_reload(void) { home_reload_ex(0); } + +/* M10:清离线重试节奏(页创建/登出/宿主测试用)。 */ +void home_retry_reset(void) { ncm_retry_ctl_init(&g_retry); } + void home_tick(uint64_t now) { - /* M9 离线恢复:离线时每 30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate - * 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。 */ - static uint64_t g_offline_retry_ms; - if (appstate_is_offline() && !g_host_mock) { - if (!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)) { - g_offline_retry_ms = now; - meow_log(MEOW_LOG_INFO, "home: offline retry"); - ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */ - home_reload(); - } - } else { - g_offline_retry_ms = 0; + /* M10 离线恢复:断网下严格 ≥30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate + * 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。 + * + * 节奏判定不使用入参 now:真机 meow_ui_time_ms() 的高 32 位是垃圾(REPORT §42), + * `now - last >= 30000` 会恒真 → 旧实现每个 tick 都重试(真机 74s 内 10 次,CPU 48%、 + * load 3.65、整机卡死)。这里统一用 core 单调钟 + retry_ctl 闸: + * - 单飞(worker 未收工不重试)、 + * - 时钟回退不下溢(回退时重定基准,不立即重试)、 + * - 在线/离线抖动不重置基准(旧 else 分支清零 g_offline_retry_ms,一抖就重试)。 */ + (void)now; + if (appstate_is_offline() && !g_host_mock && + ncm_retry_ctl_poll(&g_retry, 1, g_home_thr_on, ncm_monotonic_ms(), HOME_OFFLINE_RETRY_MS)) { + meow_log(MEOW_LOG_INFO, "home: offline retry"); + ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */ + home_reload_ex(1); } if (!g_home_thr_on) return; @@ -318,7 +361,12 @@ void home_tick(uint64_t now) g_home_acked = g_home_gen; g_home_thr_on = 0; g_home_loading = 0; - home_apply(); + int changed, silent; + pthread_mutex_lock(&g_home_mu); + changed = g_home_changed; + silent = g_home_silent; + pthread_mutex_unlock(&g_home_mu); + if (!silent || changed) home_apply_ex(silent ? 1 : 0); /* 静默刷新保留滚动位置 */ } int home_is_active(void) { return g_hc.active; } @@ -403,6 +451,7 @@ static void home_page_create(meow_view *v, void *ctx) { (void)v; ((home_ctrl *)ctx)->view = v; + home_retry_reset(); /* M10:离线重试节奏闸(首次进离线立即探测一次) */ r_cont = ncm_content_rect(); ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72); r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48);