Android SharePreferences 源码分析
来源:互联网 发布:俄罗斯聊天软件下载 编辑:程序博客网 时间:2024/06/08 08:20
在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/ < package name > /shared_prefs目录下:
简单使用
SharedPreferences sharedPreferences = getSharedPreferences("ytr", Context.MODE_PRIVATE);Editor editor = sharedPreferences.edit();//获取编辑器editor.putString("name", "MyName");editor.putInt("age", 21);editor.commit();//提交修改
会在shared_pref目录下生成如下的文件,文件名为ytr.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?><map> <string name="name">MyName</string> <int name="age" value="21" /></map>
因为SharedPreferences背后是使用xml文件保存数据,getSharedPreferences(name,mode)
方法的第一个参数用于指定该文件的名称,名称不用带后缀,后缀会由Android**自动加上**。方法的第二个参数指定文件的操作模式,共有四种操作模式,
这四种模式代表的含义为:
Context.MODE_PRIVATE = 0
Context.MODE_APPEND = 32768
Context.MODE_WORLD_READABLE = 1
Context.MODE_WORLD_WRITEABLE = 2
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问.
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件,对MODE_PRIVATE来说没有区别,都是追加文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
获取SPref中的值
SharedPreferences sharedPreferences = getSharedPreferences("ytr", Context.MODE_PRIVATE);// getString()第二个参数为缺省值,如果preference中不存在该key,将返回缺省值String name = sharedPreferences.getString("name", "");int age = sharedPreferences.getInt("age", 1);
context.getSharedPreferences() 分析
从Context中获取SharedPref对象
@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) { checkMode(mode); SharedPreferencesImpl sp; synchronized (ContextImpl.class) { // 获取SharedPerf的文件集合 final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 从SP集合中取出指定的File文件 sp = cache.get(file); if (sp == null) { // 缓存中没有时,构造一个SP对象 sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } // 多进程的情况下,会进行Check,如果有其他进程修改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文件,如果需要,则重新加载 sp.startReloadIfChangedUnexpectedly(); } return sp;}
SharedPref对象的构造
// 初始化一些变量SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; // 将SP文件加载到内存中 startLoadFromDisk();}
startLoadFromDisk()
这里会启动一个新线程进行加载
private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start();}
loadFromDisk()
从Disk中获取SP文件
private void loadFromDisk() { synchronized (SharedPreferencesImpl.this) { if (mLoaded) { return; } // 如果BackupFile存在,实际读取的是备份文件 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用来获取文件信息 StructStat stat = null; try { stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16*1024); // 将XML以map的形式装载进内存 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是Spref对象的内存缓存 mMap = map; // 记录文件信息,从Linux系统获取 mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } else { // 第一次装载时,创建Map mMap = new HashMap<>(); } // 这个通知需要持有SharedPreferencesImpl.this执行 // 一般时所有的getXXX() notifyAll(); }}
这个时候XML已经被装载到内存中了,内存中的SPref映射正是mMap对象
getXXX()
然后我们分析下getXXX()方法,这里以getString()为例
@Nullablepublic String getString(String key, @Nullable String defValue) { synchronized (this) { // 此处等待加载完毕,否则一直阻塞 awaitLoadedLocked(); String v = (String)mMap.get(key); // 实际中的取值还是从内存中取得,如果没有,使用默认的值 return v != null ? v : defValue; }}
使用awaitLoadedLocked()等待加载完毕
private void awaitLoadedLocked() { if (!mLoaded) { // Raise an explicit StrictMode onReadFromDisk for this // thread, since the real read will be in a different // thread and otherwise ignored by StrictMode. BlockGuard.getThreadPolicy().onReadFromDisk(); } while (!mLoaded) { try { // 这里使用了个wait(),如果loadFromDisk()方法没有执行完毕 // 会一直阻塞,直到内存中有数据,这里明白loadFromDisk()最后有个notifyAll()的原因了吧 wait(); } catch (InterruptedException unused) { } }}
putXXX()
这里以putString()为例
// 暂存用户设置的数据,待commit,或者apply()时进行写入private final Map<String, Object> mModified = Maps.newHashMap();private boolean mClear = false;public Editor putString(String key, @Nullable String value) { synchronized (this) { // 实际上也是先将值写入内存中 mModified.put(key, value); return this; }}
这里有两个特殊的,remove()和clear()
public Editor remove(String key) { synchronized (this) { // remove是将对象自己放在HashMap中,下面会介绍如何进行处理 mModified.put(key, this); return this; }}public Editor clear() { synchronized (this) { // 写入一个clear标志,在提交时会进行check mClear = true; return this; }}
commit和apply分析
apply是异步,commit是同步,在主线程中使用commit可能会影响性能,因为同步IO操作的耗时可能会比较长,两个方法都能保证value被正确的保存到磁盘上。两者都是Editor类的方法,它们的具体实现在EditorImpl类中,我们先大体比较一下这两个函数:
public boolean commit() { // 首先提交到内存,然后决定是异步还是同步进行IO 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;}
commitToMemory() 内存同步操作
这个内部类用来处理内存中的数据和协助通知回调
// Return value from EditorImpl#commitToMemory()private static class MemoryCommitResult { public boolean changesMade; // any keys different? public List<String> keysModified; // may be null public Set<OnSharedPreferenceChangeListener> listeners; // may be null public Map<?, ?> mapToWriteToDisk; public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); public volatile boolean writeToDiskResult = false; public void setDiskWriteResult(boolean result) { writeToDiskResult = result; writtenToDiskLatch.countDown(); }}
// Returns true if any changes were madeprivate MemoryCommitResult commitToMemory() { // 构造一个MemoryCommitResult 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. // mDiskWritesInFlight这个变量用来表示还有多少待写入Disk的请求 // 我们使用commitToMemory()修改一个值,这个变量会加1 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中实际需要写入文件的Map和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) { // 调用clear()的情况 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. // v==this是remove()一个key的情况 if (v == this || v == null) { if (!mMap.containsKey(k)) { continue; } mMap.remove(k); } else { // 对putXXX()来的值进行修改 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) { // 将所有需要回调Listener的字段加入ArrayList mcr.keysModified.add(k); } } // 清空暂存缓存 mModified.clear(); } } return mcr;}
然后是调用执行操作
/** * Enqueue an already-committed-to-memory result to be written * to disk. * * They will be written to disk one-at-a-time in the order * that they're enqueued. * * @param postWriteRunnable if non-null, we're being called * from apply() and this is the runnable to run after * the write proceeds. if null (from a regular commit()), * then we're allowed to do this disk write on the main * thread (which in addition to reducing allocations and * creating a background thread, this has the advantage that * we catch them in userdebug StrictMode reports to convert * them where possible to apply() ...) */private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { // 构造一个Runnble对象,这里将写入文件的操作封装 final Runnable writeToDiskRunnable = new Runnable() { public void run() { synchronized (mWritingToDiskLock) { writeToFile(mcr); } synchronized (SharedPreferencesImpl.this) { //每次写入文件成功,会将mDiskWritesInFlight这个变量-1 mDiskWritesInFlight--; } // 如果在写入文件完毕后有回调,在这里执行 // commit()时这个变量会为null 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; // 需要同步IO的情况 synchronized (SharedPreferencesImpl.this) { // 是否存在需要写入操作 wasEmpty = mDiskWritesInFlight == 1; } // 直接执行 if (wasEmpty) { writeToDiskRunnable.run(); return; } } // 异步的情况提交线程池进行执行 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);}
writeToFile() 实际的IO操作
// Note: must hold mWritingToDiskLockprivate void writeToFile(MemoryCommitResult mcr) { // Rename the current file so it may be used as a backup during the next read if (mFile.exists()) { // check changesMade 标志,在commitToMem()时如果字段需要修改,将写为true if (!mcr.changesMade) { // If the file already exists, but no changes were // made to the underlying map, it's wasteful to // re-write the file. Return as if we wrote it // out. mcr.setDiskWriteResult(true); return; } // 备份文件不存在,会将这个XML进行备份 if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(false); return; } } else { // 删除文件,便于下次写入 mFile.delete(); } } // Attempt to write the file, delete the backup and return true as atomically as // possible. If any exception occurs, delete the new file; next time we will restore // from the backup. try { // 使用Stream修改文件 FileOutputStream str = createFileOutputStream(mFile); if (str == null) { mcr.setDiskWriteResult(false); return; } // 将Map写成Xml XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); FileUtils.sync(str); str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); try { // 这里获取文件信息 final StructStat stat = Os.stat(mFile.getPath()); synchronized (this) { mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } } catch (ErrnoException e) { // Do nothing } // Writing was successful, delete the backup file if there is one. // 写入成功时,清除备份文件 mBackupFile.delete(); // 设置标志位为写入成功 mcr.setDiskWriteResult(true); return; } catch (XmlPullParserException e) { Log.w(TAG, "writeToFile: Got exception:", e); } catch (IOException e) { Log.w(TAG, "writeToFile: Got exception:", e); } // Clean up an unsuccessfully written file // 这里时处理异常情况,清除没有写成功的文件 if (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false);}
public void setDiskWriteResult(boolean result) { writeToDiskResult = result; // 释放闭锁, 所有调用await()的地方会继续执行 writtenToDiskLatch.countDown();}
notifyListeners()通知改变字段的Listener回调
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 { // 使用Handler post到主线程执行 // Run this function on the main thread. ActivityThread.sMainThreadHandler.post(new Runnable() { public void run() { notifyListeners(mcr); } }); }}
apply()方法
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); } }; // 准备执行IO,这时postWriteRunnable不为null,所以会提交给线程池执行 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);}
OnSharedPreferenceChangeListener
/** * Interface definition for a callback to be invoked when a shared * preference is changed. */public interface OnSharedPreferenceChangeListener { /** * Called when a shared preference is changed, added, or removed. This * may be called even if a preference is set to its existing value. * * <p>This callback will be run on your main thread. * * @param sharedPreferences The {@link SharedPreferences} that received * the change. * @param key The key of the preference that was changed, added, or * removed. */ void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);}
通过Reg和unReg注册到WeakHashMap中
// WeakReference组成的HashMap,不会造成内存泄漏private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(this) { mListeners.put(listener, mContent); }}public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(this) { mListeners.remove(listener); }}
对SharedPref的优化建议
SP的瓶颈分析
IO瓶颈
IO瓶颈造成SP性能差是最大的原因,解决了IO瓶颈,80%的性能问题就解决了。
SP的IO瓶颈包括读取数据到内存与数据写入磁盘两部分。
读取数据到内存有两个场景会触发:
- SP文件没有被加载到内存时,调用getSharedPreferences方法会初始化文件并读入内存。
- 版本低于android_H或使用了MULTI_PROCESS标志时,每次调用getSharedPreferences方法时都会读入。
我们可以优化的就是它了。每次加载数据到内存太过影响效率。
H以下版本留存率已经很低了,基本可以忽略。
对于MULTI_PROCESS,可以采用ContentProvider等其他方式,效率更好,而且可避免SP数据丢失的情况。
数据写入磁盘也有两个场景会触发:
Editor的commit方法,每次执行时同步写入磁盘。
Editor的apply方法,每次执行时在单线程池中加入写入磁盘Task,异步写入。
commit和apply的方法区别在于同步写入和异步写入,以及是否需要返回值。
在不需要返回值的情况下,使用apply方法可以极大的提高性能。
同时,多个写入操作可以合并为一个commit/apply,将多个写入操作合并后也能提高IO性能。
锁瓶颈
SP的get操作,会锁定SharedPreferences对象,互斥其他操作。
SP的put操作,getEditor及commitToMemory会锁定SharedPreferences对象,put操作会锁定Editor对象,写入磁盘更会锁定一个写入锁。
由于锁的缘故,SP操作并发时,耗时会徒增。减少锁耗时,是另一个优化点。
由于读写操作的锁均是针对SP实例对象的,将数据拆分到不同的sp文件中,便是减少锁耗时的直接方案。
降低单文件访问频率,多文件均摊访问,以减少锁耗时。
用开发机进行了简单的性能测试(写入均使用apply,若使用commit则多线程耗时更高):
读写同一文件,10个线程每个读写10次数据:
耗时80-130ms
读写10个文件,每个文件由1个线程读写10次数据:
耗时30-70ms
减少单个SPref文件的大小,将数据均摊到每个文件中,是提高SP访问性能的重要一步。
对SP操作的不当封装
我们采用ContentProvider方案支持跨进程访问,并对所有SP操作均套上了ContentProvider进行访问。
随着项目越来越庞大,通过ContentProvider访问造成的耗时性能也成了问题。
对ContentProvider操作SP测试,耗时是直接操作SP的4倍左右。
所以,最近项目中进行了SP的处理,对于不需要跨进程的SP操作去掉了ContentProvider,尽可能减少无谓耗时。
总结
尽量不要直接调用SharedPreferences进行读写操作。
若直接调用getSharedPreferences(fileName,mode).edit().putString(key,value),则对数据的操作直接耦合了fileName和key,后续想调整file和key会比较困难。
可以考虑封装一下,譬如:
public void saveUserId(){
getSharedPreferences(fileName,mode).edit().putString(“user_id”,value);
}
这样做可以直接对数据访问,而与fileName与key解耦,后续拆分与调整时会很方便。将SP作为耗时操作对待,尽量减少无谓的调用。
譬如以下代码,SP读一次即可:
if(sp.getUserId()>0){
int id=sp.getUserId();
…
}
- Android SharePreferences 源码分析
- SharePreferences源码分析
- Android SharePreferences源码解析
- SharePreferences源码分析(SharedPreferencesImpl)
- SharePreferences源码分析(SharedPreferencesImpl)
- Android应用Preference相关及源码浅析(SharePreferences篇)
- Android初学者之SharePreferences
- Android之SharePreferences
- Android存储之SharePreferences
- 【android】2、SharePreferences
- SharePreferences源码分析(commit与apply的区别以及原理)
- SharePreferences源码分析(commit与apply的区别以及原理)
- SharePreferences源码分析(commit与apply的区别以及原理)
- SharePreferences
- SharePreferences
- SharePreferences
- sharePreferences
- Sharepreferences
- CTFCrypto练习之RSA算法
- 使用canvas实现行走的小人动画
- 25. Reverse Nodes in k-Group
- git 创建服务端库Shell脚本
- 文章标题
- Android SharePreferences 源码分析
- 刘汝佳的算法竞赛入门经典(第2版) 习题解答
- #循环中的continue 和 break
- zoj2099
- java反射机制
- 简单的js验证码
- 关于maven本地仓库新建项目报错的解决
- 想要学习Linux技术,先好好的读一本Linux书籍吧
- HDU1059:Dividing(多重背包二进制优化)