Android SharedPreferences可空Int存储读取方案探讨及KMM跨平台适配需求
嘿,我来帮你梳理下这个问题~你现在要在KMM核心模块里实现一个无平台依赖的SettingsStorage接口,其中包含可空Int类型的timerDuration,Android端用SharedPreferences实现,还纠结现有两个方案的弊端,对吧?
先分析下你现有方案的问题
方案一:值为null时移除对应键
这个思路的问题确实在于SharedPreferences的异步特性:如果用apply()(KTX的edit { ... }默认是异步),频繁快速调用set(null)再set(某个值),可能会因为异步移除操作还没完成,导致后续的put操作被覆盖,出现数据不一致的情况。另外,contains(key)的判断也不是绝对原子的,和异步编辑的操作可能存在竞态,导致读取到错误的结果。
方案二:统一用字符串存储
其实你担心的.toIntOrNull()开销完全是多虑的——对于Int来说,字符串转换是非常轻量的操作,几乎不会影响性能。但这个方案的问题在于:存储类型是非原生的Int,Android端如果之前有存量的Int数据会不兼容;iOS端UserDefaults存Int和String是不同的类型,跨平台调试或者和原生代码交互时会有点别扭。
更优方案:用“标记值映射null”
其实你完全不需要重复造轮子,这个场景下最稳妥高效的方式是选一个业务中永远不会用到的Int值作为null的“替身”,在核心模块里统一处理这个映射,平台端只需要实现原生的Int读写逻辑即可。
核心模块的统一逻辑
先在核心模块里定义一个抽象基类,把null和标记值的转换逻辑封装起来,避免平台端重复编写:
interface SettingsStorage { var timerDuration: Int? // 其他设置项... } abstract class BaseSettingsStorage : SettingsStorage { // 选一个业务中绝对不会用到的Int值作为null标记,比如Int最小值 private val NULL_INT_MARKER = Int.MIN_VALUE override var timerDuration: Int? get() { val rawValue = getRawInt(TIMER_DURATION, NULL_INT_MARKER) return if (rawValue == NULL_INT_MARKER) null else rawValue } set(value) { val rawValue = value ?: NULL_INT_MARKER putRawInt(TIMER_DURATION, rawValue) } // 让平台实现原生的Int读写方法 protected abstract fun getRawInt(key: String, defaultValue: Int): Int protected abstract fun putRawInt(key: String, value: Int) companion object { const val TIMER_DURATION = "timer_duration" } }
Android端实现(基于SharedPreferences)
Android端只需要简单实现原生的Int读写,完全不用处理null逻辑:
class AndroidSettingsStorage(private val prefs: SharedPreferences) : BaseSettingsStorage() { override fun getRawInt(key: String, defaultValue: Int): Int { return prefs.getInt(key, defaultValue) } override fun putRawInt(key: String, value: Int) { prefs.edit { putInt(key, value) } } }
iOS端实现(基于UserDefaults)
iOS端同样只需要处理原生的Int存储:
import platform.Foundation.NSUserDefaults class IosSettingsStorage : BaseSettingsStorage() { private val defaults = NSUserDefaults.standardUserDefaults override fun getRawInt(key: String, defaultValue: Int): Int { return defaults.integerForKey(key) ?: defaultValue } override fun putRawInt(key: String, value: Int) { defaults.setInteger(value, key) defaults.synchronize() } }
这个方案的优势
- 性能最优:完全使用平台原生的Int存储,没有类型转换开销,也不需要移除键的操作
- 避免异步竞态:每次都是直接put值,即使是异步编辑,最后一次操作的结果会覆盖前面的,数据一致性有保障
- 逻辑统一:核心模块已经处理了null和标记值的映射,平台端只需要实现最基础的存储逻辑,不用重复编写默认值或null处理代码
- 兼容性好:Android端用原生的Int存储,和现有SharedPreferences数据兼容;iOS端用UserDefaults的原生Int类型,和原生代码交互也没问题
注意点
选择标记值的时候,一定要确保这个值永远不会出现在你的业务逻辑中。比如如果你的timerDuration是正数,那用Int.MIN_VALUE或者任意负数都可以;如果业务中可能用到所有Int值,那可以考虑用Long类型来存储(用Long的最小值作为标记),不过这种情况非常少见。
内容的提问来源于stack exchange,提问作者m.reiter

