Skip to content

flutter_patcher 自托管 Android 热更新(前端 libapp.so)

本文描述 songloft-player 用 flutter_patcher 实现的前端自托管 Android 热更新:冷启动整包替换 libapp.so,补丁作为资产随每次 GitHub Release 自动发布,客户端启动检查、手动确认下载、冷重启生效;无法热更的版本引导去设置页下载 APK。

中英双语并存,改一版需同步 docs/en/flutter_patcher_hotupdate.md。 Bundle 版把本前端补丁与后端 libgojni.so 补丁合并为一次更新体验(同一对话框、一次冷重启),详见 backend_hotupdate.md;本文聚焦前端机制与标准版发布

范围与边界

维度结论
平台仅 Android。iOS/桌面/Web 上插件与本项目封装全部安全 no-op
能热更lib/ 下任意 Dart、纯 Dart 依赖、随包注册的 Flutter assets
不能热更原生 Kotlin/Java/C++、AndroidManifestres/、原生插件增改、Flutter 引擎升级 —— 必须发新 APK
生效时机补丁下载后下次冷启动生效,不在当前进程内替换;插件自带崩溃回滚 + 坏补丁黑名单
渠道dev 更 dev、stable 更 stable,不跨渠道(由编译期 AppConfig.frontendVersion 决定)
校验目前仅 MD5(FlutterPatcher.init(strictSignature: false));后续可加 Ed25519

核心模型:无基线 + 自动发布 + 兼容键

  • 无基线:客户端查本渠道最新 Release——dev→滚动 tag dev;stable→GitHub /releases/latest(dev 是 prerelease,latest 天然返回最新正式版)。由 lib/core/updater/channel_release_resolver.dart 解析,任意非最新 → 最新。
  • 自动发布:build-and-release.ymlbuild-android job 每次发版自动 dart run flutter_patcher:pack 产出 patch-<abi>.zip + manifest-<abi>.json 随 release 上传(arm64-v8a / armeabi-v7a / x86_64)。已无手动 patch-release.yml
  • 比较规则:dev 比 git commit hash;stable 比版本号(semver,lib/core/updater/version_compare.dart)。已应用同补丁(flutter_patcher.currentVersion == patchLabel)跳过。
  • 兼容键取代 versionCode 基线:libapp.so 天然按宿主 APK 的 versionCode 绑定引擎。本项目 pubspec 的 +N 恒定(CI 不随构建 bump),故所有 dev/stable 构建共用同一 versionCode,自然可跨版本热更——versionCode 是自动兼容代理,不是手挑的基线。客户端额外比对 AppConfig.flutterBinding(= CI FLUTTER_VERSION)与 manifest 的 flutterBinding:不同 → 不热更(防同 versionCode 但换了 Flutter 引擎导致崩溃),交「整包不兼容」分支引导下 APK。仅当有意 bump versionCode(通常伴随引擎/原生变更)时才走整包。

客户端流程(启动检查 + 手动)

入口:首页 initState 每会话调一次 PatchUpdateDialog.maybeShow(lib/core/updater/)——前端补丁(本文)与后端补丁(Bundle 版)合并为一个对话框

  1. 有可热更补丁(且未被「忽略此版本」)→ 弹可关闭对话框:列出待更新组件 + GitHub 代理选择(复用 GithubProxySelectionMixin)+ 按钮 [忽略此版本] [稍后] [下载并更新];下载显示进度 → 完成弹「重启生效」([立即重启] = EmbeddedBackendService.restartProcess() 真进程冷启,让 libapp.so 冷启生效)。
  2. 无补丁但同渠道有更高整包版本(FrontendVersionApi,且未被忽略)→ 弹「需要下载新版本」→ [忽略此版本] [稍后] [前往下载];前往下载 = 跳 /settings(那里有「检查客户端更新」下 APK)。
  3. 都没有 → 静默。
  • 代理:抓 manifest 与下载 patch 都套用户所选代理(PatchUpdateService.applyProxy,前缀拼接);选择持久化到 githubProxyProvider,与插件商店/整包升级共用。
  • 忽略:分别记忆到 AppPreferences.ignoredPatchVersion / ignoredClientVersion

托管与 URL 约定

补丁作为资产随对应 Release 上传;仓库由 AppConfig.frontendUpdateRepo 决定(标准版 = songloft-org/songloft-player;Bundle 版 = 父仓库 songloft-org/songloft),客户端按渠道自动切换、无需改代码。

  • manifest:https://github.com/<repo>/releases/download/<tag>/manifest-<abi>.json
    • stable:<tag> = v<version>(经 /releases/latest 解析);dev:<tag> = dev
    • 内容为 PatchCheckResult 形状:{"hasUpdate":true,"patch":{"version","semanticVersion","gitCommit","flutterBinding","targetVersionCode","patchUrl","md5"}}(其中 version<semver>-<gitCommit> 形式的补丁标签)
  • patch 包:同 Release 的 patch-<abi>.zip(md5 取 zip 的)
  • 向后兼容:老式 manifest 无 semanticVersion / gitCommit / flutterBinding 时,客户端退回「hasUpdate + versionCode 绑定 + 已应用守卫」旧行为。

发布(build-and-release.ymlbuild-android,自动)

每次发版(tag devv<x.y.z>)自动执行:flutter build apk --release --split-per-abi → 对 arm64-v8a / armeabi-v7a / x86_64dart run flutter_patcher:pack --apk <abi apk> --version <semver>-<commit> --target-version-code <vc> --abi <abi> → 生成 patch-<abi>.zip + manifest-<abi>.json → 随 release 上传。无手动 workflow、无 cherry-pick、无 patch_label 输入。

发布纪律

  • 纯 Dart 改动 → 随下次发版自动带补丁,老包热更、冷重启生效(无需单独打补丁 tag);
  • 动了原生 / 插件原生侧 / 引擎 → Flutter 引擎变则 flutterBinding 键不匹配,老包自动落整包 APK 分支;有意 bump versionCode 时同理走整包,用户被引导下 APK。

验证

  1. flutter analyze 通过;flutter build apk --release --split-per-abi 成功。
  2. 装某 ABI 的老 dev / stable APK 到真机。
  3. 发一版更新(改一处可见 Dart)→ 打开 App → 弹「发现新版本」列前端组件 → 选代理 → 下载 → 重启 → 看到改动 = 成功。
  4. 不兼容路径:升级了 Flutter 引擎的新版 → flutterBinding 不匹配 → 打开应弹「需要下载新版本」→ 跳 /settings
  5. 忽略此版本后同版本不再提示;换更高版本恢复提示。

注意

  • Google Play 等渠道可能限制动态下发可执行 .so,本项目走自控/侧载分发。
  • flutter_patcher 标注 beta,先内测再放量。
  • 要求(已满足):AGP 8.11.1+ / Kotlin 2.2.20+ / Java 17 / minSdk 24 / compileSdk 36 / NDK 27+。