Android SharedPreferences源码分析

我们经常使用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方法:


@Override

public 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文件名进行了组装:


@Override
public 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找到,因为我没看过那些,所以自己搜吧,我是直接看的公司的。

理解,分析

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/xxxzhi/article/details/51946746
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