Android SharedPreferences源码分析

来源:互联网 发布:ubuntu 添加启动脚本 编辑:程序博客网 时间:2024/06/04 19:45

我们经常使用SharedPreferences保存一些简单的数据,比如Settings的数据。如果我们只是简单的使用,可能没什么问题,但是如果要用好它还是得明白它的实现方式,下面来从源码上来分析下SharedPreferences的缓存,异步读写实现,多线程,多进程访问。

SharedPreferences简介

SharedPreferences是Android提供的一种使用XML文件保存内容的机制。其内部就是通过xml写入文件的。

SharedPreferences是一个接口类,这是使用它的一个基础,我们可以通过Context的getSharedPreference来获取SharedPreferences。如下所示:

SharedPreferences sp = context.getSharedPreferences("name",Context.MODE_PRIVATE);

第一个参数表示存储的文件名,第二个表示创建文件时的模式。

它提供了getInt, getLong, getFloat,getChar, getString 来读取int, long, float, char, String类型的数据,并且提供了一个Editor接口来用于写入对应的数据类型。Android在API14时又提供了Set类型的数据写入读取。下面看一段简单实用示例:

SharedPreferences sp = context.getSharedPreferences("name",Context.MODE_PRIVATE);int val1 = sp.getInt("val1",0);SharedPreferences.Editor editor = sp.edit();editor.putInt("val1", val1+1);editor.apply();// editor.commit(); 跟apply方法是一样的,但是apply是异步写入。

下面就针对上面这段代码流程,分析一下SharedPreferences的源码。

获取SharedPreferences

我们通过context.getSharedPreferences方法获取SharedPreferences,而Context得真正实现者是ContextImpl,所以看看ContextImpl里面的getSharedPreferences方法:

