13、客户端升级流程:系统更新应用架构、Settings中的更新UI、下载管理器与安装调度
好,咱们今天聊聊客户端升级流程。说白了,就是用户手机上那个「系统更新」按钮点下去之后,背后到底发生了什么。
我刚开始接触这块时,以为就是个简单的下载+安装。后来发现,这里面的门道可真不少。从UI展示到下载管理,再到安装调度,每一步都可能踩坑。
嗯,咱们一步步拆开看。
系统更新应用的整体架构
Android系统更新应用,通常叫 SystemUpdate 或者 Settings 里的一个子模块。它的架构其实挺清晰的,我画了张图帮你理解。
你看,从上到下分了四层。UI层负责展示,业务逻辑层做决策,服务层干脏活累活,系统层最后收尾。每一层各司其职,耦合度很低。
我个人习惯把这种架构叫做「洋葱模型」——从外到内,一层包一层。这样做的好处是,哪天你想换UI,或者换下载引擎,都不影响其他层。
Settings中的更新UI
Settings里的系统更新入口,一般在「关于手机」->「系统更新」里。这个UI看起来简单,其实要考虑的场景不少。
核心UI状态机:
- 空闲状态:显示当前版本号,展示「检查更新」按钮
- 检查中:显示加载动画,按钮置灰
- 有更新:显示新版本信息、更新日志、下载按钮
- 下载中:显示进度条、下载速度、暂停/取消按钮
- 下载完成:显示「立即安装」按钮,或定时安装选项
- 安装中:显示安装进度,或直接跳转Recovery
我在项目中遇到过一个问题:用户点击「检查更新」后,网络请求超时了,UI一直卡在「检查中」状态。后来我加了个超时机制——15秒没响应就自动切回空闲状态,并提示「网络异常,请稍后重试」。
嗯,这里要注意:UI的刷新频率不能太高。下载进度条如果每秒刷新几十次,CPU占用会飙升。我一般控制在每秒刷新2-3次,用户体验完全够用。
下载管理器
Android系统自带的 DownloadManager 是个好东西。它是个系统级服务,支持断点续传、网络切换自动恢复、后台下载等特性。
说白了,你不需要自己写下载逻辑。直接用 DownloadManager.Request 把下载任务丢给它就行。
// 创建下载请求
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(otaUrl));
request.setTitle("系统更新");
request.setDescription("正在下载系统更新包...");
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "update.zip");
request.setAllowedOverMetered(false); // 非计量网络才下载
request.setAllowedOverRoaming(false); // 漫游时不下载
// 获取DownloadManager服务
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
long downloadId = dm.enqueue(request);
这段代码看起来简单,但有几个坑我得提醒你:
我曾经踩过的坑:
- 存储权限:Android 10以上,直接写公共目录需要权限。我建议用
getExternalFilesDir或者应用私有目录 - 网络监听:DownloadManager在网络恢复后会自动重试,但重试次数有限。我遇到过WiFi断连后重试3次失败就放弃了,用户完全不知情
- 下载ID持久化:下载ID要存到SharedPreferences里。App重启后,通过ID查询下载状态,否则用户不知道下载到哪了
小技巧:下载完成后,记得校验文件的MD5或SHA256。我见过下载包损坏导致升级失败的情况。校验通过再触发安装,能省去很多售后麻烦。
安装调度
下载完成后,就该安装调度上场了。这里分两种情况:
| 场景 | 实现方式 | 特点 |
|---|---|---|
| 非静默升级 | 调用 RecoverySystem.installPackage() |
用户可见,需要确认,重启进入Recovery |
| 静默升级 | 写入 /cache/recovery/ 目录,触发重启 |
用户无感知,适合系统级App或厂商定制 |
非静默升级的代码大概长这样:
// 非静默升级
File updateFile = new File(updatePath);
RecoverySystem.installPackage(context, updateFile);
// 静默升级(需要系统权限)
File commandFile = new File("/cache/recovery/command");
FileWriter writer = new FileWriter(commandFile);
writer.write("--update_package=" + updatePath + "\n");
writer.write("--wipe_cache\n");
writer.close();
// 然后重启系统
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
pm.reboot("recovery");
你想想看,为什么要有两种方式?
嗯,原因很简单:用户体验不同。非静默升级让用户有掌控感,知道手机在干嘛。静默升级适合厂商推送小补丁,用户睡一觉起来手机就更新好了。
我个人建议:除非你有十足的把握,否则别用静默升级。我曾经见过一个案例,静默升级过程中用户正在打电话,结果手机突然重启了……你懂的,后果很严重。
安装调度的核心逻辑
安装调度不是简单的「下载完就装」。它要考虑几个因素:
- 电量:低于30%不安装,防止升级过程中断电变砖
- 存储空间:安装包解压需要额外空间,至少预留1.5倍包体大小
- 用户使用状态:正在通话、看视频、玩游戏时,延迟安装
- 定时安装:允许用户选择凌晨2点安装,不打扰使用
安装调度决策树(简化版):
- 检查电量是否充足 → 否:提示充电
- 检查存储空间 → 否:提示清理
- 检查用户是否活跃 → 是:延迟安装
- 检查是否设置了定时安装 → 是:等待定时时间
- 触发安装流程 → 重启进入Recovery
这个决策树看起来简单,但每个节点都有分支。比如「用户是否活跃」怎么判断?我一般用 UsageStatsManager 查询最近5分钟是否有前台App运行。如果有,就再等5分钟。
说白了,安装调度的核心就一句话:在保证安全的前提下,找个最合适的时间把活干了。
好,关于客户端升级流程,咱们就聊到这儿。从UI到下载再到安装,每一步都有它的门道。你动手写代码时,多想想边界情况,能省去不少线上故障。