美文网首页
Android SharedPreferences解析

Android SharedPreferences解析

作者: iceIC | 来源:发表于2020-09-04 23:45 被阅读0次

基于Api29源码

SharedPreferences接口

首先,让我们看下SharedPreferences接口


SharedPreferences接口

其中有两个子接口 EditorOnSharedPreferenceChangeListener。我们发现SharedPreferences接口有很多getXXX系列的方法,通过这些方法可以获得我们存进去的key对应的value。其中子接口Editor有很多putXXX系列的方法,我们可以利用这些方法为指定的key设置对应的value。需要注意的是,调用一系列的putXXX方法后如果没有调用apply和commit是不会生效的。所以正常的使用规则通常类似 SharedPreferences对象.edit().putBoolean("xxx",false).putString("yyy").apply();

其中 commit 方法是有返回提交成功还是失败的,通常是同步调用(特殊情况下面有分析)。所以如果我们在主线程同时不需要知道操作是否成功的话是可以直接调用 apply 方法进行异步提交的。

对于子接口 OnSharedPreferenceChangeListener ,其作用就是在SharedPreferences的key被修改时进行回调,我们也可以看到SharedPreferences接口有registerOnSharedPreferenceChangeListener和unregisterOnSharedPreferenceChangeListener这两个方法进行回调的注册以及取消注册。

那我们怎么获得SharedPreferences对象呢?

  1. Context#getSharedPreferences
    我们这里简单分析下源码。
    Context的实现类是ContextImpl,所以我们去ContextImpl里面找getSharedPreferences函数。注意我贴的源码都会删掉一些不影响阅读的代码。
    public SharedPreferences getSharedPreferences(String name, int mode) {
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name); //1
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

首先看入参,需要一个name来代表SharedPreferences对象对应硬盘上的文件的文件名。其次需要一个文件mode,mode有四种取值:MODE_PRIVATE,MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE,MODE_MULTI_PROCESS。我们只要用第一种,后面三个都是用于多进程,且Android 7.0之后用会丢异常,后面会有分析。第一种可以简单理解为只有创建该文件的进程可以控制读写(其实如果多个进程有同一个userId是都可以处理该文件的)

我们能看到ContextImp用了一个ArrayMap对象mSharedPrefsPaths进行数据缓存。该Map对象的key为name,value为对应的硬盘File对象。

这里我们分析第一次使用,是没有缓存的,所以直接看注释1处的代码getSharedPreferencesPath

    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);  //1
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        ...
        return sp;
    }

可以看到这里又有一个ArrayMap的缓存对象cache。这个Map对象的key为File对象,value为SharedPreferencesImpl对象。SharedPreferencesImpl就是SharedPreferences的实现类。

其中注释1处的checkMode方法会检查android版本是否大于等于7.0,如果是的话mode为MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE会丢异常。

    private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }

至此我们就知道了SharedPreferences的获取过程,下一章节会继续探究SharedPreferences使用过程中的源码。

  1. Activity#getPreferences
    public SharedPreferences getPreferences(int mode) {
        return getSharedPreferences(getLocalClassName(), mode);
    }

Activity继承于Context的,内部也是调用了上面的getSharedPreferences方法,只不过文件名是Activity的名称。

SharedPreferences源码解析

创建过程

首先看下构造函数

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file); //File对象,从名字看得出这个是备份文件
        mMode = mode;
        mLoaded = false; //Boolean对象 判断是否已经从硬盘文件加载数据到内存
        mMap = null; //Map<String, Object>对象 硬盘文件存的key-value会放到该内存对象中
        startLoadFromDisk();
    }

我们直接看startLoadFromDisk方法

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

可以看到其开启一个子线程去加载读取硬盘文件,我们继续往下看

    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                //已经加载过就直接return
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        Map<String, Object> map = null;
        if (mFile.canRead()) {
            BufferedInputStream str  = new BufferedInputStream(
                new FileInputStream(mFile), 16 * 1024);
            map = (Map<String, Object>) XmlUtils.readMapXml(str);
        }
        mLoaded = true; //将标记位设置true表示已经加载过硬盘数据到内存
        if (map != null) {
            mMap = map;
        } else {
            mMap = new HashMap<>();
        }
        //如果在loadFromDisk未执行完的时候调用putXXX、getXXX系列方法
        //会执行mLock.wait()进行等待,所以执行完后将那些wait的线程唤醒。
        mLock.notifyAll();
    }

上面的代码就是读取本地文件,当一个xml来解析获得一个map对象,然后赋值给成员变量mMap。

获取数据过程

Ok,知道了创建过程,让我们看看获取value的过程。
一些列的getXXX方法我们只需要分析一个就好。
这里我们开始分析getInt方法。

    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked(); //等待loadFromDisk执行完
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

其中awaitLoadedLocked会等待到子线程执行完loadFromDisk。

    private void awaitLoadedLocked() {
        while (!mLoaded) {
            mLock.wait();
        }
    }

