SharePreferences实现

android中的文件存储方式有三种读写数据库,读写本地文件,读写SharePreferences。

其实读写SharePreferences也属于读写本地文件,android为了方便开发者存储轻量的数据,实现了这个工具。其实android为了方便开发者的使用,提供了很多类似的工具,最典型的就是SyncTask.

好,闲话不多说,今天聊聊SharePreferences的实现原理以及android实现这个简单的工具的闪光点。

假如让我们实现一个本地存储,那么我们需要注意那些东西呢?我觉得至少有三点:

1.读取策略

2.更新策略

3.容错处理

本地存储的基本流程也很简单:

1.程序将内存中的数据按照合适的格式写入文件

2.程序在有权限的情况下,从文件中读取数据

但是如果真的按照上面说的两步,那么显然会有很多的IO操作,那么SharePreferences是怎么做到的呢?我们按照本地存储的三点,分析源码。

首先说第一点,SharePreferences存储的格式,查看代码之前,先看下一个实例。通过adb命令找到包名/data/data/share_pref目录下的文件,这个目录下的文件就是当前包名的app创建的SharePreferences文件,可以看到文件是xml格式的。目录结构如下图所示:



随便打开一个xml文件,可以看到xml中存储的是标准的xml格式字符串,如图2所示。通过图2,我们可以感性的知道,SharePreferences存储格式是xml。

好的,查看了SharePreferences具体存储文件,接着看代码实现,SharePreferences的代码实现类是SharePreferencesImpl。

图3是一个典型的SharePreferences调用方式:

private static SharedPreferences sharedPreferences = AppUtils.context.getSharedPreferences(ConstantInPreference.FILE_NAME, Context.MODE_PRIVATE);
sharedPreferences.getBoolean(key, defaultValue);
sharedPreferences.edit().putBoolean(key, value);
通过Apputil的上下文会生成一个跟文件名绑定的SharePreferences实例。这也知道我们,应用程序在运行期间,对于单个文件,只需要有一个SharePreferences实例就可以了。SharePreferences的实例实际上就是SharePreferencesImpl实例,创建对象,自然离不开构造函数,先看下SharePreferencesImpl的构造函数。

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }
构造函数的入参是file,mode。这里面的file就是创建实例时,传入的filename对应的File对象,mode时传入的文件模式,实例中用的是private。构造函数的第二行

  mBackupFile = makeBackupFile(file);
是创建一个备份文件,那么为什么这儿需要创建一个备份文件呢?这个等下说,接着看构造函数的最后一行。

    startLoadFromDisk();
这行代码是将文件中存储的数据转为map,加载到内存中,这就是读操作。具体的方法实现,如下图。

private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

从实现,可以看出,startLoadFromDisk做了两件事

1.将mLoaded状态设置为false

2.开启一个线程,加载文件中的内容,也就是读文件。

这段代码,可以知道我们在创建完SharePreference的时候,并不能立刻获取到磁盘文件中的内容,因为读文件的操作是在子线程中进行的。那么不禁有人要问了,我在平时使用SharePreference的时候,是在创建完对象之后就可以正常获取数据了呀。比如

private SharedPreferences sharedPreferences = AppUtils.context.getSharedPreferences(ConstantInPreference.FILE_NAME, Context.MODE_PRIVATE);
boolean value = sharedPreferences.getBoolean(key, defaultValue);
上面代码中的value是有值的,这是为什么呢?按理说SharePreference对象创建完成之后,是在另外一个线程中,加载磁盘文件的呀,为什么下面的同步代码,可以立刻获取到文件中的值?这里面的关键就在mLoaded这个变量,这是一个全局的变量,看一下getBoolan值的代码。

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

getBoolean方法也做了两件事:

1.awaitLoadedLock

2.获取mMap中的key对应的value值

getBoolean方法能够获取到正确的值的关键就在awaitLoadedLock。

