SharedPreferences自动回滚?Android请求序列号重复问题排查
我之前在处理类似的请求序列号逻辑时也碰到过类似的诡异问题,咱们来一步步拆解可能的原因,以及对应的解决办法:
首先明确:SharedPreferences不会自动回滚到历史状态
Android的SharedPreferences本身没有内置的自动回滚机制,它的存储是基于XML文件的持久化操作,正常情况下写入成功后就会保留最新值。所以你遇到的序列号回滚,肯定是其他外部或代码逻辑导致的。
可能的原因分析
1. 多进程场景下的SharedPreferences写入冲突
如果你的应用存在多进程(比如主进程+后台服务进程),而你使用的是默认的SharedPreferences(未配置多进程支持),就很容易出现数据不一致甚至回滚的情况:
- 默认的SharedPreferences是单进程设计的,多进程同时读写时,会因为文件锁的问题导致写入失败,或者旧的进程缓存覆盖新的写入值,最终出现序列号回到旧状态的情况。
- 曾经的
MODE_MULTI_PROCESS方案早已被官方废弃,而且本身实现不稳定,极易引发数据同步问题。
2. 应用数据被备份/还原
很多设备的系统备份(比如Google备份、国内厂商的云备份)或者第三方备份工具,会自动备份应用的SharedPreferences文件。如果用户在某个时间点恢复了旧的备份,就会导致SharedPreferences里的序列号被还原到备份时的数值(比如备份时是50,恢复后就从50开始)。
3. 代码逻辑的竞态条件与fallback风险
你的getNextRequestId()方法存在线程安全问题:
private static String getNextRequestId() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ApplicationEx.getContext()); long sequenceNumber = sharedPreferences.getLong(SEQUENCE_NUMBER, 0); boolean committed = sharedPreferences.edit().putLong(SEQUENCE_NUMBER, sequenceNumber + 1).commit(); if (!committed) { return String.valueOf(System.currentTimeMillis() / 1000); } return String.valueOf(sequenceNumber); }
如果多个线程同时调用这个方法,会出现竞态条件:比如线程A读取到序列号100,还没完成commit;线程B也读取到100,然后线程A先commit成101,线程B接着commit也成101。这会导致两个请求用了同一个序列号,虽然不是直接回滚,但可能和其他问题叠加出现异常。
另外,commit失败后的fallback逻辑用System.currentTimeMillis() / 1000也有风险:如果设备的系统时间被用户手动改回过去,这个时间戳的值可能比之前的序列号小,导致出现“看起来像是回滚”的重复值。
4. 应用数据被意外清理后恢复
比如用户手动清理了应用数据,然后通过某些方式(比如重新登录后同步旧数据)恢复了SharedPreferences的旧值,或者某些清理工具误操作覆盖了SharedPreferences文件。
对应的解决方案
1. 替换为多进程安全的存储方案
如果你的应用涉及多进程,强烈建议用Jetpack DataStore替代SharedPreferences。DataStore是Google推荐的SharedPreferences替代方案,它支持线程安全和多进程安全,基于Kotlin协程和Flow,从根源上避免了SharedPreferences的各种同步问题。
2. 保证单进程下的线程安全
如果是单进程应用,给getNextRequestId()方法加上同步锁,避免多线程竞态:
private static final Object LOCK = new Object(); private static String getNextRequestId() { synchronized (LOCK) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ApplicationEx.getContext()); long sequenceNumber = sharedPreferences.getLong(SEQUENCE_NUMBER, 0); boolean committed = sharedPreferences.edit().putLong(SEQUENCE_NUMBER, sequenceNumber + 1).commit(); if (!committed) { // 改用UUID作为备用,避免时间戳的回退风险 return UUID.randomUUID().toString(); } return String.valueOf(sequenceNumber); } }
3. 优化fallback逻辑
避免使用可能回退的时间戳作为备用序列号,改用UUID或者其他不会重复的生成方式,确保即使写入失败,也不会出现重复或回退的序列号。
4. 控制备份范围
如果不需要备份请求序列号这类数据,可以在AndroidManifest.xml中配置android:allowBackup="false",或者通过backup_rules.xml指定不备份目标SharedPreferences文件,避免备份还原导致的数据回滚。
5. 后端配合校验
在后端也加入序列号的校验逻辑,比如记录每个设备的最大序列号,如果收到的序列号小于等于之前的最大值,就拒绝该请求,或者要求客户端重新生成,从业务层面避免重复请求的问题。
内容的提问来源于stack exchange,提问作者Lahiru Chandima

