1.我们都知道SharedPreferences 是android可以用来存放key value的的文件。
SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString("key","value"); editor.commit();
SharedPreferences sp = getSharedPreferences("fileName", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString("key","value"); editor.apply();
SharedPreferences是一个接口。getSharedPreferences 拿到的是它的实现类SharedPreferencesImpl。
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName); public SharedPreferences getSharedPreferences(String name, int mode) { if (sp == null) { File prefsFile = getSharedPrefsFile(name); sp = new SharedPreferencesImpl(prefsFile, mode); packagePrefs.put(name, sp); return sp; } }
在构造函数中,会把存储的键值对保存到一个hashMap中
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; //读取文件中存储的key value,并存到全局变量mMap中 startLoadFromDisk(); }
private void loadFromDiskLocked() { ....... str = new BufferedInputStream( new FileInputStream(mFile), 16*1024); map = XmlUtils.readMapXml(str); if (map != null) { mMap = map; mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } else { mMap = new HashMap<String, Object>(); } }
当我们getString等取值的时候,就是从这个mMap中取的。
get方法就是从这个map中读取。 public String getString(String key, String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
2. sharedPrefrence存数据,有两种方式,commit和apply。
sp.edit()拿到的也是一个接口,Editor,实现类是EditorImpl。
SharedPreferences.Editor editor = sp.edit(); public Editor edit() { return new EditorImpl(); }
当调用putString(String key, String value)时,先保存到了一个map中
private final Map<String, Object> mModified = Maps.newHashMap(); public Editor putString(String key, String value) { synchronized (this) { //将要修改的key value,存放到map中 mModified.put(key, value); return this; } }
那么commit和apply的区别是什么?
1).commit有返回值是一个boolean类型。
apply没有返回值,返回的是void。
2)commit是同步存储,所以必须拿到返回值,代码才能往下走,否则会阻塞在这。
apply是异步存储,直接丢在了一个线程中执行,我们不必等待他的返回结果。
直接看源码
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; } notifyListeners(mcr); return mcr.writeToDiskResult; }
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); notifyListeners(mcr); }
分析源码
commit和apply都调用了这行代码,
final MemoryCommitResult mcr = commitToMemory();
和private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) ;
这俩的不同在于第二个参数 Runnable postWriteRunnable。commit传的是一个null,而apply传的是一个Runnable对象。这个参数很关键,后面会根据这个参数进行判断,选择是异步存储还是同步存储。
先看commitToMemory()是如何实现的。
这个方法是将要修改的键值对(存在mModified中),和文件中的的全量键值对(存在mMap中),
进行比对,把更新后的map赋值给 mcr.mapToWriteToDisk = mMap;
private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); //mMap存储了文件中所有的键值对。 mcr.mapToWriteToDisk = mMap; 对要新增或修改的键值对进行遍历。并添加到mMap中 for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mMap.put(k, v); } mcr.changesMade = true; mModified.clear(); return mcr; }
在看第二个方法enqueueDiskWrite(mrc,runnable)。
如果是commit方式存储,runnable==null。则调用writeToDiskRunnable.run();进行存储,这个方法是同步的。
如果是apply方式存储,runnable!=null。会直接放进一个线程池中执行。
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
这也就是为什么apply是异步存储。
注意第二个参数,commit传的是null。apply传的是一个postWriteRunnable 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(); } } }; //根据 postWriteRunnable 是不是null来区分是commit方式还是apply方式 final boolean isFromSyncCommit = (postWriteRunnable == null); // Typical #commit() path with fewer allocations, doing a write on // the current thread. //如果是commit方式,上面的注释很也说明了commit是在当前线程执行的文件存储。 if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (SharedPreferencesImpl.this) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { //直接调用Runnable的run方法。在当前线程执行文件的存储。所以是同步方式 writeToDiskRunnable.run(); return; } } // 如果是applay方式,上面代码不会执行,也就不会return。 //则会把存储文件的方法放到一个线程池中去执行 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); }
然后在看看writeToFile(MemoryCommitResult mcr)。将修改后的键值对,保存入文件中。
先是对源文件做了一个备份,然后全量的写入文件。
如果写成功,会将备份文件删除。
如果写文件时出现异常,则会将备份文件恢复。
private void writeToFile(MemoryCommitResult mcr) { //在写文件前,先将源文件进行一个备份 if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { mcr.setDiskWriteResult(false); return; } } else { //如果备份文件存在,则将源文件删掉 mFile.delete(); } FileOutputStream str = createFileOutputStream(mFile); //将文件中所有的keyvalue,保护要修改的,全量存入新的文件中。 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); FileUtils.sync(str); str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); // Writing was successful, delete the backup file if there is one. //删除备份的文件 mBackupFile.delete(); mcr.setDiskWriteResult(true); }