首页 / 资讯中心 / 文章详情

uni-app热更新与APK覆盖安装:wgt打包、版本校验与避坑指南

uni-app热更新与APK覆盖安装:wgt打包、版本校验与避坑指南 ★ FEATURED ARTICLE
简介面向 HBuilder X 与 uni-app 开发者的版本更新配套包聚焦应用热更新资源制作、wgt 包组织与 APK 安装升级场景适合正在处理移动端版本发布、资源替换和客户端更新的初中级开发者参考。压缩包共 35 个文件整体约 718KB以 png 图片、js 脚本、css 样式、json 配置、html 页面和 ttf 字体为主涵盖界面素材、交互逻辑、样式表、应用清单、入口页面与字体资源等常见组成便于对照理解热更新包内不同类型文件的职责与引用关系。已有 2916 人学习下载说明该样例在校验资源包结构和更新流程方面具有一定参考价值。读者可从中了解热更资源包的目录组织、应用清单与页面入口配置、静态资源引用方式以及 APK 安装更新时素材与配置的搭配思路用于排查资源路径、版本包制作和升级过程中的常见问题。适合作为 HBuilder X 项目版本迭代、热更新联调与安装包整理时的辅助样例。1. 热更不是「发个包就完事」先拆清 wgt 与 APK 的分工线上版本炸了运营在群里催用户截图显示页面是三天前的旧样式这时候最省事的做法不是重新打一个全量 APK 让人去应用市场重新下载而是下发一个几百 KB 的 wgt 资源包用户重启一次就换了一副面孔。HBuilderX 版本更新这套东西说到底就是把「热更资源」和「安装 APK」当成两条并行的通道来用前者负责 JS、CSS、Vue 页面、静态图片这些跑在 webview 里的东西后者负责原生插件、权限、启动图、SDK 配置这些打包进安装包的部分。很多人第一次在 HBuilderX 里点「制作应用wgt包」以为从此所有改动都能热更结果加了原生插件之后热更包死活装不上这就是没搞清边界。这篇内容面向的是已经用 uni-app 出过包、正在补更新链路的前端和客户端同学说清版本号怎么定、wgt 怎么打怎么装、APK 怎么覆盖安装、以及那些会让你半夜爬起来的坑。2. 版本号与更新清单manifest.json 里三个字段的真实分工在动「制作wgt包」这个按钮之前先把版本号这件事定死。HBuilderX 的更新链路里版本号既是展示给用户看的字符串也是客户端判断「要不要更新」的唯一依据还是 Android 系统决定能不能覆盖安装的硬条件。这三个用途分别对应 manifest.json 里三个不同的位置混用了就等着出乱子。2.1 applicationId、versionName、versionCode 各管什么打开 manifest.json 的可视化界面在「基础配置」里能看到应用标识和两个版本字段切到源码视图长这样{ name: 某应用, appid: __UNI__A1B2C3D, versionName: 2.3.0, versionCode: 230, app-plus: { usingComponents: true, distribute: { android: { packagename: com.example.someapp, keystore: ..., permissions: [] } } } }appid是 HBuilderX 给这个应用分配的标识热更检测时plus.runtime.appid读的就是它服务端按它下发清单这个值不要手动改改了等于换了个应用。versionName是给人看的字符串用户在「关于」里看到的就是它格式建议严格三段式主.次.修订因为客户端要按点号切分做比较。versionCode是给机器看的整数Android 系统用它在覆盖安装时判断新旧热更链路里用它做「强更阈值」的下限这个值只能涨不能跌。packagename是 Android 的包名它和签名文件一起决定了 APK 能不能覆盖安装。包名变了系统会当成两个完全不同的应用并存签名换了覆盖安装会直接报INSTALL_FAILED_UPDATE_INCOMPATIBLE。血泪经验是项目一开始就把包名和签名文件定下来签名文件连同密码放进内部密码库别放在某个人的笔记本桌面上。需要特别提醒的是versionCode在源码里是字符串但实际值必须是纯数字写2.3.0或者v230都会在打包或安装阶段出问题。可视化界面里它是个数字输入框切到源码视图之后很容易被手误加上引号里的点。2.2 服务端清单该返回哪些字段客户端不该自己去猜要不要更新判断逻辑放服务端更稳客户端只负责执行。一份够用的更新清单大概长这样{ code: 0, data: { latestVersionName: 2.4.0, latestVersionCode: 240, minVersionCode: 200, wgt: { url: https://static.example.com/pkg/__UNI__A1B2C3D_240.wgt, size: 486233, md5: 9f2c1e5a7b3d4c8e0f1a2b3c4d5e6f70, force: false }, apk: { url: https://static.example.com/pkg/someapp_240.apk, size: 38742016, md5: 1a2b3c4d5e6f708192a3b4c5d6e7f809, force: false }, notice: 修复了订单页在弱网下的重复提交问题 } }latestVersionCode和客户端当前值比较决定要不要提示minVersionCode是强更红线当前值低于它就必须走全量安装不给「稍后再说」的选项wgt.size和wgt.md5是下载完成后的校验依据少了这两个字段服务端返回一个 502 的 HTML 错误页也能被当成更新包存下来安装时才报错排查成本翻好几倍。wgt.force和apk.force分开控制因为热更强制重启的体验代价比一次全量安装小得多。我一般的策略是普通资源改动走 wgt 且不强制用户下次冷启动生效涉及支付、登录态、数据结构的改动走 wgt 且强制弹窗只有一个「立即重启」按钮。清单本身要做缓存控制接口返回头带上Cache-Control: no-cache或者 URL 上带时间戳。见过不止一次因为 CDN 缓存了旧清单灰度都结束了还有一批用户卡在中间版本上反复横跳。2.3 灰度、强更与降级这三个开关灰度不是「服务端随机返回」那样用户重装一次就换了一个分组体验很割裂。稳妥的做法是用设备维度的稳定哈希拿appid 设备唯一标识做一次哈希对 100 取模落在指定区间内的才下发新版本清单。这样同一个设备永远在同一组出问题能精确圈定范围。强更的判断顺序不能颠倒先看minVersionCode低于红线直接走 APK 全量再看latestVersionCode高于当前值才考虑 wgt最后才看 wgt 自身的force标记。顺序反了会出现「用户被强制重启了三次但版本号一直没变」的死循环。降级这件事要提前想清楚。用户装了 2.4.0 的 APK服务端因为事故把清单回滚到 2.3.0这时候客户端拿到的是比自己低的版本号比较函数必须返回「不更新」而不是「更新」。同时 wgt 的版本号一定要跟着 APK 走不能出现「APK 是 2.4.0下发的 wgt 是 2.3.0」这种倒挂否则安装时会被版本校验拦下来。下面这个比较函数是所有判断的基础写成公共模块别在每个页面里复制粘贴/** * 比较两个点分版本号 * param {string} a 当前版本如 2.3.0 * param {string} b 目标版本如 2.4.0 * returns {number} 1 表示 ab-1 表示 ab0 表示相等 */ function compareVersion(a, b) { const pa String(a || 0).split(.).map(n parseInt(n, 10) || 0) const pb String(b || 0).split(.).map(n parseInt(n, 10) || 0) const len Math.max(pa.length, pb.length) for (let i 0; i len; i) { const x pa[i] || 0 const y pb[i] || 0 if (x y) return 1 if (x y) return -1 } return 0 }这段代码的关键在parseInt(n, 10) || 0它能吃掉2.3.0-beta这种带后缀的版本号把beta解析成 0避免NaN参与比较导致整个判断恒为 false。缺位补 0 的逻辑让2.3和2.3.0判定为相等方便运维在清单里少写一位。len取两者最大长度而不是固定 3是为了兼容以后可能出现的四段式版本号。判断入口建议只写一处做成checkUpdate(options)import manifestFallback from /common/version.json export function checkUpdate({ silent false } {}) { const sys uni.getSystemInfoSync() const currentCode Number(sys.appVersionCode || 0) const currentName sys.appVersion || 0.0.0 return new Promise((resolve, reject) { uni.request({ url: https://api.example.com/app/version, method: GET, data: { appid: plus.runtime.appid, code: currentCode, t: Date.now() }, timeout: 8000, success: (res) { if (res.statusCode ! 200 || !res.data || res.data.code ! 0) { return resolve({ action: none, reason: bad-response }) } const d res.data.data if (currentCode d.minVersionCode) { return resolve({ action: apk, force: true, info: d }) } if (d.wgt compareVersion(d.latestVersionName, currentName) 0) { return resolve({ action: wgt, force: !!d.wgt.force, info: d }) } if (d.apk Number(d.latestVersionCode) currentCode) { return resolve({ action: apk, force: !!d.apk.force, info: d }) } resolve({ action: none }) }, fail: (err) { if (!silent) uni.showToast({ title: 检查更新失败, icon: none }) reject(err) } }) }) }uni.getSystemInfoSync()给出的appVersion和appVersionCode在热更生效并重启之后读到的就是 wgt 包里的新值所以在一次 wgt 更新完成之后不要用内存里缓存的旧版本号继续判断否则会连续弹两次更新提示。silent参数用于启动时静默检查失败不打扰用户手动点「检查更新」时才走 toast。timeout设 8 秒是有意压低的更新检查不该阻塞首屏。3. 打 wgt 包与下发从 HBuilderX 到 plus.runtime.install版本号定好之后热更这件事就变成了三步流水线把新代码打成 wgt、把 wgt 传到静态资源服务器、客户端下载并安装。每一步都有能让你卡半天的细节。3.1 制作 wgt 包入口、命名与不可热更清单HBuilderX 里的入口在菜单「发行」→「原生App-制作应用wgt包」弹窗里会让你选输出路径并显示当前 manifest.json 里的版本名称和版本号。这里最容易翻车的一点这个弹窗不会帮你改版本号。你如果忘了先把versionName从 2.3.0 改成 2.3.1打出来的包版本和线上一致客户端比较之后认为不需要更新或者装上去之后版本号没变下次启动又提示一次更新形成鬼打墙。我的习惯是打包前先改 manifest.json改完在「关于」页里核对一遍再点「制作应用wgt包」。输出文件按appid_版本号.wgt命名比如__UNI__A1B2C3D_231.wgt好处是运维在服务器上扫一眼就知道哪个包对应哪个版本也方便做版本回溯。还有一份「不可热更清单」必须刻在脑子里。下面这些改动打 wgt 没用必须走 APK 全量改动类型能否热更说明Vue 页面、JS 逻辑、CSS 样式可以最常见的场景静态图片、字体、本地 JSON可以注意包体积wgt 别超过 10MB新增/升级原生插件不可以插件是编译进安装包的修改 manifest 权限、SDK 配置不可以权限清单在安装时确定启动图、应用图标、应用名称不可以属于安装包资源修改 packagename 或签名不可以等于换了个应用一个实际边界manifest.json里app-plus下的distribute配置改了wgt 包里虽然带着这份文件但安装时不会重新走一遍原生初始化所以你改了推送配置、地图 Key热更之后是完全不生效的。这类改动只能全量。另外wgt 包是用调试基座跑的如果你本地用的是自定义基座调试热更包打出来之后在自定义基座上是装不上的因为基座的包名和正式包不一致。验证热更一定要用正式签名的包。3.2 下载阶段进度、校验与断点下载 wgt 用plus.downloader比uni.downloadFile更合适原因是前者能拿到实时进度、能指定存放目录、能在失败时拿到明确的错误描述。uni.downloadFile的临时文件路径在部分 Android 机型上会被系统清理导致「下载成功但安装时文件不存在」。/** * 下载热更资源 * param {object} pkg { url, size, md5 } * param {function} onProgress 进度回调参数 0-100 */ function downloadWgt(pkg, onProgress) { return new Promise((resolve, reject) { // filename 以 / 结尾表示这是个目录文件名由下载器自动生成 const task plus.downloader.createDownload( pkg.url, { filename: _doc/update/, retry: 2, timeout: 30 }, (download, status) { if (status ! 200 || !download.filename) { return reject(new Error(下载失败HTTP status)) } // 相对路径转绝对路径后续 install 需要绝对路径 const absPath plus.io.convertLocalFileSystemURL(download.filename) resolve({ path: absPath, raw: download.filename }) } ) task.addEventListener(statechanged, (t, st) { if (st 3 t.getFileName) { // 3 表示 downloading const total t.totalSize || pkg.size const done t.downloadedSize || 0 if (total 0 typeof onProgress function) { onProgress(Math.floor((done / total) * 100)) } } }) task.start() }) }filename: _doc/update/指向应用私有文档目录下的 update 子目录这个目录在应用卸载前不会被系统清理也不会被用户手动删到。结尾的斜杠不能省省了会被当成文件名下载器会把整个目录当成文件写结果是写入失败。retry: 2表示失败重试两次弱网环境下能救回不少。timeout: 30单位是秒wgt 一般不大30 秒足够超大包再往上调。statechanged回调里的st 3是下载中状态t.totalSize在服务端没返回Content-Length时会是 0所以用清单里的pkg.size兜底。进度回调里做节流别每一帧都去 setData 更新进度条那是白烧性能。下载完成之后必须校验这一步省了就等于把线上稳定性交给运气// 文件大小校验允许 1KB 内的浮动防止服务端压缩造成差异 function checkSize(download, expectSize) { if (!expectSize) return true const real download.totalSize || 0 return Math.abs(real - expectSize) 1024 } // MD5 校验需要原生能力用 plus.io 读文件后交给原生计算 function checkMd5(filePath, expectMd5) { return new Promise((resolve) { if (!expectMd5) return resolve(true) plus.io.resolveLocalFileSystemURL(filePath, (entry) { entry.file((file) { const reader new plus.io.FileReader() reader.onloadend () { // 大文件不要在这里读成字符串交给原生 md5 工具或跳过 resolve(true) } reader.readAsDataURL(file) }) }, () resolve(false)) }) }大小校验是最划算的一道防线CDN 回源失败返回的 HTML 错误页通常只有几百字节到几 KB和 wgt 的实际大小差好几个数量级Math.abs一算就露馅了。MD5 校验在纯 JS 侧做大文件会很慢实际项目里我一般只在小包小于 2MB上做全量 MD5大包只校验大小和首尾若干字节的哈希或者干脆依赖 HTTPS 加 CDN 的完整性保证。3.3 安装与重启回调里该做什么安装 wgt 就一个 API但周边处理决定了用户感知。/** * 安装 wgt 并重启 * param {string} filePath 绝对路径 * param {boolean} force 是否强制安装忽略版本号校验 */ function installWgt(filePath, force false) { uni.showLoading({ title: 正在应用更新, mask: true }) plus.runtime.install( filePath, { force }, () { uni.hideLoading() // 安装成功不等于生效必须重启 plus.runtime.restart() }, (err) { uni.hideLoading() const msg (err (err.message || err.msg)) || 未知错误 console.error([wgt] install failed:, JSON.stringify(err)) uni.showModal({ title: 更新失败, content: 资源包应用失败 msg, showCancel: false }) } ) }force参数的含义是「忽略版本号比较强制安装」。正常流程里传false就行只有一种场景要传true你在做内部测试反复用同一个版本号覆盖安装。线上传true是很危险的它会让降级包也能装进去装完版本号倒退下次启动又要更新形成来回横跳。安装成功回调里直接调plus.runtime.restart()这是官方推荐路径。但实际线上有极小概率遇到重启后白屏尤其是在低端 Android 机型上原因是重启时旧 webview 还没完全销毁。稳妥一点的做法是延迟一小段时间再重启setTimeout(() { plus.runtime.restart() }, 300)300ms 是经验值太短没效果太长用户会觉得卡。另外重启前要把「已提示过的版本号」写进本地存储防止用户重启之后因为网络抖动又拉到一个旧清单再次弹窗。存储的 key 用wgt_ignored_version值是版本名称检测逻辑里先读它相等就跳过直到服务端下发的版本号变了才清除。提示iOS 端的热更行为受平台规则约束实际项目里通常只用于修复级别的小改动涉及功能新增一律走全量发版。这部分按各自团队的合规要求执行不要照搬 Android 的策略。4. APK 覆盖安装Android 7.0 之后的 FileProvider 与权限链热更包解决不了原生层面的改动该走全量还是得走。APK 的下载和安装比 wgt 麻烦麻烦在 Android 从 7.0 开始收紧了文件共享从 8.0 开始收紧了「安装未知来源应用」的权限两条限制叠在一起就是「下载完了点安装毫无反应」这个经典现象。4.1 什么时候必须走 APK 全量判断依据不是「wgt 装失败了就换 APK」而是改动本身的性质。新增原生插件、调整 SDK 配置、修改权限清单、更换启动图、升级 uni-app 编译器大版本这五类必须全量。还有一种隐性情况wgt 包体积超过 10MB 之后下载失败率和安装失败率都会明显上升尤其是弱网地区的用户这时候把资源打进 APK 反而更省事。全量安装的触发条件要写进清单逻辑里别让客户端自己判断。服务端在apk节点下给出url、size、md5、force客户端只做三件事下载、校验、调起安装器。判断逻辑集中在一处出问题好复现。还有一点容易被忽略APK 的versionCode必须大于当前设备上已安装的版本包括热更后的版本。如果用户热更到了 2.4.0 的 wgt而服务端下发的 APK 是 2.4.0 但versionCode没涨覆盖安装会被系统拦下来。所以 wgt 和 APK 的版本号要统一规划APK 的 versionCode 永远是这一批改动的上界。4.2 FileProvider 与安装未知来源权限的配置链Android 7.0 之后应用之间共享文件必须走FileProvider直接传file://开头的 URI 会抛FileUriExposedException。HBuilderX 云端打包的默认基座里已经带好了这套配置但如果你做过离线打包、改过 AndroidManifest、或者用了自定义基座就必须自己核对一遍。!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.REQUEST_INSTALL_PACKAGES / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / application provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider /applicationREQUEST_INSTALL_PACKAGES是 Android 8.0 引入的权限没有它安装器调起来了也会立刻被系统关掉用户看到的就是「闪一下就没了」。maxSdkVersion28是给存储权限加的上界Android 10 之后分区存储生效再申请这个权限没有意义反而会在应用商店审核时被挑出来。file_paths.xml里要覆盖 APK 的实际存放目录?xml version1.0 encodingutf-8? paths external-files-path namedownload pathDownload/ / files-path nameinternal pathupdate/ / cache-path namecache path. / /pathsexternal-files-path对应应用外部私有目录files-path对应内部私有目录cache-path对应缓存目录。三个都写上不浪费能省掉后面「为什么安装器说找不到文件」的排查。如果权限没配全plus.runtime.install的失败回调里给出的信息通常很含糊只说「安装失败」不看 logcat 基本定位不到。云端打包的项目这些配置默认已经处理好你要做的只是在 manifest.json 的「App权限配置」里勾上安装未知应用的权限。离线打包的项目务必在每次升级 HBuilderX 大版本之后重新对一遍 AndroidManifest官方基座的配置会变。4.3 安装失败对照表与签名校验APK 的安装失败比 wgt 更难查因为错误发生在系统安装器里JS 侧拿到的只是一个笼统的失败。把下面这张表贴在团队文档里能省掉大量重复问答现象大概率原因处理方式点安装毫无反应缺少 REQUEST_INSTALL_PACKAGES补权限并重新打包提示「解析包时出现问题」下载文件不完整或不是 APK校验 size 和 md5检查 CDN提示「应用未安装」签名与已安装版本不一致统一签名文件卸载重装验证提示「版本降级」versionCode 未递增提升 versionCode 后重新打包安装器闪一下就消失FileProvider 路径没覆盖到补 file_paths.xml 条目安装成功但打开是旧版设备上有多个同包名应用检查是否有测试包共存签名这一条值得单独说。Android 要求覆盖安装的 APK 签名与已安装版本完全一致包括证书、密钥算法、有效期。换过签名文件的包用户只能先卸载再装而卸载会丢本地数据。所以签名文件从项目第一天起就要归档交接时作为一个交付物别等到发版前才发现签名找不到了。调起安装的代码和 wgt 类似只是force传true并且要在下载完成后做一次可安装性预检/** * 下载并安装 APK * param {object} apk { url, size, md5 } */ async function installApk(apk) { const file await downloadApk(apk) // 预检文件存在且大小合理 const ok await new Promise((resolve) { plus.io.resolveLocalFileSystemURL(file.path, (entry) { entry.file((f) resolve(f.size 1024 * 1024), () resolve(false)) }, () resolve(false)) }) if (!ok) { uni.showModal({ title: 安装包异常, content: 请稍后重试, showCancel: false }) return } plus.runtime.install( file.path, { force: true }, () { console.log([apk] installer launched) }, (err) { console.error([apk] install failed, JSON.stringify(err)) uni.showModal({ title: 安装失败, content: 请检查是否允许安装未知来源应用, showCancel: false }) } ) }force: true在这里是必须的因为 APK 覆盖安装本身就是「强制」语义。预检里的 1MB 阈值是个经验下限正常 APK 不可能小于这个数小于说明下载到的是错误页。resolveLocalFileSystemURL的回调里只判断存在性和大小不做全量读取避免大文件阻塞主线程。5. 避坑与排查四条熬夜换来的记录更新链路的问题有个共同特征本地怎么测都好一上量就出事。下面四条是实际项目里反复踩过的按「现象 → 原因 → 解决」写清楚。5.1 热更装成功了但页面还是旧的现象日志显示plus.runtime.install成功回调已触发应用也重启了但用户看到的还是旧页面版本号也没变。原因最常见的是打 wgt 包时忘了改versionName包内版本和线上一致安装时被版本校验静默跳过回调却仍然返回成功。第二种可能是下载到的文件其实是个 HTML 错误页.wgt后缀是下载器按命名规则加的内容根本不是 zip安装器解压失败但不报错。第三种是应用跑在自定义基座上基座包名和正式包不同热更装到了另一个应用里。解决打包前把版本号改动做成清单里的第一步改完在「关于」页核对下载完成后强制校验大小和文件头wgt 本质是 zip前两个字节应该是PK读一下文件头就能判断验证热更一律用正式签名的包不用自定义基座。查到问题之后把已提示版本写进本地存储并加一个「版本号 文件大小」的联合 key避免缓存命中错误状态。5.2 更新提示反复弹重启完还是弹现象用户点「立即更新」、重启、进入首页几秒后更新弹窗又来了循环三次以上。原因客户端读到的当前版本号没有及时刷新。热更安装成功后如果应用没有真正重启比如用户在重启前杀掉了进程内存里缓存的appVersion还是旧值下次启动时服务端下发的仍是同一个新版本比较结果依然是「需要更新」。另一种情况是服务端清单被 CDN 缓存用户已经更新到最新版清单还停在旧数据上。解决安装成功后写入pending_version标记启动时先读标记如果标记里的版本等于服务端下发的版本直接清除标记并跳过弹窗给系统一次完整的冷启动机会。清单接口加时间戳参数并设置Cache-Control: no-cache。判断逻辑里加一条保险连续两次提示同一版本且安装都成功第三次直接跳过并上报埋点宁可漏更一次也别把用户锁在弹窗里。5.3 APK 下载完点安装没任何反应现象进度条走到 100%日志显示下载成功点「立即安装」之后界面无变化也没有任何错误提示。原因Android 8.0 之后缺少REQUEST_INSTALL_PACKAGES权限系统会静默拦截安装请求。Android 7.0 之后如果 FileProvider 的file_paths.xml没有覆盖 APK 的实际目录安装器拿到 URI 之后无法读取文件同样表现为无反应。还有一种是plus.runtime.install传了相对路径而不是绝对路径内部转 URI 失败。解决先用plus.io.convertLocalFileSystemURL把路径转成绝对路径再传核对 AndroidManifest 里的权限和 provider 配置离线打包项目每次升级 HBuilderX 后重新对一遍在失败回调里把JSON.stringify(err)完整打出来而不是只弹一句「安装失败」。真机排查时连上电脑看 logcat关键字搜FileUriExposed和SecurityException比在 JS 层猜快得多。5.4 覆盖安装之后版本反而倒退了现象用户装了新 APK打开一看版本号比之前热更的还低然后又被提示更新一次。原因wgt 的版本号跑在了 APK 前面。比如 APK 是 2.3.0versionCode 230热更把资源更到了 2.4.0这时候服务端下发 2.3.1 的 APKversionCode231 大于 230系统允许覆盖安装装完之后应用版本回到 2.3.1比之前的 2.4.0 低。解决版本号统一由一个地方管理APK 的versionCode永远是同批改动的最大值。具体做法是每次发版先定 APK 版本wgt 版本只能小于等于 APK 版本且 wgt 只用于「APK 版本内的增量修复」。清单接口里加一条校验如果apk.latestVersionCode小于等于当前设备已热更到的版本服务端直接返回不更新把矛盾挡在服务端。6. 上线前跑一遍这套自检比事后看日志便宜链路写完只是一半真正决定线上稳定的是发版前那十几分钟的核对。我现在的习惯是把下面这段检查做成一个内部页面挂在测试包的「设置」里每次发版前手动点一遍比对着文档逐条确认靠谱得多。/** * 发版前自检在测试包里调用输出到控制台 */ function preflightCheck() { const sys uni.getSystemInfoSync() const report { platform: sys.platform, appid: plus.runtime.appid, versionName: sys.appVersion, versionCode: sys.appVersionCode, // 1. 基座是否为正式签名自定义基座会返回 false isReleaseBase: plus.runtime.appid !sys.appVersion.includes(debug), // 2. 下载目录是否可写 downloadDirWritable: false, // 3. 安装权限是否已授予仅 Android installPermission: true } // 探测下载目录 plus.io.resolveLocalFileSystemURL(_doc/, (entry) { entry.getDirectory(update, { create: true }, () { report.downloadDirWritable true console.log([preflight], JSON.stringify(report, null, 2)) }, () { console.log([preflight] 目录创建失败, JSON.stringify(report, null, 2)) }) }, () { console.log([preflight] 私有目录不可用, JSON.stringify(report, null, 2)) }) }这段代码做了三件事把当前运行环境的版本信息固化下来方便和服务端清单对照探测_doc/update/目录能否创建提前暴露存储权限问题标记基座类型防止拿调试基座去验证正式热更。getDirectory的create: true参数会在目录不存在时自动创建正好模拟真实下载路径。自检之后还有几条硬性检查用清单形式过一遍服务端清单里的latestVersionCode是否大于线上所有已知版本包括灰度组的wgt 包的versionName是否比线上高一个修订号且没超过 APK 的versionCodeAPK 的签名是否和线上包一致用apksigner verify或安装到已有旧版的设备上验证静态资源服务器是否对.wgt和.apk返回了正确的Content-Type.wgt建议application/octet-stream避免被浏览器或 CDN 当成文本处理灰度比例是否从 1% 起步观察一小时无异常再放到 10%一个具体技巧把「更新检查」和「更新执行」彻底分开。检查只在启动和手动触发时发生执行只在用户确认后发生中间不允许有任何自动跳转。见过太多项目把这两件事揉在一起启动时检查到更新直接开始下载用户流量被吃掉几十兆还不知道发生了什么投诉率飙升。还有一个经验值wgt 包的体积控制在 3MB 以内超过这个数就把静态资源拆出去走 CDN 引用。下载耗时和失败率不是线性增长的5MB 以上的包在弱网下的失败率会突然抬头而失败重试又会进一步放大流量成本。从那以后我每次发版不管改动多小都会先在测试机上把「旧版 → 热更 → 重启 → 校验版本 → 再热更一次」「旧版 → APK 覆盖 → 校验版本」两条路径各走一遍确认没有鬼打墙的弹窗、没有版本倒挂、没有安装器静默失败。这套流程花不了二十分钟比线上出事后翻日志、回滚、写复盘便宜太多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站