V1.4.4: 修复曲库下载期间界面卡死/极慢(高频进度回调压死主线程)
现象: 极米 H1S 上曲库开始下载后速度极慢,并有概率卡死(ANR)。 根因: MuseDbSync.downloadFile 每读一个 256KB 分块就回调一次进度, 430MB 压缩包 = 约 1700 次回调;每次回调都 main.post -> showDatabaseLoading(), 而该函数每次刷新都在主线程做: setText + 改 ProgressBar + setOnClickListener(null) + bringToFront() (bringToFront 会让整棵视图树 requestLayout)。 1700 次全屏布局把主线程 Looper 队列压满 -> 界面停止响应(卡死); 同时整机 CPU 被布局任务吃掉,网络线程与 FUSE 写盘都被拖慢 -> 下载本身也变慢。 两个症状同一个根因,不是网络问题。 修复: - 进度回调两级去重: MuseDbSync 按百分比去重; MainActivity 按最终显示文案去重, 刷新次数从约 1700 降到不超过 100。 - showDatabaseLoading 更新路径只做真正变化的事: 文本不同才 setText、进度不同才改 progress、不再每次 bringToFront()、不再每次重设监听。 - 解压(约 1GB)与 md5 校验(430MB)阶段补上进度明细(例如 正在解压曲库 512MB), 避免界面长时间停在某个百分比被误判为卡死。 - 下载速率按每 10% 上报远程日志: MUSE-DB progress 30% 129MB/430MB 1.85MB/s, 可直接区分网络慢还是磁盘慢。 - 回调签名 (Int)->Unit 扩展为 (Int, String?)->Unit 以携带明细文案。 - AGENTS.md 增补 高频回调不能直接刷 UI 条目。 版本 1.4.3(23) 到 1.4.4(24);签名证书不变。
This commit is contained in:
@@ -160,7 +160,7 @@ $BT/apksigner verify 目标名.apk
|
||||
|
||||
### 版本号
|
||||
|
||||
- `app/build.gradle`:`versionName "1.4.3"` / `versionCode 23`(18~21 被本机临时诊断包占用过)
|
||||
- `app/build.gradle`:`versionName "1.4.4"` / `versionCode 24`(18~21 被本机临时诊断包占用过)
|
||||
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
||||
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
||||
@@ -226,6 +226,21 @@ $BT/apksigner verify 目标名.apk
|
||||
4. 曲库同步每一步失败都会 `MiliLog.warn("MUSE-DB ..." / "DB-SYNC ...")` 上报。
|
||||
老设备上「不能下载曲库」先看远程日志这几行 —— 以前只写 logcat,界面只剩
|
||||
「未找到可用曲库」兜底文案,排查极其困难。
|
||||
- **⚠️ 高频回调不能直接刷 UI(曲库下载 = 430MB)**:`MuseDbSync.downloadFile` 每读一个
|
||||
256KB 分块回调一次进度,430MB 就是 **约 1700 次**。若每次回调都 `main.post { 改文本 +
|
||||
改进度 + bringToFront() }`,主线程 Looper 队列会被压满 —— 实测表现为**界面卡死、进度停滞**,
|
||||
而且整机 CPU 被布局任务吃掉后**下载本身也变慢**(极米 H1S 上就是这个现象)。
|
||||
约定:
|
||||
1. 进度回调必须**按最终显示文案去重**(`MuseDbSync` 侧按百分比去重、
|
||||
`showDatabaseDownloadProgress` 侧按文本去重),刷新次数上限 ~100 次;
|
||||
2. `showDatabaseLoading()` 的更新路径**只做真正变化的事**:文本不同才 `setText`、
|
||||
进度不同才 `progress=`、**不要每次 `bringToFront()`**(整棵树 `requestLayout`)、
|
||||
不要每次重设 `setOnClickListener`;
|
||||
3. 解压(约 1GB)与 md5 校验(430MB)阶段**也要报进度**(回调带明细文案,
|
||||
如「正在解压曲库 512MB」),否则界面会长时间停在某个百分比、看着像卡死;
|
||||
4. 下载速率会按每 10% 上报一次 `MUSE-DB progress 30% 129MB/430MB 1.85MB/s`,
|
||||
判断"是网络慢还是磁盘慢"直接看这几行;
|
||||
5. 新增任何"周期性/高频"回调(进度、心跳、轮询)时都要按同样标准评估主线程开销。
|
||||
- **歌曲源**:`app/src/main/assets/mobile/ktv_api.js` 的 `CONFIG.HOSTS`(多源自动排序,失败源沉底)
|
||||
- **JS 热更新**:改动 `ktv_api.js` 必须同步提升 `KTV_BRIDGE_API: N` 标记(JS 与 `KtvJsBridge.kt` 两处校验),否则设备仍用缓存旧脚本
|
||||
- **曲库**:只走官方 Muse 同步(`MuseDbSync`),不再依赖代码仓库数据库文件
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.local.ktv"
|
||||
minSdk 21
|
||||
targetSdk 25
|
||||
versionCode 23
|
||||
versionName "1.4.3"
|
||||
versionCode 24
|
||||
versionName "1.4.4"
|
||||
ndk {
|
||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||
|
||||
@@ -67,7 +67,7 @@ object DatabaseBootstrapper {
|
||||
}.getOrNull()
|
||||
|
||||
/** 下载曲库:仅官方 Muse 同步源;成功后记录同步时间(重置 30 天时效)。 */
|
||||
fun download(onProgress: (Int) -> Unit): Result<File> {
|
||||
fun download(onProgress: (Int, String?) -> Unit): Result<File> {
|
||||
return runCatching {
|
||||
val file = MuseDbSync.download(onProgress)
|
||||
// MuseDbSync 内部已保存版本文件;这里记录同步时间作为时效起点。
|
||||
|
||||
@@ -615,6 +615,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private fun initializeDatabase() {
|
||||
if (databaseBootstrapRunning) return
|
||||
databaseBootstrapRunning = true
|
||||
lastDatabaseProgressText = ""
|
||||
showDatabaseLoading(true, "正在初始化曲库...", null)
|
||||
io.execute {
|
||||
var bootstrapError: Throwable? = null
|
||||
@@ -684,14 +685,28 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDatabaseDownloadProgress(value: Int) {
|
||||
/**
|
||||
* 最近一次真正刷到界面上的曲库进度文案。
|
||||
*
|
||||
* ⚠️ 曲库下载是 430MB,会回调约 1700 次;每次都在主线程做「改文本 + 改进度 + 置顶视图」
|
||||
* 会把 UI 线程压死(表现为界面卡死、进度停滞,整机 CPU 被布局任务吃掉后下载本身也变慢)。
|
||||
* 因此这里按**最终显示文案**去重:既能挡掉高频分块回调,又能让「正在解压曲库 xxxMB」
|
||||
* 这类明细正常刷新。
|
||||
*/
|
||||
@Volatile private var lastDatabaseProgressText = ""
|
||||
|
||||
private fun showDatabaseDownloadProgress(value: Int, detail: String?) {
|
||||
val percent = value.coerceIn(0, 100)
|
||||
val phase = when {
|
||||
value < 80 -> "正在下载并解压曲库"
|
||||
value < 90 -> "正在完成解压"
|
||||
value < 100 -> "正在校验数据库"
|
||||
else -> "完成"
|
||||
percent < 80 -> "正在下载曲库"
|
||||
percent < 90 -> "正在解压曲库"
|
||||
percent < 100 -> "正在校验曲库"
|
||||
else -> "曲库就绪"
|
||||
}
|
||||
main.post { showDatabaseLoading(true, "$phase ${value}%", value) }
|
||||
val text = detail ?: "$phase $percent%"
|
||||
if (text == lastDatabaseProgressText) return
|
||||
lastDatabaseProgressText = text
|
||||
main.post { showDatabaseLoading(true, text, percent) }
|
||||
}
|
||||
|
||||
private fun warmRemoteBrowseCache(count: Int) {
|
||||
@@ -8883,18 +8898,31 @@ class MainActivity : AppCompatActivity() {
|
||||
root.addView(overlay, FrameLayout.LayoutParams(-1, -1))
|
||||
databaseLoadingOverlay = overlay
|
||||
}
|
||||
databaseLoadingOverlay?.visibility = View.VISIBLE
|
||||
databaseLoadingText?.text = message
|
||||
databaseLoadingProgress?.apply {
|
||||
visibility = if (progressValue == null) View.INVISIBLE else View.VISIBLE
|
||||
progress = progressValue ?: 0
|
||||
// ⚠️ 这里是**每次进度刷新都会走**的路径(曲库下载 430MB)。必须做到「只在真正变化时
|
||||
// 才改 View、且不做重排」:早先的实现每次都 setText + 改 progress + setOnClickListener(null)
|
||||
// + bringToFront()(整棵视图树 requestLayout),把主线程压死 → 界面卡死、下载也变慢。
|
||||
val overlay = databaseLoadingOverlay ?: return
|
||||
if (overlay.visibility != View.VISIBLE) {
|
||||
overlay.visibility = View.VISIBLE
|
||||
overlay.bringToFront()
|
||||
}
|
||||
databaseRetryButton?.apply {
|
||||
visibility = View.GONE
|
||||
isEnabled = true
|
||||
setOnClickListener(null)
|
||||
val textView = databaseLoadingText
|
||||
if (textView != null && textView.text?.toString() != message) {
|
||||
textView.text = message
|
||||
}
|
||||
val bar = databaseLoadingProgress
|
||||
if (bar != null) {
|
||||
val wanted = if (progressValue == null) View.INVISIBLE else View.VISIBLE
|
||||
if (bar.visibility != wanted) bar.visibility = wanted
|
||||
val percent = progressValue ?: 0
|
||||
if (bar.progress != percent) bar.progress = percent
|
||||
}
|
||||
val retry = databaseRetryButton
|
||||
if (retry != null && retry.visibility == View.VISIBLE) {
|
||||
retry.visibility = View.GONE
|
||||
retry.isEnabled = true
|
||||
retry.setOnClickListener(null)
|
||||
}
|
||||
databaseLoadingOverlay?.bringToFront()
|
||||
}
|
||||
|
||||
private fun showDatabaseLoadingFailure(message: String) {
|
||||
|
||||
@@ -50,8 +50,12 @@ object MuseDbSync {
|
||||
file.version
|
||||
}.getOrNull()
|
||||
|
||||
/** 下载并安装曲库。失败抛异常,由调用方决定是否回退到其它源。 */
|
||||
fun download(onProgress: (Int) -> Unit): File {
|
||||
/**
|
||||
* 下载并安装曲库。
|
||||
* @param onProgress (百分比, 明细文案);明细非空时优先显示(如「正在解压曲库 512MB」),
|
||||
* 因为解压/校验阶段很慢,只给百分比会让界面长时间不动、看着像卡死。
|
||||
*/
|
||||
fun download(onProgress: (Int, String?) -> Unit): File {
|
||||
val cookie = login() ?: error("登录曲库服务失败")
|
||||
val file = sync(cookie) ?: error("曲库同步失败或无可用文件")
|
||||
val version = file.version
|
||||
@@ -59,23 +63,32 @@ object MuseDbSync {
|
||||
val gzFile = File(target.parentFile, "${target.name}.gz.download")
|
||||
val tmpDb = File(target.parentFile, "${target.name}.download")
|
||||
try {
|
||||
downloadFile(file.url, gzFile) { pct -> onProgress((pct * 80 / 100).coerceIn(0, 80)) }
|
||||
onProgress(80)
|
||||
downloadFile(file.url, gzFile) { pct -> onProgress((pct * 80 / 100).coerceIn(0, 80), null) }
|
||||
onProgress(80, "正在校验曲库文件")
|
||||
if (file.md5.isNotEmpty()) {
|
||||
check(md5(gzFile).equals(file.md5, ignoreCase = true)) { "曲库文件校验失败" }
|
||||
}
|
||||
onProgress(84)
|
||||
onProgress(84, "正在解压曲库 0MB")
|
||||
GZIPInputStream(gzFile.inputStream().buffered(256 * 1024)).use { gz ->
|
||||
BufferedOutputStream(FileOutputStream(tmpDb)).use { out ->
|
||||
val buffer = ByteArray(256 * 1024)
|
||||
var written = 0L
|
||||
var lastStep = -1
|
||||
while (true) {
|
||||
val count = gz.read(buffer)
|
||||
if (count < 0) break
|
||||
out.write(buffer, 0, count)
|
||||
written += count
|
||||
// 解压约 1GB、在该设备上要几分钟:每 64MB 报一次,界面才不会像是卡死。
|
||||
val step = (written / (64L * 1024 * 1024)).toInt()
|
||||
if (step != lastStep) {
|
||||
lastStep = step
|
||||
onProgress(84, "正在解压曲库 ${written / 1048576}MB")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onProgress(96)
|
||||
onProgress(96, "正在安装曲库")
|
||||
val backup = File(target.parentFile, "${target.name}.backup")
|
||||
backup.delete()
|
||||
if (target.exists()) check(target.renameTo(backup)) { "无法备份旧曲库" }
|
||||
@@ -85,7 +98,7 @@ object MuseDbSync {
|
||||
}
|
||||
backup.delete()
|
||||
saveVersion(version)
|
||||
onProgress(100)
|
||||
onProgress(100, "曲库就绪")
|
||||
return target
|
||||
} finally {
|
||||
gzFile.delete()
|
||||
@@ -204,6 +217,8 @@ object MuseDbSync {
|
||||
val code = conn.responseCode
|
||||
check(code in 200..299) { "HTTP $code" }
|
||||
val total = conn.contentLengthCompat()
|
||||
val startedAt = System.currentTimeMillis()
|
||||
var lastPercent = -1
|
||||
conn.inputStream.buffered(256 * 1024).use { input ->
|
||||
dest.outputStream().buffered(256 * 1024).use { out ->
|
||||
val buffer = ByteArray(256 * 1024)
|
||||
@@ -213,7 +228,26 @@ object MuseDbSync {
|
||||
if (count < 0) break
|
||||
out.write(buffer, 0, count)
|
||||
read += count
|
||||
if (total > 0) onProgress(((read * 100) / total).toInt().coerceIn(0, 99))
|
||||
if (total > 0) {
|
||||
val percent = ((read * 100) / total).toInt().coerceIn(0, 99)
|
||||
// ⚠️ 必须按百分比去重:430MB 会读约 1700 个 256KB 分块,
|
||||
// 每块都回调一次会把主线程 Looper 队列压满(界面卡死、整机 CPU 被
|
||||
// 布局任务吃掉,下载本身也跟着变慢)。
|
||||
if (percent != lastPercent) {
|
||||
lastPercent = percent
|
||||
onProgress(percent)
|
||||
if (percent % 10 == 0) {
|
||||
val seconds = ((System.currentTimeMillis() - startedAt) / 1000.0)
|
||||
.coerceAtLeast(0.001)
|
||||
val mbps = read / 1048576.0 / seconds
|
||||
MiliLog.info(
|
||||
"MUSE-DB progress $percent% " +
|
||||
"${read / 1048576}MB/${total / 1048576}MB " +
|
||||
"${String.format(Locale.ROOT, "%.2f", mbps)}MB/s",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user