编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
      • 事件分发机制
      • Surface渲染原理
      • 自定义View设计
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
      • 内存管理与GC
      • 线程与并发编程
      • 性能优化与监控
      • 序列化与数据存储
        • 组件化与路由设计
        • 插件化与热修复
        • NDK开发实践
        • WebView核心设计
        • ADB常见使用操作
      • 智能硬件

    • iOS开发和进阶

    • Web开发和进阶

    • Linux应用开发

    • Apps
    • Android提升进阶
    • 专栏博客
    杨充
    2026-04-14
    目录

    序列化与数据存储

    # 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

    # 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

    # 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

    序列化过程的底层实现:

    // 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.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 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

    # 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

    # 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
    // 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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
    性能优化与监控
    组件化与路由设计

    ← 性能优化与监控 组件化与路由设计→

    最近更新
    01
    信号崩溃快速排查
    06-15
    02
    CoreDump破案
    06-15
    03
    perf火焰图实战
    06-15
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式