获取数据的过程很简单,就是通过内存对象mMap进行操作,既然是通过内存对象进行操作,那这里就有需要注意的一点,我们不应该对mMap取出来的对象进行修改,这样的话其他线程再次从mMap中获取数据的话,取出来的就是我们修改过后的数据!

设置数据过程

设置数据稍微麻烦一点,必须要先调用edit方法获得Editor对象。

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

EditorImpl是Editor接口的实现类,我们看下其大概结构。

    public final class EditorImpl implements Editor {
        private final Object mEditorLock = new Object();//同步锁
        //所有的putXXX系列方法调用都会先用该对象存储
        private final Map<String, Object> mModified = new HashMap<>();
        private boolean mClear = false;//清除标记
    }

putXXX系列方法也基本一样,所以这里我们分析下putStringSet就好了。

    public Editor putStringSet(String key, Set<String> values) {
        synchronized (mEditorLock) {
            mModified.put(key,
                    (values == null) ? null : new HashSet<String>(values));
            return this;
        }
    }

可以看到我们所有的putXXX系列方法中都将要改变的key和value存到了mModified对象中,其会在我们调用commit和apply方法时统一作用到mMap内存对象再提交到硬盘上。

提交数据过程

接下来我们分析下commit和apply方法

    public boolean commit() {
        MemoryCommitResult mcr = commitToMemory();
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
        mcr.writtenToDiskLatch.await();
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }

    public void apply() {
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = () -> mcr.writtenToDiskLatch.await();
        QueuedWork.addFinisher(awaitCommit);
        Runnable postWriteRunnable = () -> {
            awaitCommit.run();
            QueuedWork.removeFinisher(awaitCommit);
        };
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        notifyListeners(mcr);
    }

我们分析下两个方法的区别,其都调用了commitToMemory方法、enqueueDiskWrite方法、notifyListeners方法。

notifyListeners很简单,就是通知OnSharedPreferenceChangeListener进行回调。

commitToMemory主要就是将mModified的数据依次应用到mMap上。需要注意的是其会在一开始判断你是否用Editor对象调用过clear方法,如果调用过的话他会先将mMap数据清空,再依次将mModified的数据依次应用到mMap上。比如执行如下代码后SharedPreferences.editor().putString("xx","1").clear().commit() ,mMap里就只有一个为xx的key啦。

Ok,重点就是enqueueDiskWrite方法。

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //如果是commit方法,postWriteRunnable为null
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //将数据写到文件
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        if (isFromSyncCommit) {
            //只有commit方式才会走到这
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

我们先分析apply方式,他会调用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
我们直接看queue方法。

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

如果是apply形式的话,shouldDely为true,其中getHandler获得的Handler对象是通过一个HandlerThread中的Looper创建的,简单说这个Handler对象是负责将消息发送到子线程的。DELAY是100ms,所以通过apply方法是会将上面的writeToDiskRunnable对象放到子线程延迟100ms执行。换句话说是异步的。

最后我们看看commit方式,commit方法稍微复杂点,其涉及到mDiskWritesInFlight变量。这个变量在commitToMemory时会+1,writeToDiskRunnable中执行完writeToFile后会-1,所以我们就把他理解为要写到硬盘的次数就行!
我多举几个例子。
例子1:我调用commit时,会走到commitToMemory,其 mDiskWritesInFlight+1=1表示需要一次写到硬盘,然后执行enqueueDiskWrite方法,在这里面由于我是commit形式调用,其mDiskWritesInFlight又等于1,wasEmpty标记位为true,所以就直接同步执行writeToDiskRunnable.run();
例子2:我调用commit时(线程1),会走到commitToMemory,其 mDiskWritesInFlight+1=1表示需要一次写到硬盘,这个时候我在另一个线程(线程2)又创建另一个editor对象并且调用commit,线程2走到commitToMemory,其其 mDiskWritesInFlight+1=2!,这个时候尽管是commit方式,mDiskWritesInFlight不等于1,所以wasEmpty为false,所以会走到QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);。我们知道queue方法是将writeToDiskRunnable放到子线程执行的!所以,commit形式并不一定是同步执行的!

后记

关于mBackupFile

还记得mBackupFile对象么。这个其实就是防止将内存数据写到硬盘数据失败的一种回退机制。
mBackupFile对象主要用在writeToFile方法中,我简单截取相关代码。

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        //在这个方法中我们主要是通过mFile获取一个输出流用来将内存数据写入硬盘,所以会先将之前的数据备份
        boolean backupFileExists = mBackupFile.exists();
        if (!backupFileExists) {
            //如果不存在mBackupFile,则将mFile重命名为mBackupFile进行备份
            mFile.renameTo(mBackupFile)
        } else {
            //如果存在mBackupFile,即已经有备份文件了,直接删掉mFile即可
            mFile.delete();
        }

        
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        //如果上面写入操作没有丢异常会走到这里删除备份文件
        mBackupFile.delete();
    }

相关文章

网友评论

      本文标题:Android SharedPreferences解析

      本文链接:https://www.haomeiwen.com/subject/ymztektx.html