拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MFC 播放器播放统计:从崩溃修复到图表重构

给一个开源的 MFC 音乐播放器做二次开发我想加个播放统计功能——看看自己每天听什么歌、哪个时段听得最多。第一版功能跑通了Release 编译 0 error我还挺得意。结果一开 Debug 版点开统计对话框直接崩在ucrtbased.dll菜单项上赫然显示一串 error。这一崩把我后面三天都搭进去了。起因想给播放器装个听歌账本我用的这个播放器是 MusicPlayer2一个用 MFC 写成的开源 Windows 音乐播放器本身没有播放统计。我这种人连自己上周循环了哪首歌都记不住就想加个本地统计把每次播放都记下来。思路其实不复杂在播放器几个关键节点埋点把每次播放写成一行 JSON 存到本地文件再做一个对话框把数据画出来看。存储我选了JSONL按月分文件statistics/playlog_YYYY-MM.jsonl。当时也考虑过 SQLite但放弃了一来这个项目本来就往song_data.dat写二进制我不想动它的存储格式二来 JSONL 追加写入、写完立刻 flush程序崩了也不丢数据崩溃安全性更好。这个决定后来帮了我——起码我的 bug 从没弄丢过已记录的数据。埋点我只动了四个地方尽量最小侵入歌曲开始播放、每秒 tick 计时、切歌、程序退出时 flush。每条记录带上标题、艺术家、专辑、流派、播放时长、播放结果完整收听 / 跳过 / 停止 / 出错、是否随机播放这些字段。第一版界面三个 Tab 和手绘图表统计界面我做成一个对话框三个 Tab总览、详细记录、图表。详细记录能按时间倒序看每一次播放还能导出 CSV带 BOMExcel 直接打开和 JSON。图表是用GDI 手绘的没引第三方库30 天播放趋势的折线 柱状组合、24 小时听歌时段的着色柱状图、艺术家播放时长占比的饼图。左键点图表区域循环切换三种图。第一版做完Release 编译干干净净我觉得这下稳了。然后我习惯性地开了 Debug 版点了一下——直接崩。Debug 崩溃死在 ucrtbased.dll 的越界访问崩溃点在CPlayStatisticsDlg::LoadRecords()里解析 JSONL 的那段。我写了个extract_bool的 lambda从was_shuffled:后面取字符判断真假out (line[pos] t); // pos 可能等于 line.size()越界访问问题就藏在这儿。当某一行was_shuffled:后面紧跟换行、或者这条记录字段被截断时pos会等于line.size()。而std::string::operator[]在Debug 模式下会做越界检查直接触发 assertion failure一路往上炸到_invalid_parameter最后死在ucrtbased.dll。Release 版不做这个检查所以看起来一切正常——这才是这件事最坑的地方Release 能跑不代表没 bug。修起来倒简单加一行边界判断auto extract_bool [](size_t pos, bool out) { if (pos line.size()) return; // 越界就跳过别去读那一位 out (line[pos] t); };但找到它花了我两小时。崩溃栈只告诉我死在ucrtbased.dll没有任何业务代码线索典型的报错信息和真正病因八竿子打不着。最后是把那一行 JSON 逐字段对了一遍才发现有的记录was_shuffled后面根本没值。顺手还收拾了两个小尾巴。一个是新增的菜单项文字显示 error根因是英文语言包English.ini的[menu.MAIN_TOOL]节里我没给ID_PLAY_STATISTICS写翻译函数找不到就返回了常量error。补一行翻译即可。另一个是ToJson()里snprintf(buf, sizeof(buf), \\u%04x, c)的c是char有符号而%x期望unsigned int类型不严谨我改成static_castunsigned char(c)收尾。图表扩展加上时间筛选却踩了第二个坑崩的问题解决后这个统计我越用越觉得不够看。我想看最近 30 天而不是全部时间的分布于是加了时间范围下拉框全部 / 7 天 / 30 天 / 90 天又补了 Top10 专辑、Top10 歌曲、流派分布环形图点击切换的视图从 3 个扩到 5 个。下拉框切换时调用GetFilteredRecords()过滤记录再Invalidate重绘逻辑看着没问题。可实测发现切到 30 天后趋势图和时段图完全没变还是全部时间的数据。排查下来DrawTrendChart和DrawHourChart这两个画图函数的开头直接遍历的是m_records压根没用我过滤出来的records// 修复前画的是全部记录时间筛选形同虚设 for (const auto r : m_records) { ... } // 修复后开头先拿过滤结果 auto records GetFilteredRecords(); for (const auto r : records) { ... }就这一行的事。但如果不看实现、只信我明明调了过滤函数的直觉根本想不到画图函数内部绕过了数据源。两个函数各补一行筛选终于生效。重构3 页视图扩到 5 页视图扩到 5 个后原来的m_chart_type % 3切换逻辑得改成% 5右上角的视图名提示数组也补到 5 个。这一版我把概览、排行榜、流派分布、30 天趋势、时段分布做成分页切换排行榜每个显示前 5 名布局压缩在一页里。编译 0 error功能全通。经验总结回过头看这三天踩的坑都是低级但隐蔽那一类的Debug 和 Release 行为不一致要警惕。越界访问在 Release 静默通过在 Debug 直接崩。这类 bug 不开 Debug 版根本发现不了所以功能写完我都会专门开 Debug 跑一遍。画图函数别绕过数据源。我本以为GetFilteredRecords()是全局生效的结果两个画图函数内部各自持有一份m_records。过滤类 bug 最容易被接口看起来对的骗过去改完一定得实测筛选结果。多语言资源漏一项就显示 error。MFC 的字符串表、菜单翻译每加一个新控件或菜单记得把所有语言包都补齐。这套统计模块我用了快两个月每天听歌习惯一目了然也没再崩过。如果你也在维护老 MFC 项目、想加类似的本地统计JSONL GDI 手绘这条路是走得通的就是调试时记得开 Debug 版。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门