22、OTA升级与stash:stash机制、stash空间优化

聊到OTA升级,很多人第一反应是「差分算法」或者「AB分区」。但真正让我在项目里熬夜排查的,往往是另一个不起眼的东西——stash。

说白了,stash就是升级过程中的「临时避难所」。你想想看,系统正在往分区B写新数据,但分区A的旧数据还没完全读完,这时候新数据要覆盖旧数据的物理块了,怎么办?总不能让旧数据凭空消失吧。

嗯,stash就是干这个的。它把即将被覆盖的旧数据块,先暂存到另一个地方。等升级脚本执行到需要读取这些旧数据时,再从stash里取出来。

stash机制:为什么需要它?

我刚开始接触OTA时,觉得stash是个累赘。明明有完整的升级包,为什么还要保留旧数据?后来在一次升级失败的回滚场景中,我才真正理解它的价值。

OTA升级的核心是「增量更新」。我们只下发新旧版本的差异数据,而不是整个镜像。这意味着升级过程中,系统需要同时依赖「旧数据」和「差分数据」来合成「新数据」。

举个例子:

  • 旧数据块A1、A2、A3
  • 新数据块B1、B2、B3
  • 差分数据只记录了A1→B1、A2→B2、A3→B3的变化

如果升级脚本先处理A1,把它改写成B1。但后续某个操作需要读取A2的原始内容,而A2所在的物理块已经被B2覆盖了——这时候stash就派上用场了。

核心逻辑:stash保证了「读旧写新」的原子性。没有stash,增量升级就是空中楼阁。

我在项目中遇到过一种情况:某个定制ROM的升级脚本写得比较粗糙,stash操作频繁触发,导致升级时间从3分钟暴涨到15分钟。用户反馈说「升级像死机了一样」——其实不是死机,是stash在疯狂读写。

stash空间:藏在哪?有多大?

stash的数据存在哪?不是内存,是磁盘。具体来说,是/cache分区或者/data分区下的一个临时目录。

Android系统在升级前会预留一块空间给stash。这块空间的大小,直接决定了升级的成败。

分区类型 默认stash路径 空间限制
/cache /cache/recovery/stash 通常50MB~200MB
/data /data/recovery/stash 取决于剩余空间

为什么会有两个路径?我记得在Android 8.0之前,stash主要放在/cache。但后来/cache分区越做越小,很多设备只有几十MB。一旦升级包较大,stash空间就不够用了。

于是Google在Android 8.0之后引入了/data分区的stash。如果/cache空间不足,系统会自动切换到/data

注意:如果/data分区被加密(FBE/FDE),stash操作会变得非常慢。因为每次读写都要加解密。我曾经在一个加密设备上测试,stash写入速度只有2MB/s,简直噩梦。

stash空间优化:怎么省?

stash空间不是越大越好。它占用的是宝贵的存储资源,而且会影响升级速度。我个人习惯从三个维度来优化:

1. 减少stash触发次数

升级脚本中的block_image_update命令,每次执行都可能触发stash。优化思路是:

  • 合并连续的块操作,减少脚本中的独立命令数
  • 使用--use_first参数,让系统优先使用已有的stash数据,而不是重复创建
# 优化前:多次触发stash
block_image_update("/dev/block/bootdevice/by-name/system", mapfile1);
block_image_update("/dev/block/bootdevice/by-name/system", mapfile2);

# 优化后:合并为一次操作
block_image_update("/dev/block/bootdevice/by-name/system", mapfile_merged);

2. 控制stash生命周期

stash数据不是永久保留的。系统会在以下时机清理stash:

  • 升级成功完成后
  • 升级失败回滚时
  • stash空间使用超过阈值时

我曾经在调试日志里看到过这样的信息:

I:Stash size 180MB, free space 200MB, threshold 90%
I:Stash cleanup triggered, removing stale entries...

这说明stash空间快满了,系统在自动清理。如果频繁触发清理,说明stash空间设置得太小,或者升级脚本设计不合理。

3. 合理设置stash大小

recovery.fstab中,可以指定stash的最大空间:

/cache     ext4    /dev/block/mmcblk0p5   flags=stash_max_size=104857600

这个值怎么定?我一般按以下公式估算:

  • stash大小 ≈ 最大单个文件操作涉及的块数 × 块大小 × 1.2(安全系数)
  • 对于系统分区(约2GB),stash建议设置在100MB~200MB
  • 对于vendor分区(约500MB),stash建议设置在50MB~100MB

小技巧:如果设备存储空间紧张,可以尝试将stash路径指向/data分区,并配合stash_max_size限制。但要注意/data分区的加密性能问题。

stash的避坑指南

做OTA升级这几年,我在stash上踩过的坑不少。挑几个典型的说说:

坑一:stash空间不足导致升级失败

我曾经在一台低端设备上遇到升级到一半卡死的情况。查日志发现是stash空间写满了,系统无法继续。后来发现是/cache分区只有30MB,而升级包需要80MB的stash空间。解决方案是把stash路径改到/data

坑二:stash数据损坏导致回滚失败

有一次升级过程中意外断电,重启后系统尝试回滚。但stash里的数据因为断电不完整,回滚也失败了。最后只能进入fastboot模式刷机。从那以后,我建议在升级脚本中加入stash校验机制。

坑三:stash性能瓶颈

在eMMC 5.0的设备上,stash的读写速度还能接受。但换成NAND Flash后,stash操作慢得让人抓狂。我当时的优化方案是:减少stash触发次数,尽量在内存中完成数据暂存。

stash的未来:动态调整

Android 12之后,Google引入了动态stash管理。系统会根据当前升级进度和剩余空间,动态调整stash的大小和清理策略。说白了,就是不再需要开发者手动设置stash_max_size了。

但说实话,动态管理也不是万能的。我在测试中发现,某些场景下动态stash的预测算法不够准确,导致空间浪费或者不足。所以对于定制ROM来说,手动优化stash仍然是一个必要技能。

总结一下:stash是OTA升级的「隐形守护者」。它不显眼,但缺了它不行。优化stash的核心思路就三个字——少、快、准。少触发、快读写、准控制。

stash机制与优化核心逻辑 stash机制 为什么需要stash? stash空间在哪? 如何优化stash? 增量更新需要保留旧数据 保证「读旧写新」原子性 支持升级失败回滚 /cache/recovery/stash /data/recovery/stash 空间限制:50MB~200MB 减少stash触发次数 控制stash生命周期 合理设置stash大小

嗯,stash这部分内容就讲到这里。它不像差分算法那样炫酷,也不像AB分区那样架构清晰。但恰恰是这些「不起眼」的细节,决定了OTA升级的稳定性和用户体验。

公众号:蓝海资料掘金营,微信deep3321