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找到,因为我没看过那些,所以自己搜吧,我是直接看的公司的。
理解,分析
- Android SharedPreferences源码分析
- Android源码分析之SharedPreferences
- Android源码分析之SharedPreferences
- Android源码分析之SharedPreferences
- Android源码分析之SharedPreferences
- Android面试题-SharedPreferences源码分析
- SharedPreferences 源码分析
- 源码分析:SharedPreferences实现
- android api16分析 SharedPreferences
- Android SharedPreferences 分析
- android api分析17 SharedPreferences
- Android SharedPreferences的使用分析
- 源码分析多进程下的SharedPreferences
- Android开发-数据存储SharedPreferences工具类、Set<String>保存问题、源码分析
- Android-SharedPreferences源码学习与最佳实践
- Android-SharedPreferences源码学习与最佳实践
- Android本地存储之SharedPreferences源码解析
- Android 数据分析系列一:sharedPreferences
- mac 启用与关闭root账户
- 【开发模式】 单例模式
- 利用python-opencv进行颜色标定
- PAT乙级.1008. 数组元素循环右移问题 (20)
- 三种创建servlet的方式(tomcat)
- Android SharedPreferences源码分析
- 350. Intersection of Two Arrays II
- Material Design系列,自定义Behavior实现Android知乎首页
- Ubuntu16下面使用eclipse新建项目工程卡死
- CV | Interactive Image Segmentation 图割法分割图像
- 字典排序
- POJ1390 Blocks 区间DP
- 输出100-200之间的全部素数
- js积累