private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }
这个方法,如果发现mLoaded为false,那么为一致处于wait状态,当然如果初始化完成了,之后再进行读取操作,会获取内存中的mMap中的值,速度会很快,但是同样也会导致其他的app更新了sp文件,不能感知到。到这儿,基本的读流程就可以很清晰了,流程如下所示:




这儿最重要的是调用线程挂起,等待读文件线程完成。这实际上就是说,初始化sharepreference后,立即调用的获取值的操作最好不要在UI线程进行,因为这会导致线程挂机,直到初始化工作中读取磁盘文件完成。初始化完成之后,sharepreference在获取值,会从内存中的mMap中取,速度会很快,减少IO次数,这也导致,不能及时感知其他app对文件做的更改。总结下初始化与读取过程有两点优化:

1.第一次初始化将磁盘中的数据同步到内存的mMap中,这会使得后续的读取操作不需要进行IO操作,提高速度。

2.读取值的方法都会在初始化完成之后返回,内部做了线程同步,简化了开发者的使用方式。

文件读操作的具体实现,其实很简单,解析xml,将数据存储到内存的mMap中,具体loadFromDisk方法不贴了,感兴趣的可以去查一下。

到这里,我们可以回答第一个问题,读取策略:

1.存储xml格式

2.初始化的时候,同步xml文件中的内容到内存的mMap中

3.初始化完成之后,获取值都从mMap中获取。


接着说更新策略

按照之前的分析,sharepreference读取的是内存中的mMap中的值,那么更新的时候呢?也就是说sharepreference是怎么存储数据的呢?下面的代码是一个常用的存储sharepreference的代码。

sharedPreferences.edit().putBoolean(key, value).apply();

上面的代码实现了往sharepreference中存储一个boolean值的功能,那么底层的实现原理是什么呢?

sharedPreferences.edit()
这行代码是获取一个编辑器,sharepreference对象本身不具备存储的功能,他提供了一个editor对象,以便进行存储键值对数据,当然如果这个时候还没有同步完本地的xml文件,那么线程会处于阻塞状态,知道本地的xml文件数据全部同步到内存中,edit方法返回一个EditorImpl的一个实例,真正做存储的功能是在这个类中实现的。
public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (this) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

EditorImpl是sharepreference的一个内部类,也就是说EditorImpl可以读取到sharepreference中存储的mMap得值,这也保证了Editor可以将mMap中的值同步到xml文件中。EditorImpl中包含两个全局变量,如下所示:

private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;

这两个全局变量中mModified是一个hashMap,存储的是往editorimpl中存放的数据,也就是用户想要存储到xml中的数据,这个mModify也意味着,存储数据的时候,数据首先会被存储到内存中,那么什么时候会同步到本地的xml文件中呢?是在调用apply方法之后,内存中的数据才会被同步到本地的xml文件中,下面的代码是apply方法的实现:

