关于SharedPreferences一些总结

问题来源

在QQ相互学习(编程5分钟,扯淡两小时)的过程中,有位哥们提出了一个问题,SharedPreferences最多存多少信息,这个度量各位可以理解为多少KB。他这么一问,还真不知道了,话说这个就有点尴尬了,感觉翻阅了一些源码,在查看源码的过程中,理解了一些东西,以前有些很模糊的概念,现在也有了感觉,特此记录一下其中学到的知识。以下SharedPrefences简称sp。

开始扯淡

1、 sp是Android提供一个轻量级数据存储接口,具体的实现类为SharedPreferencesImpl,源码位置在/frameworks/base/core/java/android/app/SharedPreferencesImpl.java下。 其存储格式为xml格式,在7.0源码中,可以最多存储16kB的数据,这个稍后详细说明。

2、SharedPreferencesImpl的构造函数如下:

 SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

其中file在/data/data/your_package_name/shared_prefs/下的存储文件形式,而mode就是该文件在liunx下的可读可写可操作的说明,这里就不展开扯了。对于有人问,为什么SharedPreferencesImpl对应的文件会是上面这个很奇怪的目录,这个很对不起,因为这个在源码里面写死了,不服的话可以查看源码怼一番。

我们看到了SharedPreferencesImpl构造函数中有一个方法startLoadFromDisk(),这个就是去加载xml文件到内存中来的,伪代码如下:

new Thread("SharedPreferencesImpl-load") {
    public void run() {
            loadFromDisk();
        }
    }.start();


private void loadFromDisk() {
    Map map = null;
       try {
            BufferedInputStream str = null;
                try {
                    str = 
                    //这里可以解释为啥我说sp最大可存储16kb的数据了,因为
                    //数据即使过多,BufferedInputStream只会取前16kb的数据
                    
                    new BufferedInputStream(new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (XmlPullParserException | IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }
        
        synchronized (SharedPreferencesImpl.this) {
            notifyAll();
        }    
        

我们可以看到,SpImpl中,读取存储文件直接在子线程中获取,使用子线程在解析耗时的xml时,主线程仍然能流畅运行。只是感觉到惊讶的是,构造函数中直接new一个Thread,有些感觉到意外。

3、 那么问题又来了,刚刚在构造函数中,使用了子线程去加载xml数据,那么会不会有这种情况?我一拿到sp对象,就去getString(“userName”),那样可以获取结果吗?伪代码如下:

    SharedPreferences sharedPreferences = getSharedPreferences("demo",MODE_PRIVATE);
    String userName = sharedPreferences.getString("userName","");

这里的userName会不会获取不到,或者获取为null呢?因为有可能我在xml数据没有被解析加载完成,我就是立马去请求了getString()了? 答案是不会的,这一点sp已经考虑到了,通过查看源码,我们可以看到sp所有的getXXX方法都有一个awaitLoadedLocked()方法。举个例子如下:

public int getInt(String key, int defValue) {
    synchronized (this) {
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

而这个awaitLoadedLocked()方法就是等待xml文件加载完成。其中原理很简答,就是使用了Object的wait()和notifyAll()方法,类似于生产者和消费者模式,只有等待xml文件被加载完成之后,相应的get方法才会被执行,同时我们也看到了sp.getXXX方法都是同步的,因为是异步加载数据,使用同步方法不可避免,但是由于数据只加载一次,所以这个同步方法性能开销应该不是很大。

4、 apply() & commit()方法
如果你在AS上使用commit()方法提交你所有更改的数据,会得到一个提醒:

《关于SharedPreferences一些总结》
通过该提醒,提出了apply()与commit()方法的最大区别,前者方法是异步的,而后者是同步执行的。下面就通过源码来了解一下二者的不同,这也是通常面试中可能会遇到的问题:
二者代码精简如下:

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    awaitCommit.run();
                    QueuedWork.remove(awaitCommit);
                }
            };
            
    //代码省略 
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    

而commit()方法为:

 public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    
    //代码省略
    return mcr.writeToDiskResult;
    

二者最主要的不同是在执行enqueueDiskWrite方法时,apply方法传入了一个Runnable对象,而commit方法直接传入了null值,那我们就去查看一下该方法吧:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //先判断是否为异步传入 
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //这里讲修改的对象写入到文件中
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

    // 如果是同步方法,那么直接writeToDiskRunnable#run方法
    if (isFromSyncCommit) {
         writeToDiskRunnable.run();
         return;
        }
    }
    
    //如果是异步方法,那么使用线程池执行writeToDiskRunnable
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

注释中已经解释了很清楚了,传入null则执行同步方法,非null则执行异步方法,这也印证了你在使用commit()方法时,对主线程产生阻塞的可能了。

基本总结

通过简单源码的探究,我们可以弄清楚一下事实:

  1. sp的最大存储值是16kb(也算是给那哥们的一个合理解释了);
  2. sp文件存储路径为/data/data/package_name/shared_prefs/路径下,这个是默认的没法更改;
  3. sp中getXXX/putXXX方法都是同步的,所以你不用担心多线程环境下sp数据不同步问题,sp是线程安全的;
  4. apply()是通过线程池执行的,而commit()方法时同步执行的,如果你的sp需要存储数据量比较多时,需要考虑使用apply()方法从而避免ANR的情况。
  5. sp中大量使用的锁的概念,所以我们需要明白基础很重要,即使我们认为sp很简单,自己也能实现,但是如果你读了其中的源码,你会发现要达到性能和速度的要求,对Java的基础要求还是蛮高的。
    原文作者:microhex
    原文地址: https://blog.csdn.net/u013762572/article/details/80517152
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