diff --git a/AGENTS.md b/AGENTS.md index fb0aa55..0ebab2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`),不再依赖代码仓库数据库文件 diff --git a/app/build.gradle b/app/build.gradle index 97a34e8..155a160 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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,否则 diff --git a/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt b/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt index 91cf933..f32fd0f 100644 --- a/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt +++ b/app/src/main/java/com/local/ktv/DatabaseBootstrapper.kt @@ -67,7 +67,7 @@ object DatabaseBootstrapper { }.getOrNull() /** 下载曲库:仅官方 Muse 同步源;成功后记录同步时间(重置 30 天时效)。 */ - fun download(onProgress: (Int) -> Unit): Result { + fun download(onProgress: (Int, String?) -> Unit): Result { return runCatching { val file = MuseDbSync.download(onProgress) // MuseDbSync 内部已保存版本文件;这里记录同步时间作为时效起点。 diff --git a/app/src/main/java/com/local/ktv/MainActivity.kt b/app/src/main/java/com/local/ktv/MainActivity.kt index 7d8fe3f..b350e89 100644 --- a/app/src/main/java/com/local/ktv/MainActivity.kt +++ b/app/src/main/java/com/local/ktv/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/com/local/ktv/MuseDbSync.kt b/app/src/main/java/com/local/ktv/MuseDbSync.kt index 4c01912..5849dc2 100644 --- a/app/src/main/java/com/local/ktv/MuseDbSync.kt +++ b/app/src/main/java/com/local/ktv/MuseDbSync.kt @@ -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", + ) + } + } + } } } }