一文详解Android 轻量级存储方案的前世今生

存储方案在Android开发中是一个非常重要的模块, 这里分享一篇大佬的Android 轻量级存储方案的前世今生。希望对大家的学习和工作有所帮助。
原文地址:https://juejin.cn/post/6934494768185475079

背景

对于 Android 轻量级存储方案,有大多数人都很熟悉的 SharedPreferences;也有基于 mmap 的高性能组件 MMKV,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强;还有 Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和流程(Flow)以异步、一致的事务方式存储数据。本文将一一分析这三个方案的来龙去脉,并深入源码进行分析。(本文基于Android 29 源码)

SharedPreferences

SharedPreferences 是 Android 中简单易用的轻量级存储方案,用来保存 App 的相关信息,其本质是一个键值对(key-value)的方式保存数据的 xml 文件,文件路径为 /data/data/应用程序包名/shared_prefs,文件内容如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="pref.device.id">8207e635-bd88-4220-9fc6-59c5e367ad82</string>
    <string name="pref.contact.chat">Let's chat(test)</string>
    <boolean name="pref.user.birthday.modifiable" value="true" />
    <int name="pref.user.birthday.day" value="0" />
    <string name="pref.contact.phone">123-456-8888</string>
    <string name="pref.user.phone"></string>
    <int name="pref.user.birthday.year" value="0" />
    <boolean name="pref.is.login" value="true" />
    <boolean name="pref.first_launch" value="false" />
</map>

每次读取数据时,通过解析 xml 文件,得到指定 key 对应的 value。

SharedPreferences 的设计初衷是轻量级存储,如果我们存储了大量的数据,那会对内存造成什么影响?

源码初步分析

我们先来看看 SharedPreferences 的源码设计,首先从我们的常规调用 Context.getSharedPreferences(name, mode)开始,最终都会调用到

//ContextImpl.java
@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
				···
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) { //用于记录所有的SP文件,文件名为key,file为value
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

再来看看方法 getSharedPreferences 方法

//ContextImpl.getSharedPreferences方法
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
								···
                //SharedPreferences的真正实现类是SharedPreferencesImpl
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
				···
        return sp;
    }

SharedPreferences 是个接口,其真正实现类是 SharedPreferencesImpl

final class SharedPreferencesImpl implements SharedPreferences {
    @UnsupportedAppUsage
    private final File mFile; //对应的xml文件
    private final File mBackupFile;
    private Map<String, Object> mMap; //map中缓存了xml文件中所有键值对
  	···
     @UnsupportedAppUsage
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk(); //开启一个线程加载xml文件内容
    }  
}

每当调用 SharedPreferencesImpl 的构造器的时候,都会开始调用 startLoadFromDisk 方法,然后在该方法中开启一个子线程加载 xml 文件中的内容,最后将 xml 中的内容全部加载到 mMap中

map = (Map<String, Object>) XmlUtils.readMapXml(str);

内存占用

从上面的分析可以看出当 xml 中数据过大时,肯定会导致内存占用过高,虽然 Context.getSharedPreferences(name, mode)调用时会将 xml 中的数据一股脑加载到 mMap 中导致内存占用过大,也就是空间换时间,同时 ContextImpl.getSharedPreferencesCacheLocked

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;   //静态
@GuardedBy("ContextImpl.class")
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {  
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

可以看到这个静态的 sSharedPrefsCache 保存了所有的 sp,然后 sSharedPrefsCache 的 value 值保存了所有键值对,也就是说用过的 sp 永远存在于内存中

同时开发者也需要注意对每个 sp(xml)大小进行控制,毕竟对读写操作也会有一定的影响,具体的区分可以根据相应的业务进行区分。

但是 SharedPreferences 的设计初衷就是面向轻量级的数据存储,所以该设计没毛病,设计者应该自己注意使用场景,毕竟再好的设计也不能面对所有场景

首次调用可能阻塞主线程

我们再来看看 SharedPreferencesImpl.getString() 方法

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
				···
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
				···
    }

可以看到,当 sp 还没加载完毕主线程会一直阻塞在那里,直到加载 sp 的子线程加载完成。对于上面的问题,我们可以提前调用 getSharedPreferences 方法让子线程提前加载 sp 的内容。

防止连续多次edit/commit/apply
        SharedPreferences sp = getSharedPreferences("jackie", MODE_PRIVATE);
        sp.edit().putString("a", "ljc").commit();
        sp.edit().putString("b", "cxy").commit();
        sp.edit().putString("c", "lsm").apply();
        sp.edit().putString("c", "dmn").apply();

每次调用 edit 方法都会创建一个 Editor 对象,造成额外的内存占用。很多设计者会对 SharedPreferences 进行封装,隐藏掉 edit()commit/apply()调用流程,但往往同时也忽略了Editor.commit/apply()的设计理念和使用场景。如果是复杂的场景,用户可以在多次 putXxx 方法之后再统一进行 commit/apply(),也就是一次更新多个键值对,只进行一次 IO 操作。

commit/apply 引起的 ANR 问题

commit 是同步地提交到硬件磁盘,有返回值表明修改是否成功,如果在主线程中提交会阻塞线程,影响后续的操作,可能导致 ANR;而 apply 是将修改数据提交到内存,而后异步真正提交到硬件磁盘,没有返回值。我们着重研究一下 apply 为什么会导致 ANR 问题,先来看看 apply 的源码:

@Override
        public void apply() {
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值