public void apply() {
	   final MemoryCommitResult mcr = commitToMemory()

final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run();
 QueuedWork.remove(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // Okay to notify the listeners before it's hit disk // because the listeners should always get the same // SharedPreferences instance back, which has
 the // changes reflected in memory. notifyListeners(mcr); }

appy方法比较简洁,第一行方法如下,这行方法调用了commitToMemory方法,通过方法名也可以很清晰的看到,是将mModify中的键值对数据同步到内存中,也就是说,当我们调用了apply方法之后,sharepreference中的mMap会添加editor中的mModify中存储的键值对数据,这保证了内存数据的同步,此处不贴commitToMemory的详细代码,只想说下,这个方法会操作mMap对象,所以加了同步锁,同步完成之后会将mModify清空掉。

 final MemoryCommitResult mcr = commitToMemory()

紧接着apply创建了两个runnable的局部变量,然后调用如下方法:

 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

这个方法就是调用enqueueDiskWrite方法,这个方法会将同步内存中mMap的数据到xml中。代码详细如下所示,这段代码首先汇创建一个包含同步任务的runnable,然后根据postWriteRunnable的值判断是否需要立即执行,apply方法传入的postWriteRunnable是不为空的,所以apply方法会将同步任务的runnable放到单线程池中执行。

 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        final boolean isFromSyncCommit = (postWriteRunnable == null);

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

同步任务writeToDiskRunnable会调用writeToFile的方法,writeToFile就是真正处理同步内存mMap的数据到xml文件的逻辑,具体代码不贴了。需要注意的是writeToFile方法同步的内存对象实质就是sharepreference的mMap中的数据,其次该方法在往xml中存储数据的时候,会先将本地的数据备份。

通过分析也知道了sharepreference是通过创建一个editimpl的实例进行更新逻辑的,调用方会先将需要存储的键值对数据存在editimpl的mModify中,然后调用apply放将mModify中的数据更新到sharepreference的内存中的mMap数据中去,这保证了调用了apply方法之后,editor的所属sharepreference对象能够及时得到同步,然后edit会根据需要选择合适的线程执行同步sharepreference中的mMap到本地磁盘对应的mMap中。

目前为止更新策略已经很明确了。主要分为以下几步:

1.创建一个EditImpl对象,用来存储需要更新的键值对

2.将存储的键值对同步到sharepreference的内存对象mMap中

3.更新sharepreference中的mMap到本地的xml中

最后分析以下容错处理,如果更新策略的最后一步,写入xml文件,出现了异常,写入失败,那么又该怎么办呢?一个策略是不断循环,直到写成功为止,显然这不是一个有效的办法,特别浪费电量,而且如果由于环境的原因,还会导致无限循环。另外一个策略就是忽略这次写入,为了性能降低正确性,至少内存中包含了正确的数据,还是有机会在下一次apply的时候,同步到xml中的。

sharepreference使用的就是第二个策略,放弃这次写入,此时内存中的数据与xml中的数据是不一致的,具体的实现,是借助backupfile实现的。步骤如下:

1.初始化sharepreference的时候,如果发现有同文件名,并且以.bak结尾的文件存在,那么会使用.bak文件的数据,将.bak文件中的数据同步到内存mMap中

2.更新的时候,如果.bak文件不存在,先将本地的xml文件rename为.bak文件,否则删除xml文件,通常.bak文件存在,xml文件是不存在的

3.创建新的xml文件,同步内存中的mMap到文件中,如果同步成功了,那么删除.bak文件,否则删除新创建的文件,保留.bak文件

通过这三步,就能保证xml同步内存的mMap失败之后,也能将上一次的数据存在.bak中,以便保证下一次同步的时候使用,这其实也暗示我们。sharepreference中内存中的数据与xml中的数据不一定一样,同一个xml的不同sharepreference对象中的键值对数据也不一定一样。


总结:

1.初始化sharepreference的时候,同步xml中的数据到内存的mMap中,后续取值都会从内存中获取

2.同步过程结束之前,任何获取数据都会陷入锁等待状态

3.更新的时候,先将数据存储在EditorImpl中,直到调用了apply方法的时候,才会将EditorImpl同步到sharepreference

4.同步Editor中的键值对的时候,线同步修改的数据到sharepreference的内存对象mMap中,然后才会将mMap中的数据写入xml中

5.写入xml的时候,为了容错,还需将当前文件备份,防止写入失败,数据全部丢失,写入成功之后会将备份文件删除,否则这个备份文件会一直存在,只要备份文件存在,sharepreference初始化的时候,就会用备份文件中的数据

6.sharepreference的读取,更新,容错机制说明sharepreference对象中的数据与xml中的数据不一定一直,同一个xml对象的不同sharepreference中的数据也不不一定一致,因为apply会线同步数据到内存中,然后再将数据写入到xml中,这个时候有可能写入失败,而sharepreference读取数据的时候,都会从内存中获取



















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值