Android 持久化技术(一)之SharedPreferences
由于在自己练手的App项目中使用了SharedPreferences技术,所以对其进行一定探究。
首先我们先总结一下,Android的数据持久化方式:SharedPrefences、SQLite、文件储存、ContentProvider、网络储存。其余四种,留后再进一步探究。
SharedPreferences
老规矩看看类注释是怎么介绍的
由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()。
- 从前两个信息不难推出,SharedPreferences只是一种轻量级的储存方式,所以最好不要使用这个去储存一些过大,或者复杂数据类型的数据。
-
apply()和commit()的区别:
- 从上面可以看出,commit是有返回值的,而apply是没有放回值的。当我们需要知道一个数据是否修改成功时,就需要调用commit方法。
- commit是直接将修改的数据同步提交到硬件硬盘,会阻塞调用它的线程。apply则是将修改数据原子提交到内存,而后异步真正提交到硬件硬盘,所以后面调用apply会直接覆盖前面的数据,使用apply会提高效率。
- apply方法不会提示任何失败的提示。由于在一个进程中,sharedPreference是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用apply,当然需要确保提交成功且有后续操作的话,还是需要用commit的。
- 此外SharedPreferences有一个接口,可以实现对键值变化的监听。
- 如果需要储存复杂数据(图片或对象)时,就需要对将其转化为Base64编码。
如何使用SharedPreferences?
1、获取SharedPreferences
ContextImpl.getSharePreferences() 该类在AS上不显示。根据当前应用名称获取ArrayMap(存储sp容器),并根据文件名获取SharedPreferencesImpl对象(实现SharedPreferences接口)。
- 缓存未命中, 才构造
SharedPreferences
对象,也就是说,多次调用getSharedPreferences
方法并不会对性能造成多大影响,因为又缓存机制 - SharedPreferences
对象的创建过程是线程安全的,因为使用了
synchronize`关键字 - 如果命中了缓存,并且参数
mode
使用了Context.MODE_MULTI_PROCESS
,那么将会调用sp.startReloadIfChangedUnexpectedly()
方法,在startReloadIfChangedUnexpectedly
方法中,会判断是否由其他进程修改过这个文件,如果有,会重新从磁盘中读取文件加载数据
class ContextImpl extends Context { //静态存储类,缓存所有应用的SP容器,该容器key对应应用名称,value则为每个应用存储所有sp的容器 private static ArrayMap> 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 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主要是对文件进行操作。
- 将传进来的参数
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 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) 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(); } } }
2、获取数据
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、提交数据
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 mModified = new HashMap<>(); //各种修改方法依旧类似 public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } ... }
3.3 提交数据
- commit()方法
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; }
- apply和commit主要区别就是apply的写文件操作会在一个线程中执行,不会阻塞UI线程
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是线程安全的,但是不是进程安全的。
-
SharedPreferences 不要存放特别大的数据
- 第一次加载时,需要将整个SP加载到内存当中,如果过于大,会导致阻塞,甚至会导致 ANR
- 每次
apply
或者commit
,都会把全部的数据一次性写入磁盘, 所以 SP 文件不应该过大, 影响整体性能 -
SharedPreference
的文件存储性能与文件大小相关,我们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来
- 不适宜存储JSON等特殊符号很多的数据
- 所有的
getXxx
都是从内存中取的数据,数据来源于SharedPreferences.mMap
-
apply
同步回写(commitToMemory()
)内存SharedPreferences.mMap
,然后把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。apply
不需要等待写入磁盘完成,而是马上返回 -
ommit
同步回写(commitToMemory()
)内存SharedPreferences.mMap
,然后如果mDiskWritesInFlight
(此时需要将数据写入磁盘,但还未处理或未处理完成的次数)的值等于1,那么直接在调用commit
的线程执行回写磁盘的操作,否则把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。commit
会阻塞调用线程,知道写入磁盘完成才返回 -
MODE_MULTI_PROCESS
是在每次getSharedPreferences
时检查磁盘上配置文件上次修改时间和文件大小,一旦所有修改则会重新从磁盘加载文件,所以并不能保证多进程数据的实时同步 -
多次edit多次commit/apply
- 多次edit会产生很多editor对象
- 多次apply和commit App的stop方法会等待写完为止
更多相关文章
- Android的数据存储(二)——SQLite数据库
- android/java 计算大文件的sha1值
- android数据储存之应用安装位置
- Android — Room 数据库跳跃式升级(Migration)
- android学习笔记之多线程学习
- Android开发的文件格式概述