Android ShardPreference简单的源码分析以及存在的问题(源码分析1)

本文档属于学习时总结,如有大佬发现问题请一定评论纠错改正

Android ShardPreference源码分析

ShardPreference源码分析

    • Android ShardPreference源码分析
      • 1.序
      • 2.ShardPreference读取时的使用与源码分析
      • 3.ShardPreference写入时的使用与源码分析
      • 4.总结
      • 5.一些其他的话

1.序

在Android的使用以及学习中,轻量化的持久存储是绝对必不可少的,同时ShardPreference也是很多人从刚开始用到现在的存储类,可以说是老少都用过的东西,那么这个东西真的就没有问题?

答案是否定的,因为谷歌甚至都想舍弃这个东西,从而在Jetpack全家桶里面发行了DataStore,当然我知道很多人都在使用MMKV,当然了,这里我也推荐使用MMKV或者DataStore,因为这两者是真的好用。

回到正题,要知道SP存在的问题,那么就要去阅读SP的源码,让我们从源码一步步分析

2.ShardPreference读取时的使用与源码分析

  1. 日常使用SP
SharedPreferences preferences = getSharedPreferences("hello",MODE_PRIVATE);

上面的代码就是我们日常使用的SP最常用的方式了,首先我们不使用任何封装,那么这就是最简单的获取SP示例的方式,然后我们点击 Context.getSharedPreferences() 进一步了解源码。

 @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return mBase.getSharedPreferences(name, mode);
    }
  1. 查看源码

我们发现是mBase调用的,那么这个mBase是一个什么东西呢,是一个context对象,因为我们看到的源码其实是ContextWrapper这个包装类,所以我们应该继续点击get方法,结果跑到了一个抽象方法,然后不知所措? 好了源码阅读到此结束 哈哈开个玩笑,我们知道SP其实是一个接口,那么接口肯定就有其实现类,由于接口以及实现类的命名规范我们不难找到 SharedPreferencesImpl (注意,新版SDK可能没有此方法了,查看源码的版本是SDK30,本类的所有方法被打上了灰名单),不过不影响我们看源码,实现貌似写进了ContextImpl里,阅读源码总是先看静态方法,没有则看我们调用的方法,那么问题就来咯,构造函数如下:

 SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
        //此时调用了一下方法,通过名字我们不难看出,
        //就是加载硬盘的文件,那么这个File其实就是我们的SP存的XML
    }
     private void startLoadFromDisk() {
        synchronized (mLock) {//此时同步锁住了变量判断是否加载了
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    //下面就是加载的过程 其实很简单 就是把XML中数据都加载到Map里面 但是XML文件因为标签的原因
    //所以有很多冗余的东西
      private void loadFromDisk() {
      ...
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
            ...

经过简单的源码分析我们得出,SP在初始化的时候就是去加载文件到一个Map对象中,并且是异步的,理论上是没有什么问题的,那么接下来的操作如果使用不当,则会出问题。

  1. 场景以及使用不当的操作

如果我们要应用一个场景,那就是在Splash去判断,如果用户是第一次打开,则展示隐私条款,如果不是第一次打开则展示广告,那么代码可能是这样的

 SharedPreferences preferences = getSharedPreferences("hello",MODE_PRIVATE);
 boolean isFirst = preferences.getBoolean("isFirst",false);
 if(isFirst){}else{}

代码没问题,单独这么写也没问题,那么我们点击进去看一下源码,你会发现:

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

我们可以看到,有一个等待方法,如果Map初始化没有完成,那么将阻塞所有流程

while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }

那么,你可能会说,哎呦不就是一个几字节的文件哪,加载很快的啦?
这个确实如此,但是很多人非常喜欢自己封装一个SPManager等相关的操作类,如果不区分存下的xml文件的话,那么一个xml文件的量级根本是不可控的,记得之前有人在SP里面存Base64图片,导致程序Splash直接阻塞一两秒,并且对于Splash这种最优先展示给用户的界面,这一两秒可能是致命的,所以如果只想使用SP的话,建议开机load文件要控制住!

3.ShardPreference写入时的使用与源码分析

  1. 使用SP存储数据,简单代码
 SharedPreferences preferences = getSharedPreferences("hello",MODE_PRIVATE);
        preferences.edit().putBoolean("isFirst",true).apply();
  1. Editor源码分析

用过的人都知道,edit()方法会直接new一个对象

  @Override
    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

代码如上,其实问题更大了,如果我写在一个Manager里面的话,直接搞一个静态变量不就得了,那么会直接导致上面的加载时间长,甚至ANR,当然你也可以区分xml之类的,这里就不多说了,如果随用随创建,那么使用不当可能会频繁gc,内存抖动,然后内存碎片导致OOM。

设置数据这一块没有什么可讲的,就是set进一个map等待下面的提交。

  1. 提交

首先结论:无论是commit或者是apply,都有可能造成卡顿甚至ANR。

都知道Commit是同步代码提交,那么它会返回一个成功或者失败的Boolean变量,内部逻辑呢,就是取出map然后比较两个map的差异然后写入,这里就不多介绍,既然是同步那就一定会有一定的延迟,可能是忽略不计的,同步大家都懂,同时代码逻辑也简单一些。
那么异步为什么也会这样呢,接下来我们看apply的方法源码:

  @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }

通过代码我们清楚的看到,apply方法只是帮我们封装了一下,变成了一个runnable任务,然后交给QueuedWork去提交,代码如下
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
那么熟悉AMS的小伙伴可能就发现问题了,在我们Android的生命周期里面有onPause()这个方法,那么在ActivityThread里面,我们可以找到调用它的方法,下面是代码:

 @Override
    public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (r != null) {
            if (userLeaving) {
                performUserLeavingActivity(r);
            }

            r.activity.mConfigChangeFlags |= configChanges;
            performPauseActivity(r, finished, reason, pendingActions);
            if (r.isPreHoneycomb()) {
                QueuedWork.waitToFinish();
            }
            mSomeActivitiesChanged = true;
        }
    }

哇哦,相信大家一眼就定位到了问题所在,那就是 QueuedWork.waitToFinish();,那么我们经过一个小案例来演示一下
假如你需要将用户头像缓存起来,只用SP的话,你在用户选择完头像,在你的处理Activity里调用apply的话,然后马上跳到另一个activity
那么此时onPause()会阻塞(这里夸张了一下,大家知道什么意思就行),导致你的activity跳转可能会出现一定的延迟,并且我们知道SP的更新是全量文件覆盖,那么如果xml文件本来就很臃肿,那么会出大问题甚至ANR。

4.总结

  1. 如果优先级非常高的SP变量,建议单独放在一个文件里
  2. 使用其他人/自己编写的工具类请注意是否都杂糅在一个xml文件里
  3. Editor不要经常创建,请保持一个xml文件对应一个Editor
  4. 不要在SP里存很臃肿的文件,列如加密的二进制流,base64流等等

5.一些其他的话

本文章虽然是时间比较新,但是我发现SDK31以后ShardPreferenceImpl确实消失了,并且内容开始出现了DataStore,估计是谷歌要更换掉SP的底层架构,在原有API调用不变的方式下换个内脏,也有可能直接在getShardPreference()方法打上废弃标签,大家拭目以待。
在这里还是推荐大家使用MMKV或者DataStore。

接下来会发一下MMKV的源码分析以及SDK31以后的SP底层到底发生了什么变化,喜欢可以关注一下哦,大家一起进步~!

你可能感兴趣的:(Android相关源码分析,android,xml,java,android,jetpack)