SharePreference原理及跨进程数据共享的问题
SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。为什么这么说呢?因为SharedPreferences的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在同步互斥问题,后面会详细分析这几个问题。本文源码基于SKD-25版本分析。
SharedPreferences的实现原理之:持久化数据的加载
首先,从基本使用简单看下SharedPreferences的实现原理:
[代码]java代码:
<span style= "font-size: 9pt;" > //创建 mSharedPreferences = context.getSharedPreferences( "test" , Context.MODE_PRIVATE);</span> //存取 SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.putInt(key1, value1); editor.putString(key2,value2); editor.putBoolean(key3,value3); editor.apply();<br> //获取 editor.getInt(key, default ); |
context.getSharedPreferences其实就是简单的调用ContextImpl的getSharedPreferences,具体实现如下
[代码]java代码:
@Override public SharedPreferences getSharedPreferences(String name, int mode) { // 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" ; } } File file; synchronized (ContextImpl. class ) { if (mSharedPrefsPaths == null ) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null ) { file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); } |
校验与SharePreferences对应的xml文件是否存在。若xml文件都不存在,那么还需要创建xml文件。最后调用了 getSharedPreferences(file, mode)。(与SharePreferences对应的xml文件位置一般都在/data/data/包名/shared_prefs目录下,后缀一定是.xml)
[代码]java代码:
@Override public SharedPreferences getSharedPreferences(File file, int mode) { checkMode(mode); SharedPreferencesImpl sp; synchronized (ContextImpl. class ) { final ArrayMap<file, sharedpreferencesimpl= "" > cache = getSharedPreferencesCacheLocked(); sp = cache.get(file); if (sp == null ) { sp = new SharedPreferencesImpl(file, mode); cache.put(file, 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(); } return sp; }</file,> |
[代码]java代码:
private ArrayMap<file, sharedpreferencesimpl= "" > getSharedPreferencesCacheLocked() { if (sSharedPrefsCache == null ) { sSharedPrefsCache = new ArrayMap<>(); } final String packageName = getPackageName(); ArrayMap<file, sharedpreferencesimpl= "" > packagePrefs = sSharedPrefsCache.get(packageName); if (packagePrefs == null ) { packagePrefs = new ArrayMap<>(); sSharedPrefsCache.put(packageName, packagePrefs); } return packagePrefs; }</file,></file,> |
getSharedPreferences()方法中调用getSharedPreferencesCacheLocked()方法获取ArrayMap<File, SharedPreferencesImpl>对象。首次创建时sp==null,执行sp = new SharedPreferencesImpl(file, mode);
[代码]java代码:
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false ; mMap = null ; startLoadFromDisk(); } |
代码很简单,继续调用startLoadFromDisk()方法。
[代码]java代码:
代码中我们发现启动了一个新线程加载数据。
[代码]java代码:
private void loadFromDisk() { synchronized (SharedPreferencesImpl. this ) { if (mLoaded) { return ; } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } // Debugging if (mFile.exists() && !mFile.canRead()) { Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission" ); } 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); } catch (XmlPullParserException | IOException e) { Log.w(TAG, "getSharedPreferences" , e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { /* ignore */ } synchronized (SharedPreferencesImpl. this ) { mLoaded = true ; if (map != null ) { mMap = map; mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } notifyAll(); } } |
可以看到其实就是直接使用xml解析工具XmlUtils,读取xml文件,读取完成之后,xml中的配置项都会被加载到内存,再次访问的时候,其实访问的是内存缓存。同时加载完成后回调用notifyAll()方法,通知其他等待线程。
SharedPreferences的实现原理之:持久化数据的更新
通常更新SharedPreferences的时候是首先获取一个SharedPreferences.Editor,利用它缓存一批操作,之后当做事务提交,有点类似于数据库的批量更新。Editor是一个接口,这里的实现是一个EditorImpl对象,它首先批量预处理更新操作,之后再提交更新,在提交事务的时候有两种方式,一种是apply,另一种commit,两者的区别在于:何时将数据持久化到xml文件,前者是异步的,后者是同步的。Google推荐使用前一种,因为,就单进程而言,只要保证内存缓存正确就能保证运行时数据的正确性,而持久化,不必太及时,这种手段在Android中使用还是很常见的,比如权限的更新也是这样,况且,Google并不希望SharePreferences用于多进程,因为不安全。看一下apply与commit的区别
[代码]java代码:
public final class EditorImpl implements Editor { private final Map<string, object= "" > mModified = Maps.newHashMap(); private boolean mClear = false ;<string><string> ....... 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); } }; <span style= "white-space:pre" > </span> //延迟写入到XML文件中 SharedPreferencesImpl. this .enqueueDiskWrite(mcr, 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); } // Returns true if any changes were made 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()) { 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; } 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; } private void notifyListeners( final MemoryCommitResult mcr) { if (mcr.listeners == null || mcr.keysModified == null || mcr.keysModified.size() == 0 ) { return ; } if (Looper.myLooper() == Looper.getMainLooper()) { for ( int i = mcr.keysModified.size() - 1 ; i >= 0 ; i--) { final String key = mcr.keysModified.get(i); for (OnSharedPreferenceChangeListener listener : mcr.listeners) { if (listener != null ) { listener.onSharedPreferenceChanged(SharedPreferencesImpl. this , key); } } } } else { // Run this function on the main thread. ActivityThread.sMainThreadHandler.post( new Runnable() { public void run() { notifyListeners(mcr); } }); } } }</string,></onsharedpreferencechangelistener></string></string,></string></string></string,> |
从上面可以看出两者最后都是先调用commitToMemory,将更改提交到内存,在这一点上两者是一致的,之后又都调用了enqueueDiskWrite进行数据持久化任务,不过commit函数一般会在当前线程直接写文件。而apply则提交一个事务到线程池中。
[代码]java代码:
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 ); // 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); } |
不过如果有线程在写文件,那么就不能直接写,这个时候就跟apply函数一致了,但是,如果直观说两者的区别的话,直接说commit同步,而apply异步应该也是没有多大问题的。
SharePreferences多进程使用问题
SharePreferences在新建的有个mode参数,可以指定它的加载模式,MODE_MULTI_PROCESS是Google提供的一个多进程模式,但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的。
[代码]java代码:
@Override public SharedPreferences getSharedPreferences(File file, int mode) { <span style= "white-space:pre" > </span>......<file, sharedpreferencesimpl= "" > 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(); } ...... }</file,> |
也就是说MODE_MULTI_PROCESS只是个鸡肋Flag,对于多进程的支持几乎为0。
总结
SharePreferences是Android基于xml实现的一种数据持久化手段
SharePreferences不支持多进程
SharePreferences的commit与apply一个是同步一个是异步(大部分场景下)
不要使用SharePreferences存储太大的数据
共同学习,写下你的评论
评论加载中...
作者其他优质文章