Android SharedPreferences 全面分析

2019-04-01  本文已影响0人  简书汪

我们经常用SharedPreferences用来存储一些比较小的键值对集合,适合保存应用的配置参数, 我们将会带着以下几个问题来分析SharedPreferences的源码实现:

源码分析

本文参照Android-26的源码,并不介绍SharedPreferences的基础使用,而是从源码角度来分析它的原理

获取SharedPreferences

我们通过以下方法来获取SharedPreferences实例

  1. context.getSharedPreferences
  2. 在Activity中getSharedPreferences
  3. PreferenceManager.getDefaultSharedPreferences
    这三种方法最终都会调用到 ContextImpl.getSharedPreferences
  @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        //SharedPreferences对应的xml文件,数据保存在其中
        File file;
        synchronized (ContextImpl.class) {
            ...//省略
            file = mSharedPrefsPaths.get(name);
            if (file == null) { 
                //如果没有该name命名的文件,则新建一个并放入缓存
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

 @Override
    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) {
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
       //mode设置为多进程模式时会检测SP文件最后修改的时间和大小,如果文件被其他进程改变时,则会重新加载
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

可以看到最终返回的是一个SharedPreferencesImpl对象,首先getSharedPreferencesCacheLocked()从一个静态的ArrayMap中获取SharedPreferences 缓存,如果有缓存中有SharedPreferencesImpl对象则返回,没有的话则创建一个并存入缓存中,同时synchronized 包裹可以保证多线程同步,由此可见无论getSharedPreferences调用多少次,返回的都是一个SharedPreferencesImpl对象

SharedPreferencesImpl

SharedPreferencesImpl 实现了SharedPreferences这个接口,是我们通过getSharedPreferences得到的实体对象,所有存取操作都由该类来实现

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

mBackupFile 代表发生异常时, 可通过备份文件来恢复数据.
mLoaded 表示是否已经将mFile中的数据都读取到mMap 中
mMap 用于在内存中缓存我们的配置数据, 也就是 getXxx 数据的来源
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;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        //...省略
        Map map = null;
        BufferedInputStream    str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
        map = XmlUtils.readMapXml(str);
       //...省略
        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();
        }
    }

开启一个子线程来从硬盘读取数据,如果备份文件存在则直接使用灾备文件回滚,使用XmlUtils把文件所有的数据读取到内存中的mMap中,mLoaded = true 标志SharedPreferencesImpl已经将数据读取完成,notifyAll()唤醒getXXX系列方法等待状态的线程,由于已经将数据中磁盘读取到内存中,此时调用getXXX系列方法就可以获取值了

getString分析

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

synchronized 关键字保证了线程安全,然后直接从mMap中获取对应的键值对就可以了,当我们调用getSharedPreferences 之后马上调用getString方法有可能SharedPreferencesImpl在子线程中还没有将文件中的数据读取完,此时mMap 还没有被赋值,所以awaitLoadedLocked()将会阻塞当前线程,直到读取完毕

private void awaitLoadedLocked() {
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

mLoaded为false表示尚未读取完成,其他的getXXX系列方法和getString如出一辙,都是先等待文件读取完毕,然后从mMap中获取相应的value

数据保存

我们通过getSharedPreferences().edit()来put各种值,看一下.edit()获取的是一个什么对象

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

保证磁盘读取完毕后,返回了一个新的EditorImpl对象

  public final class EditorImpl implements Editor {
        private final Object mLock = new Object();
        private final Map<String, Object> mModified = Maps.newHashMap();
        private boolean mClear = false;

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

     public Editor remove(String key) {
            synchronized (mLock) {
                mModified.put(key, this);
                return this;
            }
        }
      ...//省略
}

EditorImpl 中有两个重要属性,mModified 用来暂时保存put方法提供的值,当调用commit()或者apply()才会将mModified中的数据存储到mMap,进而保存到磁盘中,mClear标志是否要清空文件中所有数据。接下来需要注意看remove()方法,调用getSharedPreferences().edit().remove()时是将当前key的value置为this,删除数据时检测到value为this即可删除
总结:调用put()后,数据只是暂存到了EditorImpl 的mModified** 对象中,并没有回写到磁盘,调用commit()apply才会将数据写到磁盘中**

commit()

public boolean commit() {
           ...//省略
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } 
            return mcr.writeToDiskResult;
        }

