第20章:IO与序列化优化——SharedPreferences替代方案与序列化性能对决
说到IO和序列化,很多同学第一反应是「不就是存个数据吗?」。嗯,我以前也这么想。直到有一次线上反馈说App启动卡了3秒,查了半天,罪魁祸首竟然是SharedPreferences在主线程写了一个大JSON。从那以后,我对IO和序列化就再也不敢掉以轻心了。
这一章,我们来聊聊两个核心问题:怎么存数据更快,以及怎么传对象更轻。说白了,就是让你的App在读写数据时少卡顿、少占内存。
20.1 SharedPreferences的痛点与替代方案
SharedPreferences(以下简称SP)是Android最古老的本地存储方案之一。但说实话,它在高并发、大数据量场景下,问题不少。
20.1.1 SP的三大硬伤
- 全量加载: SP一启动就把整个XML文件读到内存里。如果你存了1000个key,哪怕只用1个,也得全加载。
- 主线程阻塞: apply()是异步,但commit()是同步写磁盘。很多人混着用,一不小心就卡了主线程。
- 不支持跨进程: 虽然有个MODE_MULTI_PROCESS,但官方早就标记为废弃了。说白了,就是不好用。
20.1.2 MMKV:腾讯开源的性能怪兽
MMKV是基于mmap内存映射的KV存储。它的核心思想是:把文件映射到内存,读写就像操作内存一样快。
我个人习惯在项目里用MMKV替代SP,尤其是需要频繁读写的小数据。比如用户登录状态、开关配置等。
- 读写速度是SP的几十倍(实测数据见下文)
- 支持多进程同步,自带锁机制
- 增量写入,不会全量加载
接入方式很简单,在build.gradle里加一行依赖:
dependencies {
implementation 'com.tencent:mmkv:1.3.5'
}
初始化也很轻量:
// 在Application.onCreate中初始化
MMKV.initialize(this);
// 获取默认存储实例
MMKV kv = MMKV.defaultMMKV();
// 读写操作
kv.encode("user_name", "张三");
String name = kv.decodeString("user_name", "默认值");
你想想看,同样的代码,换成SP得写多少?而且MMKV还支持加密存储,直接传个密码就行:
MMKV kv = MMKV.mmkvWithID("secure_data", MMKV.MULTI_PROCESS_MODE, "my_password");
20.1.3 DataStore:Jetpack官方方案
DataStore是Google推出的SP替代品。它分两种:Preferences DataStore(类似SP)和Proto DataStore(基于Protocol Buffers)。
DataStore最大的特点是基于Flow的异步操作,天然支持协程。不会阻塞主线程,而且能自动处理数据一致性。
不过,我建议你注意一点:DataStore的写入是事务性的。什么意思?就是要么全部写成功,要么全部回滚。这在某些场景下是好事,但如果你频繁写小数据,性能反而不如MMKV。
- 需要跨进程读写 → 选MMKV
- 需要协程+类型安全 → 选DataStore
- 简单配置、单进程 → MMKV或DataStore都行,别用SP
20.2 Serializable vs Parcelable:序列化的生死对决
序列化,说白了就是把对象变成二进制流,方便存文件或传Intent。Android里有两个选择:Java自带的Serializable,和Android专属的Parcelable。
我记得刚入行时,图省事全用Serializable。直到有一次,一个包含Bitmap的对象序列化后,内存直接飙了200MB。嗯,从那以后我再也不敢乱用了。
20.2.1 Serializable:简单但沉重
Serializable用起来确实简单,实现一个接口就行:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
public String name;
public int age;
}
但它的代价很大:
- 反射机制: 序列化时通过反射读取所有字段,性能差
- 大量临时对象: 序列化过程中会创建很多中间对象,触发GC
- 体积大: 包含类元信息,序列化后的数据比Parcelable大3-5倍
20.2.2 Parcelable:Android专属的轻量方案
Parcelable是Google专门为Android设计的。它需要你手动实现序列化逻辑,但换来的是极致的性能。
public class User implements Parcelable {
public String name;
public int age;
protected User(Parcel in) {
name = in.readString();
age = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
看着代码多了点,但你可以用Android Studio插件自动生成。我个人习惯用插件,省时省力。
| 指标 | Serializable | Parcelable |
|---|---|---|
| 序列化时间(1000次) | ~120ms | ~15ms |
| 反序列化时间(1000次) | ~150ms | ~18ms |
| 序列化后体积 | ~2.5KB | ~0.8KB |
| 内存分配 | 高(频繁GC) | 低(几乎无GC) |
为什么会差这么多?因为Parcelable直接操作内存缓冲区,没有反射,没有中间对象。说白了,就是手工优化 vs 自动挡的区别。
20.3 序列化性能对比与选型指南
除了Serializable和Parcelable,还有几个第三方方案值得关注:
- Gson / Moshi: 基于JSON,可读性好,但性能一般。适合网络传输。
- Protocol Buffers: Google出品,体积小、速度快,但需要定义.proto文件。
- Kryo: Java生态的序列化框架,性能比Serializable好很多,但Android支持有限。
我个人在项目中的选型原则是:
- Intent传数据 → 必须用Parcelable。别问为什么,问就是性能。
- 本地缓存到文件 → 用JSON(Gson/Moshi)。方便调试,可读性强。
- 跨进程通信 → 用MMKV或ProtoBuf。MMKV适合小数据,ProtoBuf适合大数据结构。
List<Bitmap>的对象用Serializable存到文件,结果文件大小超过100MB,App直接OOM。后来改用Parcelable+压缩存储,问题解决。记住:大对象不要用Serializable。
20.4 本章知识体系总览
下面这张图,帮你理清本章的核心脉络:
20.5 实战建议与总结
好了,我们来收个尾。这一章的核心就两句话:
- 别再死磕SharedPreferences了。MMKV和DataStore都更现代、更快。我个人更倾向MMKV,因为它简单粗暴,性能极致。
- 序列化选Parcelable。虽然代码多写几行,但换来的是流畅的用户体验。你想想看,用户滑动列表时突然卡一下,就是因为你在主线程反序列化了一个大对象,值不值?
最后,送你一个我自己的检查清单:
- ✅ 所有Intent传参都用Parcelable
- ✅ 本地配置存储用MMKV
- ✅ 避免在主线程做任何IO操作
- ✅ 大对象序列化前先压缩
- ✅ 定期检查SP文件大小,超过100KB就考虑迁移
嗯,这一章就到这里。记住:IO无小事,序列化要用心。
公众号:蓝海资料掘金营,微信deep3321