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

Flutter热更新实战:Shorebird接入与自定义CDN部署全攻略

Flutter 开发做到后期最让人头疼的问题之一就是发版。安卓渠道多、商店审核周期不定iOS 那边就更不用说了每次改个一两行代码都要走一遍完整的提审流程用户还得去应用商店手动更新。Shorebird 的出现算是给了 Flutter 开发者一条比较体面的出路——它提供的是 Code Push 级别的原生热更新方案直接基于 Dart VM 的 snapshots 做增量更新跟纯前端套壳那种刷 HTML 的方式完全是两码事。我最早接触 Shorebird 是大概两年前那时候它还在比较早期的阶段只支持 Android后来才慢慢补齐了 iOS。到现在Shorebird 已经可以把 patch 分发到自己的服务器上也就是自定义 CDN这意味着国内团队可以完全绕开海外源的网络问题把补丁包放到自己的 OSS、COS 或者自建静态服务器上。这篇文章我就把整套流程掰开揉碎讲清楚包括 Shorebird 热更新的核心机制、接入步骤、自定义 CDN 下载的完整配置再加上我实际踩过的坑和排查经验希望能帮到正在折腾这块的同学。1. 整体设计与思路拆解1.1 为什么 Flutter 项目需要热更新Shorebird 和传统热更新有什么区别先说痛点。Flutter 应用是编译成原生代码运行的Dart 代码会被 AOT 编译成机器码所以它不像 React Native 或者 uni-app 那样有 JS 引擎可以做动态下发。这意味着一旦线上出了崩溃或者 UI 显示问题你只能重新发包走审核。对于业务迭代频繁的团队来说这个流程的代价非常高昂——尤其是 iOS 审核动辄三五天加上用户更新率永远不可能达到 100%一个线上问题可能要影响一两周才能恢复干净。Shorebird 的解决思路是在编译阶段做文章。它使用自己定制的 Flutter Engine在标准 AOT 编译之外额外生成一份带有补丁能力的 snapshot然后在运行时动态加载这段时间的 patch 包。说白了相当于给原生编译出来的二进制加了一层“外挂”机制内核不动只替换 Dart 代码生成的那部分快照文件。这样既能保持原生性能又让业务代码具备热更新能力两个好处都占了。传统的热更新方案我这边也调研过几个一种是纯前端 WebView 套壳、用 JS 拉远端资源本质上不是真正意义上的热更新另一种是基于动态化框架的自研方案比如腾讯的 LuaView、阿里的 Weex但这些都要求你重新定义 UI 开发范式接入成本太大还有一些是直接做 dex 替换、so 替换的 Android 专用方案跟 Flutter 的 Dart 代码 AOT 编译完全不搭边。Shorebird 的价值就在于它是 Flutter 原生的热更新方案不需要你改架构也不用放弃 AOT 编译只要用它的命令走一遍构建流程就能拿到可热更新的产物。1.2 自定义 CDN 的底层逻辑和择路径Shorebird 官方默认是把 patch 包上传到他们自己的服务。如果你人在海外或者项目用户主要在海外官方 CDN 用着还挺省心。但国内团队就没那么舒服了网络延迟、稳定性都是问题更别提企业内部安全合规要求很多公司根本不允许业务数据包往第三方服务传。这时候就需要把 patch 下载地址改成自己的 CDN。自定义 CDN 在 Shorebird 里的支持方式是这样的它允许你在客户端初始化 Shorebird 的时候指定一个baseUrl这个 URL 指向你自己的服务器Shorebird 会从这个 URL 拉取补丁的元数据和 patch 文件。剩下的事情就好办了——你自己写个脚本在每次构建完补丁包之后把产物上传到 OSS/COS/S3或者干脆扔到一个 Nginx 静态目录里都行。从架构角度来说自定义 CDN 把 Shorebird 分成了两个独立的环节构建环节和分发环节。构建环节仍然依赖 Shorebird 的 CLI 和编译链这是绕不开的分发环节则完全掌握在自己手里无论是国内加速、内网部署还是灰度策略你都可以根据自己的业务场景灵活调整。这也意味着补丁的发布流程可以嵌入到你自己现有的 CI/CD 管道里从一个“第三方服务”变成一个“自有的基础组件”。2. 环境准备与 Shorebird 接入全流程2.1 安装 Shorebird CLI以及为什么推荐用 FVM 管理 Flutter 版本Shorebird 的 CLI 安装很简单官方给了一条命令curl --proto https --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/shorebird/main/install.sh | bash装完之后它会自动装一个shorebird命令到你的用户目录下。然后在你的 Flutter 项目根目录执行shorebird init它就会读取你当前的 Flutter SDK 版本并自动生成对应的 Shorebird 配置。这里我要特别提一下 FVMFlutter Version Management。Shorebird 对 Flutter 版本是有要求的——它的 patch 机制跟 Flutter Engine 的版本绑定得很紧如果你换了 Flutter 版本之前打出来的 patch 可能就没法在旧版本上生效。所以团队协作的时候最怕的就是有人悄悄升了 Flutter 版本然后你这边再打补丁就崩了。用 FVM 可以给每个项目固定一个 Flutter 版本并且通过项目内的.fvmrc文件把版本信息提交到 Git 里谁拉下来代码都是一个版本。安装 FVM 也简单dart pub global activate fvm fvm install 3.22.4 fvm use 3.22.4 --force2.2 登录授权与项目初始化附关键参数说明shorebird init会引导你登录。它走的是 GitHub OAuth 流程在终端里会弹出一个 URL你用浏览器打开、授权然后把回调码粘回终端就行。这一步会生成两个东西shorebird.yaml项目级配置记录了 App ID 和补丁策略。在pubspec.yaml里自动添加shorebird依赖并在main()里注入初始化代码。这里有个细节值得注意。shorebird init生成的初始化代码是这样的import package:shorebird/shorebird.dart; Futurevoid main() async { WidgetsFlutterBinding.ensureInitialized(); await ShorebirdCodePush().init(); runApp(const MyApp()); }ShorebirdCodePush().init()做的事情是异步下载补丁元数据并且根据当前版本号判断是否有新的 patch 需要应用。如果网络条件差或者下载超时它会走兜底逻辑直接用本地已有的代码启动应用不会阻塞用户进入页面。所以把初始化放在runApp前面是安全的但这里有一点最好注意初始化是异步的如果网络请求慢你的应用启动画面会一直卡着等待。我更建议的做法是给它加一个超时控制比如用Future.any配合Future.delayed超过 2 秒就强制进 App避免启动体验受损。2.3 首次构建与发布搞清楚 release 和 patch 的区别Shorebird 的命令体系里有两个核心命令shorebird release和shorebird patch。shorebird release生成的是一个全新的应用版本对应你正常的发版流程。它会打出一个新的 APK/IPA 安装包包含完整的 Dart AOT 快照。每一次应用商店上架的正式包都应该用shorebird release来构建。shorebird patch则是在已有 release 的基础上生成增量补丁。它只包含这次修改涉及的 Dart 代码差异部分体积比完整包小得多。补丁打好后用shorebird patch android或者shorebird patch ios来执行。从实践来看补丁包的大小通常在几十 KB 到几百 KB 不等取决于变化的代码量。因为它是基于 snapshot diff 的所以跟代码变动的逻辑复杂度有关而不是单纯按文件大小计算。我在一个中大型项目里测过一次修改了十几个页面文件补丁包大概 300KB 左右同学跑在移动网络上也是秒级完成下载。3. 自定义 CDN 下载全流程这一步是整个方案的重头戏3.1 Shorebird 自定义 CDN 的配置前提和架构形态Shorebird 官方文档对自定义 CDN 的使用描述比较简略但核心就是一句话初始化时传入自定义的baseUrl即可。这个 URL 需要满足一个条件——目录结构必须跟 Shorebird 官方的 CDN 结构保持一致因为客户端会按照约定好的路径去拼接下载地址。在实际部署中我比较推荐的形态是CDN 域名为https://cdn.example.com。CDN 根目录下有一个shorebird子路径用于存放所有补丁文件。客户端初始化时传https://cdn.example.com/shorebird。然后通过 Shorebird CLI 的shorebird patch构建完成之后产物会输出到某个临时目录你需要自己写一个上传脚本把产物同步到 CDN 上对应的路径。CDN 这边建议开启静态文件缓存文件名字里带了版本号和 hash可以设很长的 cache-control不用担心缓存污染。3.2 客户端代码改造初始化参数与重试策略客户端改造相对简单核心代码如下Futurevoid main() async { WidgetsFlutterBinding.ensureInitialized(); const baseUrl String.fromEnvironment(SHOREBIRD_BASE_URL, defaultValue: https://cdn.example.com/shorebird); await ShorebirdCodePush().init( baseUrl: baseUrl, ); runApp(const MyApp()); }这里我用String.fromEnvironment从编译环境变量里取 CDN 地址。好处是同一套代码可以通过不同的--dart-define编译出测试环境、预发布环境、生产环境的包而不需要改代码。比如flutter build apk --dart-defineSHOREBIRD_BASE_URLhttps://dev-cdn.example.com/shorebird如果希望初始化逻辑更可靠可以做成先尝试从某个本地配置读取 baseUrl读不到再用编译参数最后再落到默认值。这样线上出了紧急问题你还能通过下发一个远程配置来切换补丁下载源不用重新发版。3.3 补丁构建与上传脚本的设计这是整个自定义 CDN 方案里最需要花心思的部分。因为shorebird patch命令只负责把补丁打包出来它不知道你的 CDN 是什么也不会主动帮你上传。所以你需要做的事情是执行shorebird patch android构建补丁从构建输出目录中把补丁文件捞出来上传到 CDN 的指定路径记录版本号、渠道信息方便后续追踪。我给团队写过一个简单的 Shell 脚本逻辑大致如下#!/bin/bash # 构建产物目录由 shorebird 命令输出 ARTIFACT_DIRbuild/shorebird # 执行补丁构建 shorebird patch android --release-version 1.2.3 # 检查产物是否存在 if [ ! -d $ARTIFACT_DIR ]; then echo 产物目录不存在构建可能失败 exit 1 fi # 使用 ossutil/awscli 等工具同步到指定路径 ossutil cp -r $ARTIFACT_DIR oss://your-bucket/shorebird --update # 刷新 CDN 缓存如果 CDN 支持 curl -X POST https://cdn.example.com/api/refresh?path/shorebird这里有几个容易踩的坑。第一个是产物目录里可能会有多个变体的文件比如不同 ABI 架构的产物你要确认这些文件都上传上去了缺一个就可能导致某个架构的设备收不到补丁。第二个是要注意上传的路径不能写错多一个v1或者少一层目录客户端就找不到文件。第三个是刷新 CDN 缓存这一步不能省如果之前 CDN 上已经缓存了同名文件的旧版本你的新补丁可能不会被及时分发出去。3.4 灰度发布策略如何用自定义 CDN 做百分比灰度Shorebird 官方目前没有提供按百分比灰度的能力它的 patch 一旦发布所有已安装对应版本的客户端都会拉到。但自定义 CDN 之后你其实可以通过 CDN 层面的策略来实现灰度。最简单的做法是准备两个目录比如shorebird-stable和shorebird-canary分别对应稳定版和灰度版。灰度期间客户端在初始化时根据设备 ID、用户 ID 或者版本号做样本分配一部分走canary路径一部分走stable路径。等稳定跑一两天再把canary目录的内容同步到stable全部用户就更新到最新补丁了。具体到客户端代码就是初始化前先请求一下自己的配置中心拿到当前用户应该走哪条 CDN 路径然后再调ShorebirdCodePush().init()。这个方案实现成本低效果却非常接近原生热更新框架的灰度能力。我还见过一个团队用更极客的方式——他们直接按 app 启动时间做灰度双数整点启动的走新补丁单数整点启动的走旧补丁虽然听起来有点糙但确实起到了灰度作用。4. 日常工作流怎么跟 Shorebird 结合4.1 开发测试流程本地验证补丁包Shorebird 在本地跑的时候跟普通 Flutter 开发没有任何区别flutter run直接跑的就是当前工作区的代码不会走补丁逻辑。真正要验证补丁是否生效你得先打一个 release 包安装到手机上然后再改代码、打 patch、上传最后重新打开 App 看代码是否被替换了。这个流程有点繁琐我个人的习惯是写一个verify_patch.sh脚本把整个链路串起来拉最新代码 - 打 release - 安装到指定测试机 - 改一行明显的 UI 代码比如加个颜色标记- 打 patch - 上传到测试 CDN - 杀进程重开 App 确认颜色变化。整个过程自动化之后每次验证只需要几分钟效率能提升不少。4.2 监控和回滚发现问题怎么紧急下架或回退补丁发布出去之后最重要的就是监控。因为补丁是强制性的——只要客户端拉到它就会自动应用用户没有选择权。所以一旦补丁有 bug影响面可能就是全量用户。我建议至少要做以下监控崩溃率打补丁前后的 Crash 率对比通常用 Firebase Crashlytics 或者 Sentry。核心业务接口成功率补丁涉及的核心接口请求成功率。活跃用户变化如果出现断崖式下跌大概率是补丁有问题。回滚方面Shorebird 本身没有一键回滚功能但自定义 CDN 之后你可以通过删掉 CDN 上的补丁文件来让新用户无法下载补丁。更稳妥的做法是保留上一个补丁的文件不删除如果新补丁有问题就把 CDN 的目录指回到旧补丁的版本——这又回到灰度那一套逻辑上去了。总之自定义 CDN 给了你更大的自由度也把运维责任转移到了你自己身上别把最后一道防线丢了。4.3 多环境管理测试、预发、生产互不干扰多环境管理是另一个容易踩坑的地方。Shorebird 的 App ID 是区分补丁应用范围的第一层标识但同一个 App ID 下无法区分不同环境。我的建议是为每个环境单独申请一个 App ID也就是每个环境在 Shorebird 后台都是一个独立应用。不过这样做的代价是你在 CI 里要为每个环境走一遍完整的shorebird init流程。如果你不想搞多个 App ID也可以用 CDN 路径来区分。比如测试环境走https://dev-cdn.xxx.com/shorebird生产环境走https://prod-cdn.xxx.com/shorebird再用--dart-define注入不同的 baseUrl。这个方法更轻量我目前用的就是这套组合测试环境一个 Shorebird App ID 一个测试 CDN生产环境一个正式 App ID 一个正式 CDN两边的数据完全隔离。5. 常见问题与排查技巧实录5.1 补丁下载失败但应用正常启动怎么定位问题用户反馈“补丁没生效”但是 App 又能正常用这种情况最常见的原因是补丁下载失败后走了兜底逻辑。排查思路如下第一步先确认客户端访问 CDN 的网络状态。在代码里加一个日志打印init()的返回结果或者请求状态码。我之前遇到过一个情况用户访问 CDN 总是返回 405原因是 CDN 的回源鉴权配置把客户端默认的 GET 请求给拦截了。这种问题如果你不在客户端打日志光看服务端日志能排查到怀疑人生。第二步确认补丁文件的版本是否和客户端匹配。Shorebird 的补丁是针对特定 release 版本生成的如果客户端安装包是一个旧版本但你打的 patch 是基于新 release那旧客户端是无法识别这个补丁的。这个报错信息通常会打出类似patch version mismatch的提示。第三步检查 CDN 目录结构。如果baseUrl配错了层级客户端会沿着错误的路径去请求补丁文件自然就是 404。我整理了常见的目录结构对照表方便大家排查。场景客户端 baseUrl补丁文件实际路径常见问题官方 CDNhttps://shorebird-cdn.example.com/{appId}/{patchId}/patch.zip无需自定义直接用默认自建 CDNhttps://cdn.example.com/shorebirdhttps://cdn.example.com/shorebird/{appId}/{patchId}/patch.zipbaseUrl 必须包含子路径内网部署http://10.0.0.5:8080/shorebirdhttp://10.0.0.5:8080/shorebird/{appId}/{patchId}/patch.zip需要确认网络策略允许上下行5.2 升级 Flutter 版本后打补丁失败这是很多人会掉进去的坑。Shorebird 的 SDK 和引擎是跟随 Flutter 版本走的如果你升级了 Flutter 版本但 Shorebird CLI 没升级两边版本不匹配就会报错。典型错误是Shorebird is not compatible with this version of Flutter解决方案很简单升级 Shorebird CLI。但要注意你的应用客户端代码里也嵌入了 Shorebird 的 SDK这个 SDK 版本也会影响兼容性。如果客户端已经发布出去了用户手机里跑的还是老版本的 Shorebird SDK你新打的补丁用的是新版本 SDK就会出现旧客户端无法验证新补丁的问题。所以生产项目里升级 Shorebird 版本前一定要先出一版带新 SDK 的正式包等用户更新得差不多了再开始打新补丁。5.3 Android 打包常见报错Unable to find suitable Visual Studio toolchain 与 Gradle Plugin 冲突这两个报错是 Flutter Android 开发里的老熟人了。“Unable to find suitable Visual Studio toolchain”这个错误常见于 Windows 环境下 Flutter 构建 Android 原生模块CMake 找不到可用的 Visual Studio 环境。解决办法是在系统环境变量里明确指定 Visual Studio 的安装路径或者安装 “Desktop development with C” 工作负载。如果你用的构建环境是 CI 的 Windows runner记得在 runner 初始化时把 Visual Studio Build Tools 装好。Shorebird 方面还有个特定问题——它的 Gradle 插件跟 Flutter 默认的 Gradle 插件如果同时按传统方式 apply会报类似下面的错误You are applying Flutters main Gradle plugin imperatively using the apply这是因为 Shorebird 需要以特定的方式注入 Gradle 插件。解决方案是严格按照 Shorebird 文档的步骤来配置android/settings.gradle不要自己去改apply逻辑。我第一次接入的时候手贱动了 Gradle 配置结果折腾了一下午。5.4 iOS 上的坑补丁包签名验证和 App Store 合规性iOS 这边要比 Android 复杂一些因为 Shorebird 的 patch 功能在 iOS 上依赖于它定制的 Flutter Engine并且绕过了 App Store 的常规代码下发机制。从技术上来说Shorebird 用的仍然是苹果允许的动态库加载机制所以理论上不会被拒但你需要留意Shorebird 的 iOS 补丁只支持 release 构建Debug 模式不支持。补丁下载需要网络权限如果你的应用主要面对国内用户要确保 CDN 域名在 iOS 的 ATSApp Transport Security白名单里否则 HTTP 请求会被系统直接拦截。还有一个细节是iOS 补丁的下载时机。由于 iOS 系统的进程管理机制补丁的检查更新频率不能太高Shorebird 的默认行为是在 App 启动时检查一次。如果你需要更频繁的检查逻辑可以用ShorebirdCodePush暴露的方法手动触发检查但要注意别在后台任务里做太频繁的网络请求否则可能被系统判为滥用。6. 我的实操心得与经验总结6.1 从架构视角看热更新绝不是“能跑就行”的事如果只是个人项目或者内部工具热更新做到能下发补丁就跑起来也许问题不大。但在商业项目里你必须要考虑三个事情版本兼容性、灰度策略、回滚能力。Shorebird 本身提供了前三者的基础能力自定义 CDN 则补上了回滚和灰度自由度这块短板。我个人的判断是这套方案特别适合三类团队一是已经深度使用 Flutter 并且在迭代节奏上被应用商店审核折磨的团队二是业务链路涉及后端配置下发、需要快速修复线上问题的团队三是有运维能力、希望把补丁分发纳入自有基础设施的团队。如果你只是想做一个小 Demo感受一下热更新的效果大可不必上自定义 CDN 的完整链路直接用 Shorebird 官方 CDN 就行。等真正到了生产环境、规模上来之后再考虑把 CDN 切到自己这边。6.2 给别人踩坑的几条个人忠告第一永远保留最近两个稳定版本的可回滚状态。CDN 上的历史补丁文件不要急着清理磁盘存储花不了多少钱出问题的时候能救命的。第二补丁包进入 CDN 之前一定要做校验。最好在 CI 脚本里做一个冒烟测试把补丁包解压出来确认里面关键文件的大小和 hash 跟预期一致再上传。第三客户端初始化补丁检查的时机最好放在首帧渲染完成后进行而不是在main()里同步等待。虽然 Shorebird 的设计是异步初始化但为了不阻塞启动我一般会用一个 Splash Screen 把初始化逻辑包起来。最后分享一个我实际用的技巧可以在ShorebirdCodePush().init()返回的结果对象里读取当前补丁的版本号然后把这个版本号上报到公司的埋点系统。这样一来通过埋点数据就能实时掌握线上有多少用户跑的是哪个补丁版本判断灰度进度和覆盖率非常直观。这比事后靠崩溃率猜用户分布要靠谱得多。Shorebird 这套方案我跑了半年多前前后后在线上了十几个补丁整体稳定性还是很让人放心的。自定义 CDN 的配置并不复杂真正复杂的部分在于把它嵌入到你的发布流程、监控体系和运维体系里形成一套完整的版本管理闭环。当你按下shorebird patch回车的那一刻心里有底的感觉跟以前那种改了代码只能眼巴巴等审核的日子完全是两个世界。
分享:

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

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