fix(ncm §31): 修两处 UI 数据竞态——返回首页行空白(staging发布) + 进第二个歌单显示上一歌单(换歌单清窗)

A. 返回首页各行 2~3s 空白:appstate_load_playlists 就地 memset(g_st.playlists[])
   与 home_src2 无锁逐行绑定竞态,刷新期间 playlist_count 保留旧值 → UI 读清零行。
   照搬 s_build[] staging 范式:网络落 s_pls[] → 成功后锁内原子发布(拷回+更新count),
   失败保留旧快照(断网离线浏览)。绝不就地清零 UI 可见数组。

B. 进第二个歌单显示上一歌单:playlist_open/refresh 同步路径不调 appstate_playlist_reset,
   pl_apply(1) 绑到上一歌单残留窗口(track_ids_count/songs)。
   新增窗口归属(歌单 id 唯一标识, 对搜索歌单同样成立): worker 首屏发布后
   appstate_playlist_window_set;refresh 目标≠归属(窗口is) → 同步 reset 再 pl_apply。
   同歌单手动刷新(id 同)保留旧内容(stale-while-revalidate)。

宿主回归:playlist_race_sim(新 F 并发读无清零态/G 开A→开B 不串)、window/search/
cache_evict/player_flow/stream_sim、test_ncm_core 宿主+mipsel 全 PASS;
build_ncm.sh -Wall -Wextra 0 warning;0 真实请求;未部署。
This commit is contained in:
AskaEth committed 2026-09-02 03:03:48 +08:00
1 parent 97b741cf56
commit 10690bc348
9 files changed
+290 -9

No files matched your search