主要有三步

  1. commitToMemory()
    我们逐个分析,首先分析commitToMemory()返回一个MemoryCommitResult对象,代表了提交到内存的返回结果
 private static class MemoryCommitResult {
           //...省略代码
        final Map<String, Object> mapToWriteToDisk;
        //此处初始换CountDownLatch 的计数器为1
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
    }

其中关键有 writtenToDiskLatch 是一个 CountDownLatch 对象,它允许一个或多个线程一直等待,直到回写磁盘线程的操作执行完后再执行,mapToWriteToDisk引用内存中的mMapwriteToDiskResult代表回写磁盘是否成功,接下来继续分析commitToMemory()

private MemoryCommitResult commitToMemory() {
            Map<String, Object> mapToWriteToDisk;
            synchronized (SharedPreferencesImpl.this.mLock) {
                mapToWriteToDisk = mMap;
                //需要写入磁盘次数+1
                mDiskWritesInFlight++;
                synchronized (mLock) {
                    if (mClear) {
                        //...省略代码,
                        //如果调用了edit().clear()则清空内存中的数据
                        mMap.clear();
                        mClear = false;
                    }
                    
                    //将putXXX()的数据提交到内存中
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        //value为this则删除,与之前的getSharePreferences().edit().remove()对应
                        if (v == this || v == null) {
                            if (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                            if (mMap.containsKey(k)) {
                                Object existingValue = mMap.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mMap.put(k, v);
                        }
                    }
                    mModified.clear();
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

mDiskWritesInFlight代表写入磁盘这个操作的次数,也是由synchronized 保证线程安全,首先判断是否需要clear,如果需要这把mMap中的数据清空,需要注意此时mModified中数据还没有复制到mMap中,所以以下代码并不能将"foo" clear掉

sharedPreferences.edit()
        .putBoolean("foo";, true)        // foo 无法被 clear 掉
        .clear()
        .putBoolean("bar", true)
        .commit()

然后通过for循环将put到mModified中的数据添加到mMap中,mModified.clear()之后返回MemoryCommitResult
总结commitToMemory()只是将数据都写入到内存中

  1. SharedPreferencesImpl.this.enqueueDiskWrite
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 (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        //异步执行任务
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

commit() 时postWriteRunnable参数为null,所以isFromSyncCommit == true,进入到if (isFromSyncCommit) 语句中,如果此时只有一个commit()操作,则直接在当前线程执行writeToFile()将内存中的数据回写到磁盘中,如果此时有多个commit()则,排队进入QueuedWork中等待执行,看一下writeToFile()的实现

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        //...省略
        boolean fileExists = mFile.exists();
        boolean backupFileExists = mBackupFile.exists();
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            str.close();
            mBackupFile.delete();
            mcr.setDiskWriteResult(true, true);
            return;
        } catch (Exception e) {
        }
        //如果写入操作出现异常,则将半成品删掉
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }
  1. mcr.writtenToDiskLatch.await()
    CountDownLatch.await()会阻塞当前线程,直到CountDownLatch.countDown()使计数器值到达0时,它表示磁盘写入线程已经完成了任务,然后在锁上等待的线程就可以恢复执行任务。在writeToFile()中,写入完成之后会调用mcr.setDiskWriteResult()中的writtenToDiskLatch.countDown()
 void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }

writtenToDiskLatch初始时计数器为1,countDown()之后为0,此时磁盘已经回写完毕,commit()方法继续执行,返回结果
commit()总结

apply()

       public void apply() { 
            //第一步:提交到内存
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };
            //第二步:确保异步磁盘写入完毕
            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            // 第三步:写入磁盘
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        }

SharedPreferences总结

通过上文对SharedPreferences分析,我们已经可以对开头的几个问题进行回答并总结了

上一篇下一篇

猜你喜欢

热点阅读