Android-SharedPreferences源码学习与最佳实践
最近有个任务是要做应用启动时间优化,然后记录系统启动的各个步骤所占用的时间,发现有一个方法是操作SharedPreferences的,里面仅仅是读了2个key,然后更新一下值,然后再写回去,耗时竟然在500ms以上(应用初次安装的时候),感到非常吃惊。以前只是隐约的知道SharedPreferences是跟硬盘上的一个xml文件对应的,具体的实现还真没研究过,下面我们就来看看SharedPreferences到底是个什么玩意,为什么效率会这么低?
SharedPreferences是存放在ContextImpl里面的,所以先看写ContextImpl这个类:
ContextImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ContextImpl.java):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
/** * Map from package name, to preference name, to cached preferences. */ private static ArrayMap<string, arraymap<string,= "" sharedpreferencesimpl= "" >> sSharedPrefs; //在内存的一份缓存 @Override public 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(); } return sp; }</string,></string,></string,></string,> |
getSharedPreferences()做的事情很简单,一目了然,我们重点看下SharedPreferencesImpl.java这个类:
SharedPreferencesImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/SharedPreferencesImpl.java)
首先是构造函数:
1
2
3
4
5
6
7
8
9
10
11
|
SharedPreferencesImpl(File file, int mode) { mFile = file; //这个是硬盘上的文件 mBackupFile = makeBackupFile(file); //这个是备份文件,当mFile出现crash的时候,会使用mBackupFile来替换 mMode = mode; //这个是打开方式 mLoaded = false ; //这个是一个标志位,文件是否加载完成,因为文件的加载是一个异步的过程 mMap = null ; //保存数据用 startLoadFromDisk(); //开始从硬盘异步加载 } //还两个很重要的成员: private int mDiskWritesInFlight = 0 ; //有多少批次没有commit到disk的写操作,每个批次可能会对应多个k-v private final Object mWritingToDiskLock = new Object(); //写硬盘文件时候加锁 |
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//从硬盘加载 private void startLoadFromDisk() { synchronized ( this ) { //先把状态置为未加载 mLoaded = false ; } new Thread( "SharedPreferencesImpl-load" ) { //开了一个线程,异步加载 public void run() { synchronized (SharedPreferencesImpl. this ) { loadFromDiskLocked(); //由SharedPreferencesImpl.this锁保护 } } }.start(); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
//从硬盘加载 private void loadFromDiskLocked() { 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 = Libcore.os.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null ; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024 ); //从硬盘把数据读出来 map = XmlUtils.readMapXml(str); //做xml解析 } catch (XmlPullParserException e) { Log.w(TAG, "getSharedPreferences" , e); } catch (FileNotFoundException e) { Log.w(TAG, "getSharedPreferences" , e); } catch (IOException e) { Log.w(TAG, "getSharedPreferences" , e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { } mLoaded = true ; //设置标志位,已经加载完成 if (map != null ) { mMap = map; //保存到mMap mStatTimestamp = stat.st_mtime; //记录文件的时间戳 mStatSize = stat.st_size; //记录文件的大小 } else { mMap = new HashMap<string, object= "" >(); } notifyAll(); //唤醒等待线程 } </string,> |
然后我们随便看一个读请求:
1
2
3
4
5
6
7
|
public int getInt(String key, int defValue) { synchronized ( this ) { //还是得首先获取this锁 awaitLoadedLocked(); //这一步完成以后,说明肯定已经加载完了 Integer v = (Integer)mMap.get(key); //直接从内存读取 return v != null ? v : defValue; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//等待数据加载完成 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(); //等 } catch (InterruptedException unused) { } } } |
看一下写操作,写是通过Editor来做的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public Editor edit() { // TODO: remove the need to call awaitLoadedLocked() when // requesting an editor. will require some work on the // Editor, but then we should be able to do: // // context.getSharedPreferences(..).edit().putString(..).apply() // // ... all without blocking. //注释很有意思,获取edit的时候,可以把这个同步去掉,但是如果去掉就需要在Editor上做一些工作(???)。 //但是,好处是context.getSharedPreferences(..).edit().putString(..).apply()整个过程都不阻塞 synchronized ( this ) { //还是先等待加载完成 awaitLoadedLocked(); } return new EditorImpl(); //返回一个EditorImpl,它是一个内部类 } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public final class EditorImpl implements Editor { //写操作暂时会把数据放在这里面 private final Map<string, object= "" > mModified = Maps.newHashMap(); //由this锁保护 //是否要清空所有的preferences private boolean mClear = false ; public Editor putInt(String key, int value) { synchronized ( this ) { //首先获取this锁 mModified.put(key, value); //并不是直接修改mMap,而是放到mModified里面 return this ; } } } </string,> |
看一下commit:
1
2
3
4
5
6
7
8
9
10
11
|
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; } |
commitToMemory()这个方法主要是用来更新内存缓存的mMap:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
// Returns true if any changes were made private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl. this ) { //加SharedPreferencesImpl锁,写内存的时候不允许读 // We optimistically don't make a deep copy until a memory commit comes in when we're already writing to disk. if (mDiskWritesInFlight > 0 ) { //如果存在没有提交的写, mDiskWritesInFlight是SharedPreferences的成员变量 // 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); //clone一个mMap,没明白! } mcr.mapToWriteToDisk = mMap; mDiskWritesInFlight++; //批次数目加1 boolean hasListeners = mListeners.size() > 0 ; if (hasListeners) { mcr.keysModified = new ArrayList<string>(); mcr.listeners = new HashSet<onsharedpreferencechangelistener>(mListeners.keySet()); } synchronized ( this ) { //对当前的Editor加锁 if (mClear) { //只有当调用了clear()才会把这个值置为true if (!mMap.isEmpty()) { //如果mMap不是空 mcr.changesMade = true ; mMap.clear(); //清空mMap。mMap里面存的是整个的Preferences } mClear = false ; } for (Map.Entry<string, object= "" > e : mModified.entrySet()) { //遍历所有要commit的entry String k = e.getKey(); Object v = e.getValue(); if (v == this ) { // magic value for a removal mutation if (!mMap.containsKey(k)) { continue ; } mMap.remove(k); } else { boolean isSame = false ; if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue != null && existingValue.equals(v)) { continue ; } } mMap.put(k, v); //这里是往里面放,因为最外层有对SharedPreferencesImpl.this加锁,写是没问题的 } mcr.changesMade = true ; if (hasListeners) { mcr.keysModified.add(k); } } mModified.clear(); //清空editor } } return mcr; }</string,></onsharedpreferencechangelistener></string></string,> |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
//这是随后的写硬盘 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 ); //如果是commit,postWriteRunnable是null // Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { //如果是调用的commit boolean wasEmpty = false ; synchronized (SharedPreferencesImpl. this ) { wasEmpty = mDiskWritesInFlight == 1 ; //如果只有一个批次等待写入 } if (wasEmpty) { writeToDiskRunnable.run(); //不用另起线程,直接在当前线程执行,很nice的优化! return ; } } //如果不是调用的commit,会走下面的分支 //如或有多个批次等待写入,另起线程来写,从方法名可以看出来也是串行的写,写文件本来就应该串行! QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } |
看下writeToDiskRunnable都干了些什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
final Runnable writeToDiskRunnable = new Runnable() { //这是工作在另一个线程 public void run() { synchronized (mWritingToDiskLock) { //mWritingToDiskLock是SharedPreferencesImpl的成员变量,保证单线程写文件, //不能用this锁是因为editor上可能会存在多个commit或者apply //也不能用SharedPreferences锁,因为会阻塞读,不错! writeToFile(mcr); //写到文件 } synchronized (SharedPreferencesImpl. this ) { mDiskWritesInFlight--; //批次减1 } if (postWriteRunnable != null ) { postWriteRunnable.run(); //这个是写完以后的回调 } } }; |
下面是真正要写硬盘了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
// Note: must hold mWritingToDiskLock private void writeToFile(MemoryCommitResult mcr) { // Rename the current file so it may be used as a backup during the next read if (mFile.exists()) { 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 ; } 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 { FileOutputStream str = createFileOutputStream(mFile); if (str == null ) { mcr.setDiskWriteResult( false ); return ; } XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); FileUtils.sync(str); //强制写到硬盘 str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0 ); try { final StructStat stat = Libcore.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 ); } |
1
2
3
4
5
6
7
8
9
10
|
public static boolean sync(FileOutputStream stream) { try { if (stream != null ) { stream.getFD().sync(); //强制写硬盘 } return true ; } catch (IOException e) { } return false ; } |
这里面还有一个跟commit长得很像的方法叫apply():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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); //这个地方传递的postWriteRunnable不再是null // 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); } |
我们已经看过enqueueDiskWrite()这个方法了,因为参数postWriteRunnable不是null,最终会执行:
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
这是在单独的线程上做写硬盘的操作,写完以后会回调postWriteRunnable,等待写硬盘完成!
从上面的代码可以得出以下结论:
(1)SharedPreferences在第一次加载的时候,会从硬盘异步的读文件,然后会在内存做缓存。
(2)SharedPreferences的读都是读的内存缓存。
(3)如果是commmit()写,是先把数据更新到内存,然后同步到硬盘,整个过程是在同一个线程中同步来做的。
(4)如果是apply()写,首先也是写到内存,但是会另起一个线程异步的来写硬盘。因为我们在读的时候,是直接从内存读取的,因此,用apply()而不是commit()会提高性能。
(5)如果有多个key要写入,不要每次都commit或者apply,因为这里面会存在很多的加锁操作,更高效的使用方式是这样:editor.putInt("","").putString("","").putBoolean("","").apply();并且所有的putXXX()的结尾都会返回this,方便链式编程。
(6)这里面有三级的锁:SharedPreferences,Editor, mWritingToDiskLock。
mWritingToDiskLock是对应硬盘上的文件,Editor是保护mModified的,SharedPreferences是保护mMap的。
参考:
http://stackoverflow.com/questions/19148282/read-speed-of-sharedpreferences
http://stackoverflow.com/questions/12567077/is-sharedpreferences-access-time-consuming