因为在本身练手的App项目中使用了SharedPreferences技术,因此对其进行必定探究。java
首先咱们先总结一下,Android的数据持久化方式:SharedPrefences、SQLite、文件储存、ContentProvider、网络储存。其他四种,留后再进一步探究。android
老规矩看看类注释是怎么介绍的缓存
由Context.getSharedPreferences方法放回的,能够访问和修改参数数据的接口。对于任一一组数据,都有一个全部客户机共享的该类实例。对参数数据的修改,必须经过Editor对象,该对象确保了参数数值的一致性和控制客户端将数值提交到储存。由各类get方法获得的对象必须是不可变得对象。
该类保证了强一致性,可是不支持跨进程使用。安全
从这段注释中,咱们不难发现:SharedPreferences须要经过Context建立,该类与Editor对象密切相关,在应用内能够数据共享。网络
咱们在SharedPreferences类中往下寻找,就找到Editor接口并发
/** * Interface used for modifying values in a {@link SharedPreferences} * object. All changes you make in an editor are batched, and not copied * back to the original {@link SharedPreferences} until you call {@link #commit} * or {@link #apply} */ public interface Editor { Editor putString(String key, @Nullable String value); Editor putLong(String key, long value); Editor putFloat(String key, float value); Editor putBoolean(String key, boolean value); Editor remove(String key); Editor clear(); boolean commit(); void apply(); }
从这个Editor接口中,咱们能够获得几个信息。首先SharedPreferences只能储存4类数据,String,Long,Float,Boolean;其次SharedPreferences是使用key-value键值对的方式进行储存的;最后,有两种提交方式apply(),commit()。app
apply()和commit()的区别:异步
ContextImpl.getSharePreferences() 该类在AS上不显示。根据当前应用名称获取ArrayMap(存储sp容器),并根据文件名获取SharedPreferencesImpl对象(实现SharedPreferences接口)。
SharedPreferences
对象,也就是说,屡次调用getSharedPreferences
方法并不会对性能形成多大影响,由于又缓存机制对象的建立过程是线程安全的,由于使用了
synchronize`关键字mode
使用了Context.MODE_MULTI_PROCESS
,那么将会调用sp.startReloadIfChangedUnexpectedly()
方法,在startReloadIfChangedUnexpectedly
方法中,会判断是否由其余进程修改过这个文件,若是有,会从新从磁盘中读取文件加载数据class ContextImpl extends Context { //静态存储类,缓存全部应用的SP容器,该容器key对应应用名称,value则为每一个应用存储全部sp的容器 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache; @Override public SharedPreferences getSharedPreferences(String name, int mode) { ...... // 根据 名字获取相对应的文件名。 // 若是没有则直接新建一个 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); } @Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { // 从ArrayMap中获取到应用储存的value final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 从当前的Map中获取一个,若是没有则直接新建一个而且放回 sp = cache.get(file); if (sp == null) { checkMode(mode); if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) { if (isCredentialProtectedStorage() && !getSystemService(UserManager.class) .isUserUnlockingOrUnlocked(UserHandle.myUserId())) { throw new IllegalStateException("SharedPreferences in credential encrypted " + "storage are not available until after user is unlocked"); } } 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; } }
sharedPreferences的对象实例 sharedPreferencesImpl类
用SP存储的静态变量键值数据在内存中是一直存在(文件存储),经过SharedPreferencesImpl构造器开启一个线程对文件进行读取。SharedPreferencesImpl主要是对文件进行操做。ide
file
以及mode
分别保存在mFile
以及mMode
中.bak
备份文件,当用户写入失败的时候会根据这个备份文件进行恢复工做mMap
初始化为null
startLoadFromDisk()
方法加载数据startLoadFromDisk()
getSharedPreferences
方法的时候,会从磁盘中加载数据,而数据的加载时经过开启一个子线程调用loadFromDisk
方法进行异步读取的mMap
中mStatTimestamp
以及mStatSize
中(保存这两个值有什么用呢?咱们在分析getSharedPreferences
方法时说过,若是有其余进程修改了文件,而且mode
为MODE_MULTI_PROCESS
,将会判断从新加载文件。如何判断文件是否被其余进程修改过,没错,根据文件修改时间以及文件大小便可知道)notifyAll()
方法通知唤醒其余等待线程,数据已经加载完毕SharedPreferencesImpl(File file, int mode) { mFile = file; // 文件备份 mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; // 开启一个线程读取文件 startLoadFromDisk(); } private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (mLock) { //若是文件已经加载完毕直接返回 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<String, Object> map = null; StructStat stat = null; Throwable thrown = null; try { stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { //读取文件 BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); //使用XmlUtils工具类读取xml文件数据 map = (Map<String, Object>) XmlUtils.readMapXml(str); } catch (Exception e) { Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { // An errno exception means the stat failed. Treat as empty/non-existing by // ignoring. } catch (Throwable t) { thrown = t; } synchronized (mLock) { //修改文件加载完成标志 mLoaded = true; mThrowable = thrown; // It's important that we always signal waiters, even if we'll make // them fail with an exception. The try-finally is pretty wide, but // better safe than sorry. try { if (thrown == null) { if (map != null) { mMap = map;/若是有数据,将数据已经赋值给类成员变量mMap(将从文件读取的数据赋值给mMap) mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { //没有数据直接建立一个hashmap对象 mMap = new HashMap<>(); } } // In case of a thrown exception, we retain the old map. That allows // any open editors to commit and store updates. } catch (Throwable t) { mThrowable = t; } finally { //此处很是关键是为了通知其余线程文件已读取完毕,大家能够执行读/写操做了 mLock.notifyAll(); } } }
sharedPreferencesImpl重写了SharedPreferences的方法,基本上结构都一致,这里拿String类举例。工具
getXxx
方法是线程安全的,由于使用了synchronize
关键字getXxx
方法是直接操做内存的,直接从内存中的mMap
中根据传入的key
读取value
getXxx
方法有可能会卡在awaitLoadedLocked
方法,从而致使线程阻塞等待(何时会出现这种阻塞现象呢?前面咱们分析过,第一次调用getSharedPreferences
方法时,会建立一个线程去异步加载数据,那么假如在调用完getSharedPreferences
方法以后当即调用getXxx
方法,此时的mLoaded
颇有可能为false
,这就会致使awaiteLoadedLocked
方法阻塞等待,直到loadFromDisk
方法加载完数据而且调用notifyAll
来唤醒全部等待线程)public String getString(String key, @Nullable String defValue) { synchronized (mLock) { //此处会阻塞当前线程,直到文件加载完毕,第一次使用的时候可能会阻塞主线程 awaitLoadedLocked(); //从类成员变量mMap中直接读取数据,没有直接返回默认值 String v = (String)mMap.get(key); return v != null ? v : defValue; } }
3.1 获取editor对象
public Editor edit() { synchronized (mLock) { awaitLoadedLocked();//若是文件未加载完毕,会一直阻塞当前线程,直到加载完成为止 } return new EditorImpl(); }
3.2 对数据进行修改
SharedPreferences
的写操做是线程安全的,由于使用了synchronize
关键字mModified
中,而并非直接对SharedPreferences.mMap
进行操做(mModified
会在commit/apply
方法中起到同步内存SharedPreferences.mMap
以及磁盘数据的做用)public final class EditorImpl implements Editor { //先存储在Editor的map中 private final Map<String, Object> mModified = new HashMap<>(); //各类修改方法依旧相似 public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } ... }
3.3 提交数据
public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } //第一步 commitToMemory方法能够理解为对SP中的mMap对象同步到最新数据状态 //mcr对象就是最终须要写入磁盘的mMap MemoryCommitResult mcr = commitToMemory(); //第二步 写文件;注意第二个参数为null,写文件操做会运行在当前线程 //当前只有一个commit线程时。会直接在当前线程执行 //若是是UI线程 则可能会形成阻塞 //会判断有无 备份文件,必定要有备份文件,防止写入错误 //将mcr写入磁盘 SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { if (DEBUG) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms"); } } //第三步 通知监听器数据改变 notifyListeners(mcr); //第四步 返回写操做状态 return mcr.writeToDiskResult; }
public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } if (DEBUG && mcr.wasWritten) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms"); } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; 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); }
SharedPreferences 不要存放特别大的数据
apply
或者commit
,都会把所有的数据一次性写入磁盘, 因此 SP 文件不该该过大, 影响总体性能SharedPreference
的文件存储性能与文件大小相关,咱们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来getXxx
都是从内存中取的数据,数据来源于SharedPreferences.mMap
apply
同步回写(commitToMemory()
)内存SharedPreferences.mMap
,而后把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。apply
不须要等待写入磁盘完成,而是立刻返回ommit
同步回写(commitToMemory()
)内存SharedPreferences.mMap
,而后若是mDiskWritesInFlight
(此时须要将数据写入磁盘,但还未处理或未处理完成的次数)的值等于1,那么直接在调用commit
的线程执行回写磁盘的操做,不然把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。commit
会阻塞调用线程,知道写入磁盘完成才返回MODE_MULTI_PROCESS
是在每次getSharedPreferences
时检查磁盘上配置文件上次修改时间和文件大小,一旦全部修改则会从新从磁盘加载文件,因此并不能保证多进程数据的实时同步屡次edit屡次commit/apply