Android 数据存储知识梳理(3) - SharedPreference 源码解析

一、概述

SharedPreferences在开发当中常被用作保存一些类似于配置项这类轻量级的数据,它采用键值对的格式,将数据存储在xml文件当中,并保存在data/data/{应用包名}/shared_prefs下:


今天我们就来一起研究一下SP的实现原理。

二、SP 源码解析

2.1 获取 SharedPreferences 对象

在通过SP进行读写操作时,首先需要获得一个SharedPreferences对象,SharedPreferences是一个接口,它定义了系列读写的接口,其实现类为SharedPreferencesImpl、在实际过程中,我们一般通过Application、Activity、Service的下面这个方法来获取SP对象:

public SharedPreferences getSharedPreferences(String name, int mode)

来获取SharedPreferences实例,而它们最终都是调用到ContextImplgetSharedPreferences方法,下面是整个调用的结构:


ContextImpl当中,SharedPreferences是以一个静态双重ArrayMap的结构来保存的:

private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;

下面,我们看一下获取SP实例的过程:

    public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }
            //1.第一个维度是包名.
            final String packageName = getPackageName();
            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
            if (packagePrefs == null) {
                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
                sSharedPrefs.put(packageName, packagePrefs);
            }
            //2.第二个维度就是调用get方法时传入的name,并且如果已经存在了那么直接返回
            sp = packagePrefs.get(name);
            if (sp == null) {
                File prefsFile = getSharedPrefsFile(name);
                sp = new SharedPreferencesImpl(prefsFile, mode);
                packagePrefs.put(name, sp);
                return sp;
            }
        }

        return sp;
    }

在上面,我们看到SharedPreferencesImpl的构造传入了一个和name相关联的File,它就是我们在第一节当中所说的xml文件,在构造函数中,会去预先读取这个xml文件当中的内容:

SharedPreferencesImpl(File file, int mode) {
        //..
        startLoadFromDisk(); //读取xml文件的内容
}

这里启动了一个异步的线程,需要注意的是这里会将标志位mLoad置为false,后面我们会谈到这个标志的作用:

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

loadFromDiskLocked中,将xml文件中的内容保存到Map当中,在读取完毕之后,唤醒之前有可能阻塞的读写线程:

    private Map<String, Object> mMap;

    private void loadFromDiskLocked() {
        //1.如果已经在加载,那么返回.
        if (mLoaded) {
            return;
        }

        //...
        //2.最终保存到map当中
        map = XmlUtils.readMapXml(str);
        mMap = map;

        //...
        //3.由于读写操作只有在mLoaded变量为true时才可进行,因此它们有可能阻塞在调用读写操作的方法上,因此这里需要唤醒它们。
        notifyAll();
    }

SP对象的获取过程来看,我们可以得出下面几个结论:

  • 与某个name所对应的SP对象需要等到调用getSharedPreferences才会被创建
  • 对于同一进程而言,在Activity/Application/Service获取SP对象时,如果name相同,它们实际上获取到的是同一个SP对象
  • 由于使用的是静态容器来保存,因此即使Activity/Service销毁了,它之前创建的SP对象也不会被释放,而SP中的数据又是用Map来保存的,也就是说,我们只要调用了某个name相关联的getSharedPreferences方法,那么和该name对应的xml文件中的数据都会被读到内存当中,并且一直到进程被结束。

2.2 通过 SharedPreferences 进行读取操作

读取的操作很简单,它其实就是从之间预先读取的mMap当中去取出对应的数据,以getBoolean为例:

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

这里唯一需要关心的是awaitLoadedLocked方法:

    private void awaitLoadedLocked() {
        //这里如果判断没有加载完毕,那么会进入无限等待状态
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {}
        }
    }

在这个方法中,会去检查mLoaded标志位是否为true,如果不为true,那么说明没有加载完毕,该线程会释放它所持有的锁,进入等待状态,直到loadFromDiskLocked加载完xml文件中的内容调用notifyAll()后,该线程才被唤醒。

