fix(ncm): 离线重试改单调钟节奏闸,断网不再每个 tick 重试(M10)

根因(真机日志 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(旧代码每轮泄漏线程栈)。
This commit is contained in:
AskaEth committed 2026-11-05 13:00:24 +08:00
1 parent aa225a1973
commit 35a636ef42
4 files changed
+140 -18

No files matched your search

+30
View File
@@ -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;
}
+42
View File
@@ -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 <stdint.h>
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<last(时钟回退)→ 重置基准、返回 0。 */
int ncm_retry_ctl_poll(ncm_retry_ctl *c, int offline, int busy, int64_t now_ms,
int64_t interval_ms);
#endif /* NCM_RETRY_CTL_H */
+1
View File
@@ -345,6 +345,7 @@ void home_touch_down(int x, int y);
void home_touch_move(int x, int y); void home_touch_move(int x, int y);
void home_reload(void); /* 刷新我的歌单(后台 worker,非阻塞) */ void home_reload(void); /* 刷新我的歌单(后台 worker,非阻塞) */
void home_tick(uint64_t now); /* UI tick:检查歌单 worker 是否完成并刷新 */ void home_tick(uint64_t now); /* UI tick:检查歌单 worker 是否完成并刷新 */
void home_retry_reset(void); /* M10:清离线自动重试节奏(页创建/登出/宿主测试) */
int home_go_up(void); /* 左缘返回:到底则退出 */ int home_go_up(void); /* 左缘返回:到底则退出 */
/* playlist */ /* playlist */
+67 -18
View File
@@ -12,8 +12,14 @@
* *
* 左缘返回 = 退出 App(app.c)。歌单加载后台 worker(home_worker,保留 REPORT §10.11 * 左缘返回 = 退出 App(app.c)。歌单加载后台 worker(home_worker,保留 REPORT §10.11
* 的异步结构,不回退成同步——P1 启动卡顿已修,勿回退)。 * 的异步结构,不回退成同步——P1 启动卡顿已修,勿回退)。
*
* M10:离线自动重试不再用「裸 u64 差值 + 状态抖动清零」的 30s 守卫(真机 meow_ui_time_ms()
* 高 32 位是垃圾 → 守卫恒真 → 断网重试风暴 / META 洪泛 / 列表被反复拽回顶部)。改由
* logic/retry_ctl.c 统一管节奏(core 单调钟、单飞、时钟回退安全、抖动不重置),
* 静默重试不预读盘/不复位滚动/无可见变化不重绘。详见 REPORT §42。
*/ */
#include "../ncm_app_internal.h" #include "../ncm_app_internal.h"
#include "../logic/retry_ctl.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@@ -36,6 +42,13 @@ static int g_home_thr_on;
static volatile int g_home_gen; static volatile int g_home_gen;
static int g_home_acked; static int g_home_acked;
static volatile int g_home_loading; 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 ? "我收藏的" : "我创建的"; } 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); ncm_draw_footer(r_cont, foot);
} }
/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。 */ /* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。
static void home_apply(void) * keep_scroll=1:后台静默刷新用——保留用户当前滚动位置(数量变化由 SDK 钳位);
* 0=复位到顶部(进页/切 tab/手动刷新,与原行为一致)。
* M10:离线重试不再每轮把列表拽回顶部(旧代码每轮调本函数复位滚动 + 整帧重绘,
* 重试风暴时表现为「歌单列表滑动特别卡」)。 */
static void home_apply_ex(int keep_scroll)
{ {
if (g_list) { 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_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); meow_listview_set_sel(g_list, -1);
} }
if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab); if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab);
@@ -248,6 +266,8 @@ static void home_apply(void)
meow_ui_paint_full(); meow_ui_paint_full();
} }
static void home_apply(void) { home_apply_ex(0); }
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 /* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。
* §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存; * §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存;
* 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */ * 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */
@@ -256,6 +276,8 @@ static void *home_worker(void *arg)
(void)arg; (void)arg;
int rc; int rc;
int from_cache = 0; 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; if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else { else {
/* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。 /* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。
@@ -273,15 +295,22 @@ static void *home_worker(void *arg)
pthread_mutex_lock(&g_home_mu); pthread_mutex_lock(&g_home_mu);
g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */ g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */
g_home_loading = 0; 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++; g_home_gen++;
pthread_mutex_unlock(&g_home_mu); pthread_mutex_unlock(&g_home_mu);
return NULL; 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_host_mock) { if (!silent) { g_load_err = 0; home_apply(); } return; }
if (g_home_thr_on) return; if (g_home_thr_on) return; /* 单飞:同一时刻只一个 worker */
if (!silent) {
/* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */ /* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */
int64_t ts = 0; int64_t ts = 0;
int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0); int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0);
@@ -290,27 +319,41 @@ void home_reload(void)
g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */ g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */
g_home_loading = 1; g_home_loading = 1;
} }
}
/* M10 静默刷新:界面已有数据 → 不预读盘(旧实现每轮都在 UI 线程同步读 playlists.bin)、
* 不复位滚动、不重绘;等 worker 完成后由 home_tick 按 g_home_changed 决定是否重绘。 */
g_home_silent = silent;
g_home_thr_on = 1; g_home_thr_on = 1;
if (ncm_thread_create(&g_home_thr, home_worker, NULL) != 0) { 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; 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) void home_tick(uint64_t now)
{ {
/* M9 离线恢复:离线时每 30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate /* M10 离线恢复:断网下严格 ≥30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate
* 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。 */ * 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。
static uint64_t g_offline_retry_ms; *
if (appstate_is_offline() && !g_host_mock) { * 节奏判定不使用入参 now:真机 meow_ui_time_ms() 的高 32 位是垃圾(REPORT §42),
if (!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)) { * `now - last >= 30000` 会恒真 → 旧实现每个 tick 都重试(真机 74s 内 10 次,CPU 48%、
g_offline_retry_ms = now; * 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"); meow_log(MEOW_LOG_INFO, "home: offline retry");
ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */ ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */
home_reload(); home_reload_ex(1);
}
} else {
g_offline_retry_ms = 0;
} }
if (!g_home_thr_on) return; if (!g_home_thr_on) return;
@@ -318,7 +361,12 @@ void home_tick(uint64_t now)
g_home_acked = g_home_gen; g_home_acked = g_home_gen;
g_home_thr_on = 0; g_home_thr_on = 0;
g_home_loading = 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; } 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; (void)v;
((home_ctrl *)ctx)->view = v; ((home_ctrl *)ctx)->view = v;
home_retry_reset(); /* M10:离线重试节奏闸(首次进离线立即探测一次) */
r_cont = ncm_content_rect(); r_cont = ncm_content_rect();
ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72); 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); r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48);