@Overridepublic SharedPreferences getSharedPreferences(String name, int mode) {   SharedPreferencesImpl sp;   synchronized (ContextImpl.class) {       if (sSharedPrefs == null) {           sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();       }       final String packageName = getPackageName();       ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);       if (packagePrefs == null) {           packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();           sSharedPrefs.put(packageName, packagePrefs);       }       // At least one application in the world actually passes in a null       // name.  This happened to work because when we generated the file name       // we would stringify it to "null.xml".  Nice.       if (mPackageInfo.getApplicationInfo().targetSdkVersion <               Build.VERSION_CODES.KITKAT) {           if (name == null) {               name = "null";           }       }       sp = packagePrefs.get(name);       if (sp == null) {           File prefsFile = getSharedPrefsFile(name); //根据文件名,获取存储的文件           sp = new SharedPreferencesImpl(prefsFile, mode);           packagePrefs.put(name, sp);           return 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();       //在多进程模式或者目标sdk版本在HONEYCOMB以下版本每次读取缓存了的sp,Android会检查xml文件是否已经被重写了。   }   return sp;}

这段代码首先判断sSharedPrefs是否为空,如果为空则给他初始化。sSharedPrefs是一个用来缓存SharedPreferences的ArrayMap,它的key为包名,它的value为ArrayMap,这个ArrayMap保存的键值对是SharedPreferences文件名和对应的SharedPreferencesImpl(是SharedPreferences的实现类)。如果SharedPreferencesImpl已经存在,它会直接返回已经存在的SharedPreferencesImpl。如果是在多进程模式下,或者目标版本低于HONEYCOMB的时候,会检查是否需要重新从磁盘中加载文件。但是需要说的是MODE_MULTI_PROCESS模式已经被deprecated了,官方建议使用ContentProvider来处理多进程访问,其实我们项目中就遇到这么一个问题导致了一个BUG。

在重新创建SharedPreferencesImpl的时候,getSharedPreferences会调用getSharedPrefsFile来获取存储的xml文件,这个函数对xml文件名进行了组装:

@Overridepublic File getSharedPrefsFile(String name) {   return makeFilename(getPreferencesDir(), name + ".xml");}

通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀。Android没有提供直接访问shared_prefs目录的API,getPreferencesDir是一个私有类,我们如果想要直接访问这个目录,可以通过下面这段代码访问:

String sharedPrefsDir = context.getCacheDir().getParent().getAbsolutePath()+"/shared_prefs";

SharedPreferencesImpl构造函数

从上面的代码已经知道SharedPreferences具体的实现者是SharedPreferencesImpl。我们都知道Android的SharedPreferences对XML操作是使用DOM方式解析的(一开始就把整个XML给读取出来)。在SharedpreferencesImpl源码中,它的构造函数里面它就把XML文件给读取出来了:

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

它的构造函数中startLoadFromDisk就是将xml给读取出来的。下面看看startLoadFromDisk:

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

它使用了一个异步线程来读取xml,最终实现的函数是loadFromDiskLocked(),在读取的时候它必须获取SharedPreferencesImpl.this的锁:

private void loadFromDiskLocked() {...   Map map = null;   StructStat stat = null;   try {       stat = Os.stat(mFile.getPath());       if (mFile.canRead()) {           BufferedInputStream str = null;           try {               str = new BufferedInputStream(                       new FileInputStream(mFile), 16*1024);               map = XmlUtils.readMapXml(str);...  mLoaded = true;  if (map != null) {      mMap = map;      mStatTimestamp = stat.st_mtime;      mStatSize = stat.st_size;  } else {      mMap = new HashMap<String, Object>();  }

这个函数里面省略了一些代码,想看全部的,可以直接去SharedPreferencesImpl文件看。这个函数最终调用了XmlUtils.readMapXml来调用,读取整个xml的内容,放到mMap当中。

读取key对应的值

SharedPreferencesImpl的读取是非常简单的,因为在构造函数当中就已经读取整个xml文件的内容到mMap当中了,所以再次读取的时候直接从mMap当中读取就好了,但是得注意同步的问题:

public int getInt(String key, int defValue) {   synchronized (this) {       awaitLoadedLocked();       Integer v = (Integer)mMap.get(key);       return v != null ? v : defValue;   }}

函数awaitLoadedLocked就是等待读取文件完成。因为如果读取具体元素的时候,读取文件线程却没有完成,那么必须等待文件读取完成,不然结果肯定会乱。

写入

SharedPreferences的写入是通过Editor来实现的,Editor接口在SharedPreferencesImpl具体实现是EditorImpl,在这看看它的源码:

public final class EditorImpl implements Editor {   private final Map<String, Object> mModified = Maps.newHashMap();   private boolean mClear = false;   public Editor putInt(String key, int value) {       synchronized (this) {           mModified.put(key, value);           return this;       }   }   //... 省略了其他类型的value操作,和clear,remove函数。   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); // enqueueDiskWrite会调用异步线程执行postWriteRunnable。       // Okay to notify the listeners before it's hit disk       // because the listeners should always get the same       // SharedPreferences instance back, which has the       // changes reflected in memory.       notifyListeners(mcr);   }....省略了commitToMemory   public boolean commit() {       MemoryCommitResult mcr = commitToMemory();       SharedPreferencesImpl.this.enqueueDiskWrite(           mcr, null /* sync write on this thread okay */); //第二个参数为null,enqueueDiskWrite会直接写入。       try {           mcr.writtenToDiskLatch.await();       } catch (InterruptedException e) {           return false;       }       notifyListeners(mcr);       return mcr.writeToDiskResult;   }... 省略了notifyListeners}

从源码上面可以看出,首先使用put写入的时候,只是写入到一个mModified里面,但是实际上还没写入SharedPreferencesImpl的mMap当中,更没有写入磁盘,只有当调用commit或者apply函数的时候才会开始写入。而apply是异步写入,而commit是在当前线程直接写入。commit在enqueueDiskWrite的第二个参数传入null,看看enqueueDiskWrite的实现:

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();               }           }       };   final boolean isFromSyncCommit = (postWriteRunnable == null); //如果postWriteRunnable就同步写入   // Typical #commit() path with fewer allocations, doing a write on   // the current thread.   if (isFromSyncCommit) {       boolean wasEmpty = false;       synchronized (SharedPreferencesImpl.this) {           wasEmpty = mDiskWritesInFlight == 1;       }       if (wasEmpty) {           writeToDiskRunnable.run();           return;       }   }   QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);}

但是需要指出的是,两种方式首先都会先使用commitTomemory函数将修改的内容写入到SharedPreferencesImpl当中。看看commitToMemory的实现:

private MemoryCommitResult commitToMemory() {   MemoryCommitResult mcr = new MemoryCommitResult();   synchronized (SharedPreferencesImpl.this) {       // We optimistically don't make a deep copy until       // a memory commit comes in when we're already       // writing to disk.       if (mDiskWritesInFlight > 0) {           // We can't modify our mMap as a currently           // in-flight write owns it.  Clone it before           // modifying it.           // noinspection unchecked           mMap = new HashMap<String, Object>(mMap);       }       mcr.mapToWriteToDisk = mMap;       mDiskWritesInFlight++;       boolean hasListeners = mListeners.size() > 0;       if (hasListeners) {           mcr.keysModified = new ArrayList<String>();           mcr.listeners =                   new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());       }       synchronized (this) {           if (mClear) {               if (!mMap.isEmpty()) {                   mcr.changesMade = true;                   mMap.clear();               }               mClear = false;           }           for (Map.Entry<String, Object> e : mModified.entrySet()) {  // 在这开始将修改的内容写入到mMap当中。               String k = e.getKey();               Object v = e.getValue();               // "this" is the magic value for a removal mutation. In addition,               // setting a value to "null" for a given key is specified to be               // equivalent to calling remove on that key.               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);               }               mcr.changesMade = true;               if (hasListeners) {                   mcr.keysModified.add(k);               }           }           mModified.clear();       }   }   return mcr;}

通知修改的变化

我们可以通过下面两个函数注册监视xml文件变化的通知,在这里我直接把函数源码给顺便贴出来了,因为比较简短:

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

在前面分析了的函数commitToMemory中会返回修改的内容保存在MemoryCommitResult当中,然后使用使用notifyListener函数通知监听者。

总结

SharedPreferences从功能上面来讲就是三个部分读取(一开始异步全部读取出来,get的时候,如果没有读取完,会等待),写入,监听SharedPreferences的变化。另外Android会使用ArrayMap对SharedPreferences进行缓存,以SharedPreferences的name作为key。需要进一步理解的是关于多线程,多进程时的使用。

首先从线程方面来看,从源码上看apply是使用异步线程写入磁盘,commit是同步写入磁盘。所以我们在主线程使用的commit的时候,需要考虑是否会出现ANR问题。我们不用担心apply异步写入会出现先写入的内容,在该线程之后读取会读取不到,因为它写入内存的时候没有使用异步线程,所以在主线程最好使用apply。所有的线程读取的时候都会加SharedPreferencesImpl.this锁,editor写入内存的时候(写入SharedPreferencesImpl.this.mMap)也会加SharedPreferencesImpl.this锁,另外editor调用put,clear, remove方法的时候都会加上EditorImpl.this锁,这些是线程安全的保证,只有在commit/apply后才会写入内存(mMap, xml内容缓存的map变量)和磁盘。

另外从多进程方面来看,SharedPreferences本身提供了MODE_MULTI_PROCESS的模式,但是现在已经deprecated了,不建议使用。MODE_MULTI_PROCESS也仅仅是每次读取缓存的SharedPreferencesImpl时重写读取一次磁盘(其实效率很低,而且从源码看,并不能很好地保持同步)。所以Android建议使用ContentProvider来保持多进程的访问。有人已经实现了,可以通过google搜索multi process sharedpreferences找到,因为我没看过那些,所以自己搜吧,我是直接看的公司的。


理解,分析

0 0
原创粉丝点击