从读取操作来看,我们可以得出以下两个结论:

  • 任何时刻读取操作,读取的都是内存中的值,而并不是xml文件的值。
  • 在调用读取方法时,如果构造函数中的预读取线程没有执行完毕,那么将会导致读取的线程进入等待状态。

2.3 通过 SharedPreferences 进行写入操作

2.3.1 获取 EditorImpl

当我们需要通过SharedPreferences写入信息时,那么首先需要通过.edit()获得一个Editor对象,这里和读取操作类似,都是需要等到预加载的线程执行完毕:

    public Editor edit() {
        synchronized (this) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

Editor的实现类为EditorImpl,以putString为例:

    public final class EditorImpl implements Editor {

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

        public Editor putString(String key, @Nullable String value) {
            synchronized (this) {
                mModified.put(key, value);
                return this;
            }
        }
   }

由上面的代码可以看出,当我们调用EditorputXXX方法时,实际上并没有保存到SPmMap当中,而仅仅是保存到通过.edit()返回的EditorImpl的临时变量当中。

2.3.2 apply 和 commit 方法

我们通过editor写入的数据,最终需要等到调用editorapplycommit方法,才会写入到内存和xml这两个地方。

(a) apply

下面,我们先看比较常用的apply方法:

        public void apply() {
            //1.将修改操作提交到内存当中.
            final MemoryCommitResult mcr = commitToMemory();
           
            //2.写入文件当中
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //postWriteRunnable在写入文件完成后进行一些收尾操作.
            
            //3.只要写入到内存当中,就通知监听者.
            notifyListeners(mcr);
        }

整个apply分为三个步骤:

  • 通过commitToMemory写入到内存中
  • 通过enqueueDiskWrite写入到磁盘中
  • 通知监听者

其中第一个步骤很好理解,就是根据editor中的内容,确定哪些是需要更新的数据,然后把SP当中的mMap变量进行更新,之后将变化的内容封装成MemoryCommitResult结构体。

我们主要看一下第二步,是如何写入磁盘当中的:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //1.写入磁盘任务的runnable.
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    //1.1 写入磁盘
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    //....执行收尾操作.
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        
        //2.这里如果是通过apply方法调用过来的,那么为false
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        if (isFromSyncCommit) { //apply 方法不走这里
                //...
                writeToDiskRunnable.run();
                return;
        }

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

可以看出,如果调用apply方法,那么对于xml文件的写入是在异步线程当中进行的。

(b) commit

如果调用的commit方法,那么执行的是如下操作:

       public boolean commit() {
            //1.写入内存
            MemoryCommitResult mcr = commitToMemory();
            //2.写入文件
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null); //由于是同步进行,所以把收尾操作放到Runnable当中.
            //在这里执行收尾操作..
            //3.通知监听
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

当使用commit方法时,和apply类似,都是三步操作,只不过第二步在写入文件的时候,传入的Runnablenull,因此,对于写入文件的操作是同步的,因此,如果我们在主线程当中调用了commit方法,那么实际上是在主线程进行IO操作。

(c) 回调时机

  • 对于apply方法,由于它对于文件的写入是异步的,但是notifyListener方法不会等到真正写入完成时才通知监听者,因此监听者在收到回调或者apply返回时,对于SP数据的改变只是写入到了内存当中,并没有写入到文件当中。
  • 对于commit方法,由于它对于文件的写入是同步的,因此可以保证监听者收到回调时或者commit方法返回后,改变已经被写入到了文件当中。

2.4 监听 SP 的变化

如果希望监听SP的变化,那么可以通过下面的这两个方法:

    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(this) {
            mListeners.put(listener, mContent);
        }
    }

    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(this) {
            mListeners.remove(listener);
        }
    }

由于对应于NameSP在进程中是实际上是一个单例模式,因此,我们可以做到在进程中的任何地方改变SP的数据,都能收到监听。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容