优化
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
---
|
||||
|
||||
# 一份「先搬砖再装修」的优化清单
|
||||
|
||||
先说结论:这个项目的**业务复杂度堆得比工程基建快很多**。嗅探→解析→原生播放这条链路写得很实在(`PlaylistCache` 解决一次性 m3u8、`seriesPayloads` 规避 `TransactionTooLargeException`、`PlayerSsl` 兜底老电视证书,都是有真实现场经验的方案,注释也讲清了 why)。真正欠账的是**安全边界、功能细节 Bug、构建可复现性、可测试性、架构解耦与颜色资源化**。
|
||||
|
||||
---
|
||||
|
||||
## P0 · 安全与功能级 Bug(建议优先做,改动小、风险降幅最大)
|
||||
|
||||
**1. 手机遥控 HTTP 服务完全没有鉴权,且常驻监听 `0.0.0.0:8766`**
|
||||
- `RemoteControlServer.kt:69-97` 的 `serve()` 不校验任何 token/cookie/来源;`RemoteControlServer.kt:56-67` 还返回 `Access-Control-Allow-Origin: *`。同一个 WiFi 下任何设备(甚至同一浏览器里打开的任意网页)都能 `POST /cmd`。
|
||||
- 通过 `/cmd` 能做的:`open_url` 让电视加载任意地址(`BrowserActivity.kt:899-907`)、`text` 往当前输入框注入文字、`key` 模拟按键。
|
||||
- 更严重的是 `/upload`(`RemoteControlServer.kt:162-219`)→ 落盘 → `.apk` 触发安装器(`RemoteControlHelper.kt:171-199`,配合 Manifest 的 `REQUEST_INSTALL_PACKAGES`)。即"局域网内任意人可投递 APK 到电视,只差用户点一下确认"。
|
||||
- 建议方向:去掉 `Access-Control-Allow-Origin: *`(阻断恶意网页通过 JS 跨站请求 `127.0.0.1:8766` 的本地 CSRF),并在服务端渲染 `remote.html` 时内嵌随机 Session Token 供同源请求校验;上传目录加扩展名白名单,`.apk` 弹窗明确展示来源并走二次确认。
|
||||
|
||||
**2. `PlayerSsl.enableLenient()` 是进程级、永久、不可回滚的全局降级**
|
||||
- `PlayerSsl.kt:60-79` 改了 `HttpsURLConnection.setDefaultSSLSocketFactory` + `setDefaultHostnameVerifier`,一旦播放中遇到一次 SSL 错误,**整个进程**后续所有 `HttpsURLConnection`(`VodSearchHelper` 抓站、其他网络请求)都不再校验证书。
|
||||
- 建议:不要动全局默认值。给 ExoPlayer 单独配 `OkHttpDataSource.Factory` 或自定义 `DataSource` 的 SSLSocketFactory,作用域收敛到播放器;`VodSearchHelper` 用独立的 client。
|
||||
|
||||
**3. Manifest 备份与安装权限偏松**
|
||||
- `allowBackup="true"`:收藏、浏览历史、播放记录会被云备份/`adb backup` 导出。
|
||||
- `REQUEST_INSTALL_PACKAGES` 与上面的上传链路组合即攻击面,见第 1 条。
|
||||
|
||||
**4. 写死年份的"系统时钟异常"判定会漂移**
|
||||
- `BrowserActivity.kt:334-338` 用 `Calendar.YEAR < 2025` 放行证书错误,`BrowserUiHelper.kt:373` 又是 `< 2024`。两个阈值未来会慢慢变成"全年无条件放行/永远不提示"。建议改成"设备时间与 HTTPS 响应 Date 头偏差超过 N 天"这类不随时间腐烂的判据。
|
||||
|
||||
**5. 快传文件目录多套一层 `uploads`,导致过期清理失效 & 无外置存储时崩溃**
|
||||
- **路径嵌套错位**:`RemoteControlServer.kt:180` 写了 `File(context.getExternalFilesDir("uploads") ?: context.filesDir, "uploads")`。当 `getExternalFilesDir("uploads")` 非空时,其路径本身已是 `.../files/uploads`,再拼一层 `"uploads"` 会将文件实际存入 `.../files/uploads/uploads/`。
|
||||
- **过期清理完全失效**:`BrowserUiHelper.cleanExpiredUploadFilesAsync`(`BrowserUiHelper.kt:204-219`)仅遍历 `getExternalFilesDir("uploads")` 一级目录下的 `f.isFile`,而子目录 `uploads` 的 `isFile` 为 `false`,导致启动时的「3 天过期上传文件清理」永远不会删掉任何文件。
|
||||
- **内部存储回退崩溃**:当部分电视盒子无外置存储回退到 `context.filesDir/uploads` 时,`res/xml/file_paths.xml` 仅声明了 `<external-files-path>` 和 `<cache-path>`,缺少 `<files-path>`,此时 `RemoteControlHelper.promptInstallApk`(`RemoteControlHelper.kt:173-177`)调用 `FileProvider.getUriForFile` 会直接抛出 `IllegalArgumentException`。
|
||||
|
||||
**6. 快传视频使用 `Uri.fromFile` 导致调起外部播放器必崩(`FileUriExposedException`)**
|
||||
- `RemoteControlHelper.handleFileUploaded`(`RemoteControlHelper.kt:162`)通过 `Uri.fromFile(file).toString()` 生成 `file://` 地址传给 `PlayerActivity`。
|
||||
- 内置 ExoPlayer 可直接播放 `file://`,但若用户在播放器内点击「外部播放」(`PlayerActivity.launchExternalPlayer`,`:1319-1340`),在 Android 7.0+(本项目 `targetSdk = 34`)下向外部应用发送 `file://` URI 会直接抛出 `FileUriExposedException`(被 catch 后误提示为"未找到已安装的外部视频播放器")。应统一通过 `FileProvider` 生成 `content://` URI 并授予 `FLAG_GRANT_READ_URI_PERMISSION`。
|
||||
|
||||
**7. 进入 `PlayerActivity` 全屏看片时,手机遥控器的方向键/暂停/返回失效**
|
||||
- `RemoteControlHelper` 目前强绑定在 `BrowserActivity` 上,其回调 `BrowserActivity.onRemoteKey`(`BrowserActivity.kt:880-897`)只向 `BrowserActivity` 分发按键。
|
||||
- 当电视跳转到全屏 `PlayerActivity` 播放视频时,用户在手机遥控器网页上按下的方向键(快进/快退/呼出选集)、返回键、菜单键全部发往处于后台的 `BrowserActivity`,无法控制正在前台播放的 `PlayerActivity`。建议将遥控事件路由给当前处于前台 `RESUMED` 状态的 Activity。
|
||||
|
||||
---
|
||||
|
||||
## P1 · 构建、依赖与死代码清理(能直接减小体积、让换机器能编译)
|
||||
|
||||
| 问题 | 证据 | 影响 |
|
||||
|---|---|---|
|
||||
| 3 个依赖零引用 | `leanback 1.2.0-alpha04`、`material`、`constraintlayout` 在 Kotlin 与 layout 中均 0 处引用 | alpha 版 leanback 还把它的人声资源 `lb_voice_*.ogg`、`exo_ic_*` 打进 APK |
|
||||
| `nanohttpd-websocket` 零引用 | 全仓库无 `NanoWSD` / `WebSocket`,`remote.html` 用的是 `XMLHttpRequest`(3 处) | 声明了双向通道依赖但从未实现 |
|
||||
| **整文件死代码与废弃布局未清理** | `VideoWebActivity.kt`(及 `AndroidManifest.xml:54-58` 与 `activity_video_web.xml`)、`BookmarksDialog.kt`、`BookmarkAdapter.kt`(及 `dialog_bookmarks.xml`、`item_bookmark.xml`)、`video_control_bar.xml` 全工程 0 处引用 | 首页已改用网格卡片 `HomeCardAdapter`,旧版弹窗与 Web 播放页纯属历史遗留负重 |
|
||||
| **死方法未清理** | `RemoteControlDialog.getLocalIpAddress()`(`:192-212`,实际调的是 `NetworkChecker`)、`VideoControlHelper` 中的 `JS_HAS_VIDEO`/`JS_GET_STATE`/`JS_GET_VIDEO_SRC`/`JS_GET_CONTENT_URL`、`PlaySource.listToJson`/`listFromJson`(`VideoSource.kt:42-56`) | 增加阅读与维护干扰 |
|
||||
| 测试依赖声明了却没用 | `libs.versions.toml:34-36` 有 junit/junit-ext/espresso,`app/build.gradle.kts` 里从未引用,也不存在 `src/test`、`src/androidTest` 目录 | 见 P1「零测试」 |
|
||||
| **缺少 Gradle Wrapper 脚本** | 只有 `gradle/wrapper/gradle-wrapper.properties`,`gradlew`、`gradlew.bat`、`gradle-wrapper.jar` 都不存在 | 换机器/CI 只能用 Android Studio 内建 Gradle,命令行无法构建 |
|
||||
| 无 release 签名配置 | `app/build.gradle.kts:18-27` 的 `release {}` 没有 `signingConfig` | `assembleRelease` 产出未签名包 |
|
||||
| 版本全线落后 | AGP 8.2.2 / Gradle 8.2 / Kotlin 1.9.22 / media3 1.3.1 / compileSdk 34 | `kotlinOptions` 已废弃;`targetSdk` 偏低在长期分发上会被卡 |
|
||||
| `gradle.properties` 偏保守 | 只有 `-Xmx2048m`,无 `parallel` / `caching` / `configuration-cache` | 构建慢 |
|
||||
| 1.4MB 的 `classes.jar` 提交进了仓库根目录 | `git ls-files` 可查 | 与项目无关的构建产物污染仓库 |
|
||||
| 无 `.gitattributes` | 实测 62 个 CRLF 文件 / 19 个 LF 文件混用 | Windows 与其他平台协作会产生无意义 diff |
|
||||
| 无 CI、无 `.github` | — | 没有最低限度的「编译 + lint」护栏 |
|
||||
|
||||
体积实测(`app-debug.apk`):压缩后 10.4MB,解压 21.35MB,含 **5 个 dex(9.2MB + 8.3MB 两个大 dex)**、`resources.arsc` 1.6MB。移除上面无用依赖与死代码 + 开启 R8 full mode 能实打实变小。
|
||||
|
||||
---
|
||||
|
||||
## P1 · 架构与可维护性(决定后续还能不能改)
|
||||
|
||||
**1. 两个巨型文件**
|
||||
- `PlayerActivity.kt` **1717 行 / 68 个函数**:`dispatchTouchEvent` 单函数 114 行(1417-1537)、`onCreate` 75 行、`dispatchKeyEvent` 84 行、`handlePlayerError`/`resolveAndPlay`/`tryFailover` 交织。合理切法:`PlaybackSession`(源/线路/失败换线)、`EpisodeNavigator`(选集/预解析/连播)、`PlayerOsdController`(OSD/进度/网速/反馈)、`PlayerGestureHandler`(触摸手势)。
|
||||
- `dialog_settings.xml` **880 行 / 110 个 view**,且 5 个面板全在一个文件里。建议按面板拆成 5 个 `View`/`include` 子布局。
|
||||
|
||||
**2. 83KB 内联 JavaScript 塞在 Kotlin 三引号字符串里**
|
||||
`VideoSnifferHelper.kt` 28KB、`PurifyHelper.kt` 27KB、`ContentFilterHelper.kt` 12KB、`VideoControlHelper.kt` 3.8KB、`UserAgentHelper.kt` 2.9KB、`AdBlockHelper.kt` 2.8KB、`RemoteControlHelper.kt` 1.6KB。后果:没有语法高亮、不能单独 lint、改一次要重新编译 APK、无法在浏览器里独立调试、diff 全是噪音。建议迁到 `assets/js/*.js`,首次需要时读取 + 缓存,甚至可以做一次 `JS 端单测`(Node 跑一遍语法与关键规则)。
|
||||
|
||||
**3. 三份几乎相同的 JSON Repository(且 `UserScriptRepository` 漏了内存缓存)**
|
||||
`BookmarkRepository.kt` / `HistoryRepository.kt` / `PlaybackHistoryRepository.kt` 是同一套「静态缓存 + `synchronized(Companion)` + `JSONArray` 全量重写 + `apply()`」的复制粘贴,约 200 行重复;而 `UserScriptRepository.kt` 甚至漏掉了静态内存缓存,每次 `onPageFinished` 都在主线程重读 SharedPreferences 并解析 JSON。抽 `JsonListStore<T>(prefsName, key, maxEntries, encode, decode)` 即可统一全部四个 Repository 的缓存、并发与裁剪策略。
|
||||
|
||||
**4. 设置项散落在 9 个 SharedPreferences 文件里**
|
||||
`tv_browser_settings` 被 `BrowserActivity.kt:84` 和 `AppSettings.kt:12` **两处重复定义常量各自访问**;此外还有 `_adblock` / `_bookmarks` / `_content_filter` / `_history` / `_playback_history` / `_search_engine` / `_user_scripts` / `_vod_search`。建议收敛为一个 `SettingsRepository`(键集中 + 迁移版本号),`ContentFilterHelper`/`AdBlockHelper` 里那些 `@Volatile cachedXxx` 的手写缓存也可以统一。
|
||||
|
||||
**5. Activity 强耦合与进程重建状态丢失**
|
||||
`SettingsDialog.kt` 里 12 处 `(activity as? BrowserActivity)?` 强耦合具体 Activity;**全项目 0 个 `onSaveInstanceState`**——进程被杀后,`PlayerActivity` 的选集/线路数据本来只存在内存 token 里(`PlayerActivity.kt:1619-1638`,为规避 Binder 限制的巧妙做法),此时无法直接恢复内存列表。可考虑落一个轻量 JSON 缓存文件存剧集页 + 集号 + 进度。
|
||||
|
||||
**6. 硬编码颜色与废弃资源条目**
|
||||
|
||||
| 指标 | 数量 |
|
||||
|---|---|
|
||||
| layout 里十六进制颜色 | **109 处**,19 种颜色,`#FFFFFF` 出现 30 次、`#38BDF8` 22 次、`#94A3B8` 20 次 |
|
||||
| `colors.xml` 里真正被用到的 | 只有 `black` / `white` / `toolbar_bg`,`primary` / `accent` / `focus_highlight` / `url_bar_bg` 基本没人用 |
|
||||
| `strings.xml` 废弃条目 | 78 条中有 **12 条从未被引用** |
|
||||
|
||||
建议清理未引用的 `strings.xml` / `colors.xml` 条目,并将高频的 `#FFFFFF`、`#38BDF8`、`#94A3B8` 等主题色抽入 `colors.xml` 统一管理。
|
||||
|
||||
**7. 默认影视站名单硬编码在代码里**
|
||||
`BookmarkRepository.kt:111-141` 有 14 个默认站点 + 4 个版本的新增迁移表(`DEFAULTS_ADDED_BY_VERSION` 这个设计其实很好)。问题是站点会死,改一次要发版。建议把这套表挪到远端 JSON(失败时回落到内置表),迁移版本号机制可以原样保留。
|
||||
|
||||
**8. 小事但会误导用户**
|
||||
`SettingsDialog.kt:386` 把 `"${BuildConfig.VERSION_NAME} (Release)"` 写死,debug 包也显示 "(Release)"。
|
||||
|
||||
---
|
||||
|
||||
## P1 · 零测试(这是最值得投资的"优化")
|
||||
|
||||
项目里 **`src/test` 与 `src/androidTest` 目录都不存在**,但代码中恰恰有一大批**纯函数、无 Android 依赖、非常好测**的点:
|
||||
|
||||
- `VideoSnifferHelper.isVideoStreamUrl` / `isHls` / `isMediaSegmentUrl` / `isLikelyVariantOf` / `buildPlaybackHeaders` / `isWebOnlyStream`(`:27-190`)
|
||||
- `ContentFilterHelper.registrableDomain` / `shouldBlockImage` / `shouldBlockScript`
|
||||
- `AdBlockHelper.shouldBlockUrl` / `isHijackScheme`(这里用 `lower.contains(pattern)` 的朴素子串匹配,误伤风险需要用例兜住)
|
||||
- `PlaylistCache` 的 `isMaster` / `isVod` / `take` 语义(一次性链接取完即弃,逻辑微妙)
|
||||
- `SiteInfo.normalizeUrl`、`AppSettings.seriesKey`、`PlaybackRecord.displaySubtitle` / `getProgressPercent`
|
||||
|
||||
这些写 30~50 个 JUnit 用例就能覆盖嗅探与过滤的核心判定,未来改规则时才敢动。这一步做完,`libs.versions.toml` 里躺着的 junit 依赖也就名正言顺了。
|
||||
|
||||
---
|
||||
|
||||
## P2 · 性能与资源
|
||||
|
||||
- **`onProgressChanged` 每个进度回调都注入一次脚本**:`BrowserActivity.kt:343-347` 只要 `hasDocStartHook == false`,每次进度变化都 `evaluateJavascript(JS_DOCUMENT_START_HOOK)`。这个 hook 是 20KB+ 的字符串(`VideoSnifferHelper.kt:213-435`),值得只在 `onPageStarted` 里做、并用一个"已注入"标志位替代 `hasDocStartHook` 的反复尝试。
|
||||
- **`updateProgress()` 高频无差别干活太重**:`PlayerActivity.kt:1132-1183` 每 500ms 做 `TrafficStats.getUidRxBytes` + 新建 `Calendar` 实例 + 多次 `String.format`,并且每 5 秒走一次 `saveCurrentProgress()` → 整个播放历史 **全表 JSON 序列化 + `prefs.apply()`**(`PlaybackHistoryRepository.kt:180-199`)。建议改为仅在控制栏可见(`isOsdVisible()`)时才计算网速与格式化 UI 文本(并复用 `Calendar`),并将断点落盘改为 15~30 秒一次(配合 `onPause/onStop`/切集时必存),主线程压力会明显下降。
|
||||
- **30 处 `Log.d` / `Log.v` 在 release 也打**,并且 `BrowserActivity.kt:355-358` 把**网页所有 console 消息**全量转进 Logcat。建议统一到一个 `Logx` 门面,由 `BuildConfig.DEBUG` 关闭。
|
||||
- **崩溃日志无上限**:`TVBrowserApplication.kt:35-64` 每次崩溃写一个文件,从不清理,长期会占满 `filesDir`(而 `crash_logs` 里可能含 URL 等隐私)。加"保留最近 20 个 + 启动时清理"即可。
|
||||
- **每 3 秒的常驻轮询**:`BrowserActivity.kt:138-144` 的 `remoteStatusChecker` 无条件改 `homeStatusBar.visibility = GONE`,可以在首页显示或退到后台时停掉这个轮询。
|
||||
- **`notifyDataSetChanged` 9 处**(`HomeCardAdapter.kt:40,47`、`PlayerActivity.kt:915,937`、`VodSearchDialogHelper.kt:102` 等),选集动辄上百项,建议改 `DiffUtil` 或至少 `notifyItemRangeChanged`。
|
||||
|
||||
---
|
||||
|
||||
## P2 · 健壮性/代码卫生
|
||||
|
||||
- **19 处空 `catch` 静默吞异常**(`BrowserUiHelper.kt` 6 处、`VideoWebActivity.kt` 3 处、`RemoteControlDialog.kt` 2 处…),排查线上问题时这些会变成「什么都查不到」。
|
||||
- **52 处 `catch (Exception/Throwable)`**、**23 处 `!!`**——大部分在 WebView/媒体回调里,可以换成更具体的异常 + 明确兜底。
|
||||
- **`BrowserActivity.kt:318-330` 的新旧 `onReceivedError` 导致重复报错 Toast**:新版回调调用了 `super.onReceivedError(view, request, error)`,而系统默认实现会在主框架出错时转调旧版回调,导致主框架报错弹两次 Toast;且旧版回调未加 `API < 23` 守卫,低版本系统下子资源加载失败也会误弹 Toast。
|
||||
- **零协程**:全项目 0 个 `lifecycleScope`/`suspend`,异步全靠 **17 处裸 `Thread { }` + 17 处 `postDelayed`**(`BrowserUiHelper.kt:365-385` 甚至在线程里调 `activity.getString`,Activity 已销毁时会崩)。引入 `lifecycleScope` 会同时解决可取消性与线程归属。
|
||||
- **日志 TAG 分散**:`Log.d("TVBrowser.AdBlock", ...)`(`BrowserActivity.kt:239`)这类裸字符串与其他 `private const val TAG` 混用。
|
||||
- 仓库习惯:**5 次提交全部同一天、提交信息是 `init` 和 4 个 `4k`**——即使单人也值得写清 commit message,否则半年后无法回溯「这个兜底为什么加」。
|
||||
|
||||
---
|
||||
|
||||
## 建议的落地顺序
|
||||
|
||||
1. **第一批(1~2 天,风险低收益高)**:删 4 个无用依赖 + 清理整文件死代码与废弃布局 + 补全 Gradle Wrapper + 提交 `classes.jar` 移出仓库 + 补 `.gitattributes` + 收紧 Manifest(`allowBackup`)+ 日志门控。产出:包变小、换机器能编译、代码库清爽。
|
||||
2. **第二批(安全与核心 Bug 修复)**:修复快传 `uploads` 路径嵌套与 `file_paths.xml` + 修复快传视频 `FileProvider` 调外部播放器崩溃 + 支持 `PlayerActivity` 前台接收手机遥控按键 + 遥控去除通配 CORS 与同源 Token 校验 + `PlayerSsl` 作用域收敛 + 年份判定改造。
|
||||
3. **第三批(可测性,收益长期)**:建 `src/test`,先把 `VideoSnifferHelper`/`ContentFilterHelper`/`PlaylistCache`/`SiteInfo` 的纯函数用 JUnit 锁住——**这一步做完再动嗅探规则才安全**。
|
||||
4. **第四批(可维护性)**:83KB 内联 JS 迁 `assets/js`;抽 `JsonListStore` 统一四个 Repository(补齐 `UserScriptRepository` 缓存);设置项收敛到一个 `SettingsRepository`。
|
||||
5. **第五批(重构,按需)**:`PlayerActivity` 按会话/选集/OSD/手势四块拆分;`dialog_settings.xml` 按面板拆分;高频颜色资源化。
|
||||
Reference in New Issue
Block a user