+52 -7
View File
@@ -34,6 +34,29 @@ int appstate_playlist_begin(int idx)
return g;
}
/* 当前窗口归属歌单 id(§NEW bug B:新进歌单显示上一个歌单内容)。窗口数据(song_ids/songs/
* track_ids_count)属于哪个歌单由 id 唯一标识(对搜索歌单同样成立——不同搜索结果 = 不同 id)。
* worker 首屏发布窗口后调 appstate_playlist_window_set 标记;playlist_refresh 在起 worker 前用
* appstate_playlist_window_is 判「目标歌单是否=当前窗口归属」,不一致 → 同步 appstate_playlist_reset()
* 清掉上一歌单残留,防 pl_apply(1) 把新歌单标题绑到上一歌单数据(「开 B 显示 A」)。 */
static pthread_mutex_t g_winpl_mu = PTHREAD_MUTEX_INITIALIZER;
static char g_win_plid[32];
void appstate_playlist_window_set(const char *plid)
{
pthread_mutex_lock(&g_winpl_mu);
if (plid && plid[0]) ncm_strlcpy(g_win_plid, plid, sizeof g_win_plid);
else g_win_plid[0] = 0;
pthread_mutex_unlock(&g_winpl_mu);
}
int appstate_playlist_window_is(const char *plid)
{
pthread_mutex_lock(&g_winpl_mu);
int same = (plid && plid[0] && g_win_plid[0] && strcmp(g_win_plid, plid) == 0);
pthread_mutex_unlock(&g_winpl_mu);
return same;
}
/* 清窗口/索引(UI 丢弃过期歌单结果时用;播放快照不受影响——播放经 play_song 快照)。 */
void appstate_playlist_reset(void)
{
@@ -44,6 +67,9 @@ void appstate_playlist_reset(void)
g_st.song_count = 0;
g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
pthread_mutex_lock(&g_winpl_mu); /* 窗口已清 → 归属歌单一并清空(下一 worker 重新标记) */
g_win_plid[0] = 0;
pthread_mutex_unlock(&g_winpl_mu);
}
static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */
@@ -174,19 +200,38 @@ int appstate_ensure_uid(void)
return rc;
}
/* 歌单列表 staging 发布锁(§NEW bug A):appstate_load_playlists 的「网络成功后锁内原子发布
* (拷回 g_st.playlists[] + 更新 playlist_count)」与 UI 线程 home_src2/home_draw 无锁逐行绑定
* 之间的发布点。持久化(playlists.bin)同单写者(home worker)在发布后读,无相互竞态。 */
static pthread_mutex_t g_pls_mu = PTHREAD_MUTEX_INITIALIZER;
int appstate_load_playlists(void)
{
if (!g_st.logged_in) return NCM_ERR_AUTH;
int rc = appstate_ensure_uid();
if (rc != NCM_OK) return rc;
memset(g_st.playlists, 0, sizeof g_st.playlists);
/* §NEW bug A(返回首页各行 2~3 秒空白):旧实现在**UI 可见数组**就地 memset→网络回填,与
* home_src2 无锁逐行绑定竞态——刷新期间 playlist_count 保留旧值(cache),UI 读到被清零的行
* (无标题/曲目数 0),仅下标 0 因先读幸存。照搬本文件 appstate_load_playlist_songs_impl 的
* s_build[] staging 范式:网络结果落 staging(s_pls[]),成功后锁内**一次性拷回** g_st.playlists[]
* + 更新 playlist_count(原子发布——UI 全程读到旧或新完整快照,绝不读到清零态);失败保留旧
* 快照(不改 playlists/playlist_count,断网可离线浏览旧列表)。静态数组:单写者(home worker
* 单飞),免 96KB 栈。 */
static ncm_playlist_t s_pls[NCM_PLAYLIST_MAX];
int n = 0;
rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n);
g_st.playlist_count = (rc == NCM_OK) ? n : 0;
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid);
if (rc == NCM_OK) appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */
return rc;
int rc2 = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
NCM_PLAYLIST_MAX, 0, s_pls, NCM_PLAYLIST_MAX, &n);
if (rc2 == NCM_OK) {
int cnt = (n > NCM_PLAYLIST_MAX) ? NCM_PLAYLIST_MAX : n;
pthread_mutex_lock(&g_pls_mu);
if (cnt > 0) memcpy(g_st.playlists, s_pls, (size_t)cnt * sizeof(ncm_playlist_t));
g_st.playlist_count = cnt; /* count 在拷回后更新:读侧绝不见「新 count + 未拷回行」 */
pthread_mutex_unlock(&g_pls_mu);
appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */
}
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc2,
(rc2 == NCM_OK) ? n : g_st.playlist_count, g_st.uid);
return rc2;
}
/* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
+6
View File
@@ -358,6 +358,12 @@ int appstate_playlist_open_gen(void); /* 当前打开代次(0=从未
int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */
int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */
void appstate_playlist_reset(void); /* 清窗口/索引(UI 丢弃过期结果、在播安全)。加锁。 */
/* 窗口归属(§NEW bug B:新进歌单显示上一个歌单内容)——
* worker 首屏发布窗口后 appstate_playlist_window_set 标记
* 归属歌单 id;playlist_refresh 在换歌单前用 window_is 判
* 目标≠归属 → 同步 reset 清窗,防「开 B 显示 A」。 */
void appstate_playlist_window_set(const char *plid); /* 标记当前窗口归属歌单 id */
int appstate_playlist_window_is(const char *plid); /* 1=窗口归属该歌单(换歌单清窗判据) */
int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
int ctx_gen, int ctx_idx, int offset, int limit);
/* 快照式加载(歌单详情/曲目物化):
+12
View File
@@ -272,6 +272,10 @@ static void *playlist_worker(void *arg)
}
g_plloaded_idx = tidx;
g_plloaded_gen = tgen;
/* §NEW bug B:首屏发布后标记窗口归属=目标歌单(失败/部分也归属,支持同歌单重试不清窗)。
* 过期(STALE)不标记——窗口仍属旧歌单。playlist_refresh 据此换歌单清窗。 */
if (rc != NCM_ERR_STALE)
appstate_playlist_window_set(tpl.id);
} else {
g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
g_plloaded_idx = cur_active;
@@ -324,6 +328,14 @@ void playlist_refresh(void)
g_tgt.idx = g_st.active_playlist;
g_tgt.gen = appstate_playlist_begin(g_tgt.idx);
pthread_mutex_unlock(&g_plmu);
/* §NEW bug B(新进歌单显示上一个歌单内容):目标歌单与当前窗口归属不一致(跨歌单/搜索不同
* 结果)→ 同步清窗口,再 pl_apply。否则 pl_apply(1) 把新歌单标题绑到上一歌单残留窗口
* (track_ids_count/songs),新页瞬间渲染上一歌单行。同歌单手动刷新(id 相同)不清窗,
* 保留旧内容(stale-while-revalidate)——worker 异步重载替换。 */
if (!appstate_playlist_window_is(g_tgt.pl.id))
appstate_playlist_reset();
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
g_pl_stale = 0;
g_plloading = 1;