存储系列知识 (一)——SharePreference

124 阅读4分钟

SharePreference

SharePreference是Android最基本的一个键值对存储,底层其实是使用的xml文件,通过app->内存->xml的二级缓存来实现效率的提高

我们以下四点来进行说明:

  1. SharePreference的基本使用
  2. put做了什么
  3. apply和commit的实现
  4. SharePreference的线程安全
  5. SharePreference的进程间通信
  6. mDiskWritesInFlight有什么作用

一、SharePreference的基本使用

写:

val sp = getSharedPreferences("default", MODE_PRIVATE)
sp.edit().putBoolean("isFirst",false).apply()//也可以用commit,后面会讲

读:

sp = getSharedPreferences("default", MODE_PRIVATE)
sp.getBoolean("isFirst",false)

put做了什么,源码位置:SharedPreferencesImpl.java


@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();

@Override
public Editor putBoolean(String key, boolean value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

一个线程安全的往hashmap放key/value的方法

二、apply和commit的实现

2.1 先看看commit的实现

@Override
public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    //.enqueueDiskWrite(mcr, null); 第二个参数为null,表示再当前县城
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        //这里是一个CountDownLatch,等上面写完,这里的阻塞会停止
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

代码的流程如下: image.png

这里面有概念我们先要进行一个大概的讲解:

  1. MemoryCommitResult类
  2. enqueueDiskWrite做了什么

2.1.1 MemoryCommitResult类

// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
     ...
    //需要写入磁盘的map
    final Map<String, Object> mapToWriteToDisk;
    //等待写文件的CountDownLatch
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    @GuardedBy("mWritingToDiskLock")
    volatile boolean writeToDiskResult = false;
    boolean wasWritten = false;

    private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
            @Nullable List<String> keysModified,
            @Nullable Set<OnSharedPreferenceChangeListener> listeners,
            Map<String, Object> mapToWriteToDisk) {
          ....
        this.listeners = listeners;
        this.mapToWriteToDisk = mapToWriteToDisk;
    }

    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        //写文件完毕,唤醒await
        writtenToDiskLatch.countDown();
    }
}

通读这个类,发现就是一个用来把Map写入磁盘的工具类。然后来看一下commitToMemory做了什么


private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        //其他线程在使用
        if (mDiskWritesInFlight > 0) {
            //深拷贝map,因为有其线程正在读写,只能生成一个新的map
            mMap = new HashMap<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        //操作线程数++
        mDiskWritesInFlight++;
        
        boolean hasListeners = mListeners.size() > 0;
        //深拷贝Listener
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                keysCleared = true;
                mClear = false;
            }
            //深拷贝已经修改的值
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}

这个方法,主要做两件事:

  1. 如果有其他线程再修改,先把当前的mMap产生一个新的
  2. 把modified和Listener全都深拷贝一份备用

2.1.2 enqueueDiskWrite做了什么

其实这里还有一个技术细节,那就是enqueueDisWrite的实现:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
     //没有传runnable,isFromSyncCommit = true
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    //写文件
    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //操作完线程数--
                    mDiskWritesInFlight--;
                }
                //这里postWriteRunnable == null
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

     //同步commit执行 
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            //有其他线程执行
            wasEmpty = mDiskWritesInFlight == 1;
        }
        //直接调用写文件
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //放入队列执行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

总结一下:

  1. isFromSyncCommit是否同步提交,mDiskWritesInFlight表示有几个线程在执行,如果只有一个线程(自己)再执行直接调用writeToDiskRunnable.run();
  2. 如果有多个线程执行,那就放入队列,等写完调用countdown,继续执行
  3. QueueWork是一个处理队列的工具类

2.1.3 总结一下commit

先深拷贝一份需要的内存变量,放到MemoryCommitResult,如果只有当前自己这一个线程,直接写入到文件中。如果有多个线程再执行,那就放入队列中

2.2 apply的源码

接下来我们看看apply的不同

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    //放入写磁盘的队列
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);
}

apply就是两个runnable。一个是等待文件写完的,一个是写文件的。依据我们前面分析的enqueueDiskWrite,这里不会走直接提交

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
     //没有传runnable,isFromSyncCommit = true
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    ...
     //同步commit执行 
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            //有其他线程执行
            wasEmpty = mDiskWritesInFlight == 1;
        }
        //直接调用写文件
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //放入队列执行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

因为postWriteRunnable!=null,所以isFromSyncCommit = true,会直接走到 QueuedWork.queue,enquque的第二个参数是表示是否delay,默认值为100ms

2.2.1 apply小总结

apply会延迟100毫秒,放入延迟100ms放入写文件的队列中。在写文件完成后,结束countdownLatch。

三、SharePreference的线程安全

3.1 存储的线程安全

@Override
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}
@Override
public Editor putStringSet(String key, @Nullable Set<String> values) {
    synchronized (mEditorLock) {
        mModified.put(key,
                (values == null) ? null : new HashSet<String>(values));
        return this;
    }
}
@Override
public Editor putInt(String key, int value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}
@Override
public Editor putLong(String key, long value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}
@Override
public Editor putFloat(String key, float value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}
@Override
public Editor putBoolean(String key, boolean value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

@Override
public Editor remove(String key) {
    synchronized (mEditorLock) {
        mModified.put(key, this);
        return this;
    }
}

@Override
public Editor clear() {
    synchronized (mEditorLock) {
        mClear = true;
        return this;
    }
}

3.2 保存的线程安全

前面我们说了:

  1. commit无多线程情况下,会直接在调用线程保存,若有多线程,则在QueueWork,使用MesssageQueue与Handler执行
  2. apply也是用MesssageQueue与Handler执行,不过每次会delay 100ms

四、进程间通信

SP的基本无进程间通信,所谓的进程间通信,也即是每次会reload一次xml。源码如下:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
   ....
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

这样一来,要保证数据同步,每次都必须要调用这个方法,不能存储SharePreference对象。sp的缓存机制将失效,所以不推荐使用MODE_MULTI_PROCESS。本身是有很多设计缺陷的

最后的总结:

  1. commit单线程情况下,会直接在当前线程写入文件,如果是多线程同时操作的情况下(mDiskWritesInFlight>1),会放入QueueWork的队列中执行
  2. apply总是会放到QueueWork的队列中,延迟100ms执行
  3. sp的线程安全包括修改安全和保存安全,第一个是用Sychronized关键字,第二个是用MessageQueue来保证线程安全的。
  4. sp支持多进程的操作值MULTI_PROCESS,但其实质是每次load xml,并无做特殊操作,