序列化与数据存储
# 17.序列化与数据存储
# 目录介绍
- 01.为什么需要序列化与数据存储
- 1.1 序列化的核心需求
- 1.2 数据存储的层级
- 02.Serializable序列化原理
- 2.1 Serializable的工作机制
- 2.2 serialVersionUID的作用
- 2.3 Serializable的性能问题
- 03.Parcelable序列化原理
- 3.1 Parcelable的设计理念
- 3.2 Parcel的底层实现
- 3.3 Parcelable为什么快
- 04.Serializable与Parcelable对比
- 4.1 详细对比
- 4.2 选择建议
- 05.JSON序列化原理
- 5.1 JSON解析器的工作原理
- 5.2 Gson的反射机制
- 5.3 Moshi的代码生成优化
- 06.ProtoBuf序列化原理
- 6.1 ProtoBuf的编码原理
- 6.2 ProtoBuf vs JSON对比
- 07.SharedPreferences原理详解
- 7.1 SP的文件格式
- 7.2 SP的加载过程
- 7.3 SP的写入过程
- 7.4 全量写入机制
- 08.SharedPreferences的问题与陷阱
- 8.1 ANR风险
- 8.2 多进程问题
- 8.3 大文件性能问题
- 09.MMKV高性能存储原理
- 9.1 MMKV的核心设计
- 9.2 mmap内存映射原理
- 9.3 MMKV的增量追加写入
- 9.4 MMKV的多进程支持
- 10.DataStore原理与设计
- 10.1 DataStore的设计目标
- 10.2 DataStore的核心实现
- 10.3 DataStore vs SP vs MMKV
- 11.SQLite数据库原理
- 11.1 SQLite在Android中的角色
- 11.2 SQLite的WAL模式
- 11.3 SQLite性能优化
- 12.Room数据库框架原理
- 12.1 Room的架构
- 12.2 Room的编译时代码生成
- 12.3 Room的编译时SQL验证
- 13.ContentProvider与数据共享
- 13.1 ContentProvider的数据共享模型
- 13.2 ContentProvider的线程安全
- 14.文件存储与IO优化
- 14.1 Android文件存储位置
- 14.2 IO优化策略
- 14.3 StrictMode检测IO问题
- 15.数据存储方案选型指南
- 15.1 选型决策树
- 15.2 性能基准对比
- 15.3 迁移建议
- 16.总结与技术思考
- 16.1 核心要点回顾
- 16.2 面试高频问题
# 01.为什么需要序列化与数据存储
# 1.1 序列化的核心需求
序列化是将内存中的对象转换为可存储或可传输格式的过程,反序列化则是逆过程。在Android开发中,序列化出现在多个场景:
序列化的使用场景:
├── 进程间通信(IPC)
│ ├── Activity之间通过Intent传递对象
│ ├── AIDL跨进程传递数据
│ └── Binder通信
├── 数据持久化
│ ├── 保存对象到文件
│ ├── 存储到数据库
│ └── 网络传输
├── 状态保存
│ ├── onSaveInstanceState保存Activity状态
│ ├── Fragment参数传递
│ └── ViewModel的SavedStateHandle
└── 网络通信
├── HTTP请求/响应体序列化
└── WebSocket数据序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.2 数据存储的层级
Android数据存储方案层级:
轻量级KV存储:
├── SharedPreferences (传统方案)
├── MMKV (高性能替代)
└── DataStore (Jetpack推荐)
结构化存储:
├── SQLite (底层数据库)
├── Room (ORM框架)
└── Realm (第三方ORM)
文件存储:
├── 内部存储 (/data/data/包名/)
├── 外部存储 (/sdcard/Android/data/包名/)
└── 缓存目录 (getCacheDir())
跨进程共享:
├── ContentProvider
├── MMKV多进程模式
└── 文件+文件锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 02.Serializable序列化原理
# 2.1 Serializable的工作机制
Serializable是Java标准的序列化接口:
// 使用Serializable
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // transient字段不参与序列化
}
1
2
3
4
5
6
7
2
3
4
5
6
7
序列化过程的底层实现:
// ObjectOutputStream的序列化流程
public final void writeObject(Object obj) {
// 1.检查是否实现了Serializable
if (!(obj instanceof Serializable)) {
throw new NotSerializableException();
}
// 2.检查是否有自定义writeObject方法
// 如果有,调用自定义方法(如HashMap自定义了序列化逻辑)
// 3.使用反射获取所有非static、非transient字段
ObjectStreamClass desc = ObjectStreamClass.lookup(obj.getClass());
ObjectStreamField[] fields = desc.getFields();
// 4.写入类描述信息(类名、serialVersionUID、字段类型等)
writeClassDesc(desc);
// 5.逐个写入字段值
for (ObjectStreamField field : fields) {
Object value = field.getField().get(obj);
writeObject0(value); // 递归序列化引用类型字段
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.2 serialVersionUID的作用
serialVersionUID的工作机制:
序列化时:
User对象 → 字节流 [serialVersionUID=1, name="张三", age=25]
反序列化时:
1. 读取字节流中的serialVersionUID (=1)
2. 读取当前类定义的serialVersionUID
3. 比较两者是否一致
├── 一致 → 正常反序列化
└── 不一致 → 抛出InvalidClassException
为什么需要?
如果不显式定义serialVersionUID:
└── 编译器会根据类结构自动计算一个值
└── 修改了类(新增字段、修改方法签名)后值会变
└── 导致旧数据无法反序列化
最佳实践:
└── 始终显式定义serialVersionUID
└── 只在不兼容的结构变更时才修改它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2.3 Serializable的性能问题
Serializable的性能开销来源:
1. 反射开销
└── 每次序列化都通过反射获取字段信息
└── 反射调用比直接调用慢10-100倍
2. 临时对象创建
└── ObjectOutputStream内部创建大量临时对象
└── 触发频繁GC
3. IO流开销
└── 使用Stream方式逐个写入字段
└── 每个字段可能触发一次IO操作
4. 类描述冗余
└── 每个对象都写入完整的类描述信息
└── 包括类名、字段名、字段类型等
└── 增加了数据大小
基准测试(以1000次序列化/反序列化为例):
Serializable:约250ms
Parcelable:约5ms
差距约50倍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 03.Parcelable序列化原理
# 3.1 Parcelable的设计理念
Parcelable是Android专为IPC设计的高效序列化方案:
// Parcelable的使用
class User implements Parcelable {
private String name;
private int age;
// 写入:将对象的每个字段按顺序写入Parcel
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
}
// 读取:按照写入的相同顺序读取
protected User(Parcel in) {
name = in.readString();
age = in.readInt();
}
// 工厂方法
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];
}
};
@Override
public int describeContents() {
return 0; // 如果含文件描述符则返回CONTENTS_FILE_DESCRIPTOR
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 3.2 Parcel的底层实现
Parcel是Parcelable的数据载体,底层是一块连续的内存缓冲区:
Parcel的内存结构:
┌──────────────────────────────────────┐
│ Parcel Buffer │
│ ┌─────────────────────────────────┐ │
│ │ mData (连续内存块) │ │
│ │ ┌─────┬─────┬─────┬─────┬────┐ │ │
│ │ │type │len │data │type │... │ │ │
│ │ │(4B) │(4B) │(NB) │(4B) │ │ │ │
│ │ └─────┴─────┴─────┴─────┴────┘ │ │
│ │ ↑ │ │
│ │ mDataPos (当前读写位置) │ │
│ │ ↑ │ │
│ │ mDataSize │ │
│ └─────────────────────────────────┘ │
│ │
│ mDataCapacity (已分配容量) │
│ mFds[] (文件描述符数组) │
└──────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Parcel.cpp (Native实现)
status_t Parcel::writeInt32(int32_t val) {
// 直接内存写入,无反射开销
return writeAligned(val);
}
template<class T>
status_t Parcel::writeAligned(T val) {
// 检查空间是否足够
if ((mDataPos + sizeof(val)) <= mDataCapacity) {
restart_write:
// 直接写入内存(指针操作)
*reinterpret_cast<T*>(mData + mDataPos) = val;
return finishWrite(sizeof(val));
}
// 空间不足,扩容
status_t err = growData(sizeof(val));
if (err == NO_ERROR) goto restart_write;
return err;
}
status_t Parcel::writeString16(const String16& str) {
// 写入字符串长度
writeInt32(str.size());
// 写入字符串内容(UTF-16编码)
memcpy(mData + mDataPos, str.string(), str.size() * sizeof(char16_t));
return finishWrite(str.size() * sizeof(char16_t));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 3.3 Parcelable为什么快
Parcelable快的原因:
1. 无反射
└── 开发者手写writeToParcel和createFromParcel
└── 直接调用Parcel的read/write方法
└── 避免了反射的开销
2. 内存操作
└── Parcel底层是连续内存块
└── read/write本质是指针移动+内存拷贝
└── 没有IO流的包装开销
3. 无类描述信息
└── 不需要写入类名、字段名等元信息
└── 数据更紧凑
4. 无临时对象
└── 不需要创建ObjectStreamClass等辅助对象
└── 减少GC压力
5. 专为Binder优化
└── Parcel可以直接传递文件描述符
└── 支持Active Object(IBinder)传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 04.Serializable与Parcelable对比
# 4.1 详细对比
Serializable vs Parcelable 全面对比:
维度 Serializable Parcelable
────────────────────────────────────────────────────
实现复杂度 简单(实现接口即可) 较复杂(手写read/write)
性能 慢(反射+IO流) 快(直接内存操作)
数据大小 大(含类描述信息) 小(只有数据)
适用场景 持久化存储/网络传输 Android IPC
跨平台 Java标准,跨平台 Android专用
版本兼容 serialVersionUID 手动维护字段顺序
深拷贝 支持(通过序列化) 支持
文件描述符 不支持 支持
IBinder传递 不支持 支持
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 4.2 选择建议
选择策略:
Android内部传递(Intent/Bundle):
└── 优先使用Parcelable
└── Kotlin可以用@Parcelize注解自动生成
持久化存储:
└── 不推荐Serializable(性能差)
└── 推荐JSON/ProtoBuf等格式
网络传输:
└── JSON(可读性好,兼容性好)
└── ProtoBuf(性能好,数据小)
跨平台数据交换:
└── JSON或ProtoBuf
└── Parcelable不跨平台
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 05.JSON序列化原理
# 5.1 JSON解析器的工作原理
JSON解析的两种模式:
1. DOM模式(如JSONObject)
└── 将整个JSON解析为树形结构
└── 可以随机访问任意节点
└── 内存占用大(整个JSON都在内存中)
└── 适合小JSON
2. 流式模式(如JsonReader/Gson/Moshi)
└── 逐Token读取JSON
└── 内存占用小(不需要全部加载)
└── 只能顺序访问
└── 适合大JSON
解析过程(以Gson为例):
{"name":"张三","age":25}
Token序列:
BEGIN_OBJECT → NAME("name") → STRING("张三")
→ NAME("age") → NUMBER(25) → END_OBJECT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 5.2 Gson的反射机制
// Gson使用反射进行序列化/反序列化
class ReflectiveTypeAdapterFactory {
TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
// 1.获取所有字段
List<BoundField> fields = getBoundFields(gson, type, raw);
// 2.创建TypeAdapter
return new Adapter<T>(constructor, fields) {
@Override
public void write(JsonWriter out, T value) {
out.beginObject();
for (BoundField field : fields) {
out.name(field.name);
// 反射获取字段值
Object fieldValue = field.get(value);
field.typeAdapter.write(out, fieldValue);
}
out.endObject();
}
@Override
public T read(JsonReader in) {
// 反射创建对象
T instance = constructor.construct();
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = fieldMap.get(name);
if (field != null) {
Object value = field.typeAdapter.read(in);
// 反射设置字段值
field.set(instance, value);
}
}
in.endObject();
return instance;
}
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 5.3 Moshi的代码生成优化
Gson vs Moshi vs kotlinx.serialization:
Gson:
├── 基于反射
├── 运行时解析类型信息
└── 性能较差(反射开销)
Moshi:
├── 支持反射和代码生成两种模式
├── 代码生成模式无反射开销
└── Kotlin友好
kotlinx.serialization:
├── 编译时代码生成
├── 无反射
├── Kotlin原生支持
└── 性能最好
性能对比(序列化+反序列化1000次):
Gson(反射): ~120ms
Moshi(反射): ~100ms
Moshi(代码生成): ~60ms
kotlinx.serialization:~40ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 06.ProtoBuf序列化原理
# 6.1 ProtoBuf的编码原理
ProtoBuf(Protocol Buffers)是Google开发的高效二进制序列化格式:
ProtoBuf的编码格式:
message User {
string name = 1; // field_number=1
int32 age = 2; // field_number=2
}
编码后的二进制(TLV格式):
┌─────────────────────────────────────┐
│ Tag(1, LEN) │ Length(6) │ "张三" │
│ 0x0a │ 0x06 │ E5BCA0... │
├─────────────────────────────────────┤
│ Tag(2, VARINT) │ Value(25) │
│ 0x10 │ 0x19 │
└─────────────────────────────────────┘
Tag编码:(field_number << 3) | wire_type
wire_type:
0 = VARINT (int32, int64, bool)
1 = 64-bit (double, fixed64)
2 = LEN (string, bytes, embedded messages)
5 = 32-bit (float, fixed32)
Varint编码(变长整数):
小数字用更少字节表示
25 → 1字节(0x19)
300 → 2字节(0xAC 0x02)
大大减少小整数的空间占用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 6.2 ProtoBuf vs JSON对比
ProtoBuf vs JSON对比:
JSON ProtoBuf
格式 文本(UTF-8) 二进制
可读性 好 差(需要.proto定义)
数据大小 大(含字段名) 小(只有Tag+Value)
解析速度 慢(字符串解析) 快(直接读取二进制)
Schema 无(自描述) 需要.proto文件
跨语言 所有语言 大多数语言
版本兼容 灵活 良好(未知字段保留)
数据大小对比示例:
User(name="张三", age=25)
JSON: {"name":"张三","age":25} → 29字节
ProtoBuf: → 11字节
减小约62%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 07.SharedPreferences原理详解
# 7.1 SP的文件格式
SharedPreferences以XML文件存储在/data/data/包名/shared_prefs/目录下:
<!-- config.xml -->
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="username">张三</string>
<int name="age" value="25" />
<boolean name="isVip" value="true" />
<float name="score" value="99.5" />
<long name="timestamp" value="1234567890" />
<set name="tags">
<string>Android</string>
<string>Java</string>
</set>
</map>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 7.2 SP的加载过程
// SharedPreferencesImpl.java
class SharedPreferencesImpl implements SharedPreferences {
private final File mFile; // XML文件
private Map<String, Object> mMap; // 内存缓存
private boolean mLoaded; // 是否已加载
// 构造时异步加载文件
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mLoaded = false;
startLoadFromDisk();
}
private void startLoadFromDisk() {
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
// 从XML文件读取所有KV对到mMap
Map<String, Object> map = null;
try {
BufferedInputStream str = new BufferedInputStream(
new FileInputStream(mFile));
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
// 读取失败,使用空Map
}
mMap = map != null ? map : new HashMap<>();
mLoaded = true;
mLock.notifyAll(); // 唤醒等待的读操作
}
}
// 读取操作
public String getString(String key, String defValue) {
synchronized (mLock) {
awaitLoadedLocked(); // 等待文件加载完成(可能阻塞!)
String v = (String) mMap.get(key);
return v != null ? v : defValue;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 7.3 SP的写入过程
// SharedPreferencesImpl.java
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
class EditorImpl implements Editor {
private final Map<String, Object> mModified = new HashMap<>();
private boolean mClear;
public Editor putString(String key, String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
// commit:同步写入
public boolean commit() {
// 1.将修改合并到内存Map
MemoryCommitResult mcr = commitToMemory();
// 2.同步写入文件(在当前线程)
enqueueDiskWrite(mcr, null);
// 3.等待写入完成
mcr.writtenToDiskLatch.await();
// 4.通知监听器
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
// apply:异步写入
public void apply() {
// 1.将修改合并到内存Map
MemoryCommitResult mcr = commitToMemory();
// 2.异步写入文件(在工作线程)
// 注意:apply会将写入任务添加到QueuedWork
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
// 3.立即返回,不等待写入完成
notifyListeners(mcr);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 7.4 全量写入机制
SP的一个关键问题是每次写入都是全量写入:
SP的全量写入问题:
假设SP文件有100个KV对,修改其中1个值:
1. 读取全部100个KV对到内存
2. 修改1个值
3. 将全部100个KV对写回文件(包括未修改的99个)
全量写入流程:
1. 创建临时文件 config.xml.bak
2. 将原文件重命名为 config.xml.bak
3. 创建新文件 config.xml
4. 写入全部KV对到新文件
5. 删除备份文件
这意味着:
修改1个值 = 重写整个文件
如果有100个KV对,每个都修改 → 写100次全量文件
这就是SP性能差的根本原因!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 08.SharedPreferences的问题与陷阱
# 8.1 ANR风险
SP导致ANR的三个场景:
场景1:首次加载阻塞
getString() → awaitLoadedLocked() → 等待文件加载
如果文件很大(>100KB),加载可能需要几十ms
在主线程调用就有ANR风险
场景2:apply导致的ActivityThread等待
Activity.onPause() → QueuedWork.waitToFinish()
如果有未完成的apply写入任务
onPause会阻塞等待所有apply完成
大量连续apply → onPause卡顿 → ANR
场景3:commit阻塞主线程
commit()在当前线程同步写文件
文件越大,写入时间越长
主线程commit就是直接阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 8.2 多进程问题
SP不支持多进程的原因:
进程A 进程B
mMap = {key: "value1"} mMap = {key: "value1"}
│ │
├── putString("key","v2") │
├── apply() │
│ 写文件: {key: "v2"} │
│ │
│ ├── getString("key")
│ ├── 返回"value1" (内存缓存!)
│ │ 不是文件中的"v2"
│ │
│ ├── putString("key","v3")
│ └── apply()
│ 写文件: {key: "v3"}
│ 覆盖了进程A的写入!
问题本质:
每个进程有独立的mMap内存缓存
读操作从内存读取,不会重新读文件
写操作独立写文件,互相覆盖
MODE_MULTI_PROCESS标记(已废弃):
每次getSharedPreferences时重新读文件
仍然无法解决并发写入冲突
Android官方已废弃此模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 8.3 大文件性能问题
SP文件大小与性能的关系:
文件大小 加载时间 单次写入时间
─────────────────────────────────
1KB <1ms <1ms
10KB 2-5ms 2-3ms
50KB 10-20ms 10-15ms
100KB 20-50ms 20-30ms
500KB 100-200ms 100-150ms
1MB 200-500ms 200-300ms
建议:
SP文件控制在50KB以内
超过的数据应该使用数据库
按功能拆分多个SP文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 09.MMKV高性能存储原理
# 9.1 MMKV的核心设计
MMKV是腾讯开源的高性能KV存储框架,替代SharedPreferences:
MMKV vs SharedPreferences:
SharedPreferences MMKV
写入方式 全量XML写入 增量追加写入
文件映射 FileInputStream mmap内存映射
多进程 不支持 支持(文件锁)
加密 不支持 支持(AES)
性能 慢(10-100ms) 快(μs级)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 9.2 mmap内存映射原理
mmap的工作原理:
传统文件IO:
应用内存 ─read()→ 内核缓冲区 ─read()→ 磁盘
应用内存 ─write()→ 内核缓冲区 ─write()→ 磁盘
每次读写都要从用户态切换到内核态(系统调用开销)
数据要拷贝两次(用户空间→内核空间→磁盘)
mmap内存映射:
应用内存 ←─映射─→ 内核页缓存 ←─→ 磁盘
┌─────────────┐ ┌─────────────┐ ┌──────┐
│ 用户空间 │ │ 内核空间 │ │ 磁盘 │
│ 虚拟地址A ├────►│ 物理页框 ├────►│ 文件 │
│ (mmap返回) │映射 │ (Page Cache)│同步 │ 数据 │
└─────────────┘ └─────────────┘ └──────┘
读:直接读虚拟地址A → 触发缺页中断 → 从磁盘读入页缓存 → 映射到用户空间
写:直接写虚拟地址A → 修改页缓存 → 操作系统异步刷盘
mmap的优势:
1. 减少数据拷贝(用户空间直接映射页缓存)
2. 减少系统调用(读写如同操作内存)
3. 自动持久化(操作系统在合适时机刷盘)
4. 进程崩溃时内核会确保已写入的数据刷盘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 9.3 MMKV的增量追加写入
MMKV的增量追加写入机制:
传统SP(全量写入):
修改key1 → 重写全部: {key1:v1', key2:v2, key3:v3, ...}
修改key2 → 重写全部: {key1:v1', key2:v2', key3:v3, ...}
MMKV(增量追加):
修改key1 → 追加写入: [...原有数据..., key1:v1']
修改key2 → 追加写入: [...原有数据..., key1:v1', key2:v2']
文件结构:
┌──────┬──────┬──────┬──────┬──────┐
│key1 │key2 │key3 │key1 │key2 │
│:v1 │:v2 │:v3 │:v1' │:v2' │
└──────┴──────┴──────┴──────┴──────┘
旧数据 ↑ 新追加的数据
读取时:
后面的值覆盖前面的值(key1取v1'而不是v1)
空间回收(当文件膨胀到一定大小时):
去除冗余数据,重新整理
将所有最新KV写入新文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 9.4 MMKV的多进程支持
MMKV多进程安全机制:
写入时:
1. 获取文件锁 (flock)
2. 检查文件是否被其他进程修改(通过序列号)
3. 如果被修改,重新从mmap读取最新数据
4. 追加写入数据
5. 更新序列号
6. 释放文件锁
进程A 进程B
flock(LOCK_EX) flock(LOCK_EX) → 阻塞等待
写入key1:v1
更新序列号 1→2
flock(LOCK_UN) ─────────────► flock获得锁
检查序列号: 本地=1, 文件=2
重新读取mmap数据
写入key2:v2
更新序列号 2→3
flock(LOCK_UN)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 10.DataStore原理与设计
# 10.1 DataStore的设计目标
DataStore是Jetpack推出的SP替代方案:
DataStore的两种实现:
1. Preferences DataStore
└── KV存储,类似SP但基于协程
└── 类型安全的Key
└── 基于Flow响应式API
2. Proto DataStore
└── 存储自定义的ProtoBuf对象
└── 完整的类型安全
└── 更强的Schema支持
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 10.2 DataStore的核心实现
// DataStore的关键设计
class DataStoreImpl<T>(
private val file: File,
private val serializer: Serializer<T>,
private val scope: CoroutineScope
) {
// 使用Flow暴露数据
val data: Flow<T> = flow {
// 1.读取文件
val currentData = readFile()
emit(currentData)
// 2.监听后续变化
updateChannel.collect {
emit(it)
}
}
// 使用协程保证线程安全
suspend fun updateData(transform: suspend (t: T) -> T): T {
return withContext(scope.coroutineContext) {
// 单线程执行,避免并发
val currentData = readFile()
val newData = transform(currentData)
writeFile(newData)
updateChannel.emit(newData)
newData
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 10.3 DataStore vs SP vs MMKV
三者对比:
SP MMKV DataStore
线程安全 synchronized mmap+flock 协程单线程
异步API apply(有坑) 同步写入 Flow+协程
多进程 不支持 支持 不支持(需要额外处理)
类型安全 弱 弱 强
性能(写入) 慢(全量写) 快(增量追加) 中等(全量但优化过)
ANR风险 高 低 低
Kotlin友好 一般 一般 好(Flow/协程原生)
学习成本 低 低 中
选型建议:
新项目Kotlin:DataStore(Preferences)
性能敏感/多进程:MMKV
旧项目迁移:MMKV(API接近SP)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 11.SQLite数据库原理
# 11.1 SQLite在Android中的角色
SQLite是Android内置的关系型数据库引擎:
SQLite的特点:
├── 嵌入式:库形式链接到应用中,无需独立进程
├── 零配置:无需安装、管理
├── 单文件:整个数据库存储在一个文件中
├── 事务性:ACID兼容
├── 跨平台:C语言编写,可移植性好
└── 轻量级:库大小约500KB
数据库文件位置:
/data/data/包名/databases/xxx.db
/data/data/包名/databases/xxx.db-journal (回滚日志)
/data/data/包名/databases/xxx.db-wal (WAL日志)
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 11.2 SQLite的WAL模式
SQLite的两种日志模式:
1. Journal模式(默认):
写操作流程:
├── 将原始数据页复制到journal文件(备份)
├── 修改数据库文件中的数据页
├── 提交事务时删除journal文件
└── 异常时从journal恢复
问题:写入时阻塞所有读操作
2. WAL模式(Write-Ahead Logging):
写操作流程:
├── 将修改追加到WAL文件(不修改原数据库)
├── 提交事务时在WAL中标记提交点
└── Checkpoint时将WAL数据合并回主数据库
优势:
├── 读写可以并发执行
├── 写操作更快(顺序追加而非随机写入)
└── 减少磁盘同步次数
Android 9.0+默认使用WAL模式:
db.enableWriteAheadLogging()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 11.3 SQLite性能优化
SQLite性能优化关键点:
1. 使用事务批量操作
单条插入: 1000条 → 约2秒
事务批量: 1000条 → 约50ms(40倍提升)
2. 使用索引
无索引查询: O(N)全表扫描
有索引查询: O(logN) B-Tree查找
3. 使用预编译语句
SQLiteStatement复用 vs 每次编译SQL
4. 使用WAL模式
读写并发,提升整体吞吐
5. 避免在主线程操作
所有数据库操作放在工作线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 12.Room数据库框架原理
# 12.1 Room的架构
Room是Jetpack提供的SQLite抽象层:
Room的三层架构:
应用代码
│
├── @Entity (实体类 → 数据库表)
├── @Dao (数据访问对象 → SQL操作)
└── @Database (数据库类 → 数据库实例)
│
▼
Room编译时处理(APT)
│
├── 生成Dao实现类 (XxxDao_Impl)
├── 生成Database实现类 (XxxDatabase_Impl)
└── 生成SQL语句和类型转换代码
│
▼
SQLite(底层数据库引擎)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 12.2 Room的编译时代码生成
// 开发者编写的Dao接口
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// Room APT自动生成的实现类(简化版)
class UserDao_Impl implements UserDao {
private final RoomDatabase __db;
@Override
public void insert(User user) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
// 自动生成的绑定代码
SQLiteStatement stmt = __insertionAdapterOfUser.acquire();
stmt.bindLong(1, user.getId());
stmt.bindString(2, user.getName());
stmt.bindLong(3, user.getAge());
stmt.executeInsert();
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public User getUserById(long userId) {
final String _sql = "SELECT * FROM users WHERE id = ?";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
_statement.bindLong(1, userId);
Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _cursorIndexOfId = CursorUtil.getColumnIndex(_cursor, "id");
final int _cursorIndexOfName = CursorUtil.getColumnIndex(_cursor, "name");
User _result = null;
if (_cursor.moveToFirst()) {
_result = new User();
_result.setId(_cursor.getLong(_cursorIndexOfId));
_result.setName(_cursor.getString(_cursorIndexOfName));
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 12.3 Room的编译时SQL验证
Room在编译时就会验证SQL的正确性:
Room编译时检查:
1. SQL语法检查
@Query("SELCT * FROM users") → 编译错误!(SELCT拼写错误)
2. 表名/列名检查
@Query("SELECT * FROM user") → 编译错误!(表名是users不是user)
3. 参数绑定检查
@Query("SELECT * FROM users WHERE id = :uid")
fun getUser(userId: Long) → 编译错误!(参数名uid vs userId不匹配)
4. 返回类型检查
@Query("SELECT name FROM users")
fun getNames(): List<User> → 编译警告(查询列不完整)
5. 数据库版本迁移检查
版本1 → 版本2,缺少Migration → 编译错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 13.ContentProvider与数据共享
# 13.1 ContentProvider的数据共享模型
ContentProvider跨进程数据共享:
应用A (数据消费者) 应用B (数据提供者)
┌──────────────┐ ┌──────────────┐
│ContentResolver│ │ContentProvider│
│ .query() │──Binder──►│ .query() │
│ │ │ return Cursor│
│ │◄─────────│ (CursorWindow│
│ │ 共享内存 │ 匿名共享内存)│
└──────────────┘ └──────────────┘
URI格式:
content://authority/path/id
├── authority: com.example.provider (唯一标识)
├── path: users (表/集合路径)
└── id: 123 (具体记录ID,可选)
例如:content://com.example.provider/users/123
表示获取com.example.provider提供的users表中id=123的记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 13.2 ContentProvider的线程安全
ContentProvider的线程模型:
ContentProvider的方法(query/insert/update/delete)
└── 在Binder线程池中被调用
└── 可能被多个线程同时调用
└── 开发者需要自己保证线程安全
保证线程安全的方式:
1. 使用SQLite(SQLite本身支持并发)
2. 使用synchronized
3. 使用ReentrantReadWriteLock
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 14.文件存储与IO优化
# 14.1 Android文件存储位置
Android文件存储位置:
内部存储(应用私有):
├── /data/data/包名/files/ ← getFilesDir()
├── /data/data/包名/cache/ ← getCacheDir()
├── /data/data/包名/shared_prefs/ ← SP文件
├── /data/data/包名/databases/ ← 数据库文件
└── /data/data/包名/no_backup/ ← getNoBackupFilesDir()
外部存储(应用专属,Android 10+ Scoped Storage):
├── /sdcard/Android/data/包名/files/ ← getExternalFilesDir()
└── /sdcard/Android/data/包名/cache/ ← getExternalCacheDir()
公共存储(Android 10+需要MediaStore API):
├── /sdcard/DCIM/ ← 图片/视频
├── /sdcard/Download/ ← 下载文件
└── /sdcard/Documents/ ← 文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 14.2 IO优化策略
Android IO优化关键策略:
1. 使用缓冲IO
✗ FileOutputStream直接写入(每次write触发系统调用)
✓ BufferedOutputStream包装(缓冲区满再写入)
2. 使用mmap大文件
✗ read()逐块读取大文件
✓ MappedByteBuffer映射大文件
3. 避免主线程IO
✓ 所有文件操作在工作线程/协程中执行
✓ StrictMode可以检测主线程IO
4. 合理使用缓存
✓ LruDiskCache缓存网络数据
✓ OkHttp的Cache机制
5. Scoped Storage适配
✓ 使用MediaStore API访问共享文件
✓ 使用SAF (Storage Access Framework)让用户选择文件
6. 压缩存储
✓ 大JSON数据使用gzip压缩后存储
✓ 图片使用合适的压缩格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 14.3 StrictMode检测IO问题
// 开发阶段开启StrictMode检测
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 检测主线程磁盘读
.detectDiskWrites() // 检测主线程磁盘写
.detectNetwork() // 检测主线程网络
.penaltyLog() // 违规时输出日志
.penaltyFlashScreen() // 违规时闪屏(直观)
.build());
}
// 常见违规场景
// ❌ 主线程读SP(SP加载可能阻塞)
// ❌ 主线程写文件
// ❌ 主线程查询数据库
// ❌ 主线程读取Assets大文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 15.数据存储方案选型指南
# 15.1 选型决策树
数据存储方案选型决策树:
需要存储什么?
├── 简单KV配置
│ ├── 需要多进程? → MMKV
│ ├── 新项目Kotlin? → DataStore
│ └── 旧项目/简单需求? → MMKV(或SP)
│
├── 结构化数据(表/关系)
│ ├── 数据量大(>1000条)? → Room/SQLite
│ ├── 需要复杂查询? → Room/SQLite
│ └── 简单对象存储? → MMKV(序列化为JSON/ProtoBuf)
│
├── 文件数据
│ ├── 图片/视频? → 外部存储 + MediaStore
│ ├── 缓存数据? → getCacheDir()
│ └── 配置文件? → getFilesDir()
│
├── 跨应用共享
│ └── ContentProvider + Room
│
└── 网络缓存
└── OkHttp Cache / Room
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 15.2 性能基准对比
各方案性能对比(写入1000次KV操作):
方案 写入时间 读取时间
─────────────────────────────────────────
SP (commit) ~2000ms ~5ms (内存)
SP (apply) ~50ms* ~5ms (内存)
MMKV ~3ms ~1ms
DataStore ~100ms ~5ms
SQLite ~500ms ~50ms
Room ~500ms ~50ms
* SP apply虽然快,但有ANR风险
存储大小对比(存储100个String KV对):
SP (XML): ~5KB
MMKV: ~3KB
ProtoBuf: ~2KB
SQLite: ~8KB (含索引)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 15.3 迁移建议
从SP迁移到其他方案:
迁移到MMKV(推荐,改动最小):
// 一行代码迁移
MMKV kv = MMKV.mmkvWithID("myData");
kv.importFromSharedPreferences(
getSharedPreferences("myData", MODE_PRIVATE));
// 之后使用MMKV API,与SP类似
迁移到DataStore:
// 使用SharedPreferencesMigration
val dataStore = context.createDataStore(
name = "settings",
migrations = listOf(
SharedPreferencesMigration(context, "old_sp_name")
)
)
// 首次访问时自动迁移,迁移后删除旧SP文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 16.总结与技术思考
# 16.1 核心要点回顾
序列化与数据存储核心要点:
序列化:
Parcelable > Serializable (Android IPC)
ProtoBuf > JSON > XML (网络传输/持久化)
编译时代码生成 > 运行时反射
KV存储:
MMKV(mmap+增量写入) > DataStore(协程) > SP(全量写入)
SP的apply有ANR隐患,commit阻塞主线程
数据库:
Room提供编译时SQL验证和类型安全
SQLite WAL模式支持读写并发
批量操作使用事务提升40倍性能
文件IO:
使用缓冲IO、避免主线程IO
大文件使用mmap
Android 10+注意Scoped Storage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 16.2 面试高频问题
问题:Parcelable为什么比Serializable快?
- Parcelable:直接内存操作,无反射,无类描述信息
- Serializable:使用反射遍历字段,写入类元信息,IO流包装开销
问题:SP的apply和commit区别?
- commit:同步写入,返回成功/失败
- apply:异步写入,立即返回
- 陷阱:apply的写入任务在Activity暂停时会被等待,可能导致ANR
问题:MMKV为什么这么快?
- mmap内存映射,避免用户态-内核态切换
- 增量追加写入,不需要全量重写
- 进程崩溃时内核保证数据不丢失
问题:Room相比直接用SQLite的优势?
- 编译时SQL验证,提前发现错误
- 自动生成样板代码,减少手写SQL
- 支持Flow/LiveData响应式查询
- 支持协程挂起函数
- 数据库版本迁移验证
上次更新: 2026/06/10, 11:13:41