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

原文:http://www.2cto.com/kf/201312/268547.html

时间: 2024-08-01 04:19:39

Android-SharedPreferences源码学习与最佳实践的相关文章

Android SharedPreferences源码分析.md

我们经常使用SharedPreferences保存一些简单的数据,比如Settings的数据.如果我们只是简单的使用,可能没什么问题,但是如果要用好它还是得明白它的实现方式,下面来从源码上来分析下SharedPreferences的缓存,异步读写实现,多线程,多进程访问. SharedPreferences简介 SharedPreferences是Android提供的一种使用XML文件保存内容的机制.其内部就是通过xml写入文件的. SharedPreferences是一个接口类,这是使用它的一

Android系统源码学习步骤

Android系统是基于Linux内核来开发的,在分析它在运行时库层的源代码时,我们会经常碰到诸如管道(pipe).套接字(socket)和虚拟文件系统(VFS)等知识. 此外,Android系统还在Linux内核中增加了一些专用的驱动程序,例如用于日志系统的Logger驱动程序.用于进程间通信的Binder驱动程序和用于辅助内存管理的匿名共享内存Ashmem驱动程序.在分析这些Android专用驱动程序的时候,也会碰到Linux内核中与进程.内存管理相关的数据结构. 因此,我们有必要掌握一些L

[Android阅读代码]android-async-http源码学习一

android-async-http 下载地址 一个比较常用的Http请求库,基于org.apache.http对http操作进行封装. 特点: 1.每一个HTTP请求发生在UI线程之外,Client通过回调处理HTTP请求的结果,使得Client代码逻辑清晰 2.每一个请求使用线程池管理执行 3.支持gzip , cookie等功能 4.支持自动重试连接功能 [Android阅读代码]android-async-http源码学习一,布布扣,bubuko.com

多个Android项目源码-覆盖方方面面值得学习

Android PDF 阅读器 http://sourceforge.net/projects/andpdf/files/个人记账工具 OnMyMeans http://sourceforge.net/projects/onmymeans/developAndroid电池监控 Android Battery Dog http://sourceforge.net/projects/andbatdog/RSS阅读软件 Android RSS http://code.google.com/p/andr

Android事件分发详解(三)——ViewGroup的dispatchTouchEvent()源码学习

package cc.aa; import android.os.Environment; import android.view.MotionEvent; import android.view.View; public class UnderstandDispatchTouchEvent { /** * dispatchTouchEvent()源码学习及其注释 * 常说事件传递中的流程是:dispatchTouchEvent->onInterceptTouchEvent->onTouchE

[Android FrameWork 6.0源码学习] View的重绘过程之WindowManager的addView方法

博客首页:http://www.cnblogs.com/kezhuang/p/ 关于Activity的contentView的构建过程,我在我的博客中已经分析过了,不了解的可以去看一下 <[Android FrameWork 6.0源码学习] Window窗口类分析> 本章博客是接着上边那篇博客分析,目的是为了引出分析ViewRootImpl这个类.现在只是分析完了Window和ActivityThread的调用过程 从ActivityThread到WindowManager再到ViewRoo

【流媒体开发】VLC Media Player - Android 平台源码编译 与 二次开发详解 (提供详细800M下载好的编译源码及eclipse可调试播放器源码下载)

作者 : 韩曙亮  博客地址 : http://blog.csdn.net/shulianghan/article/details/42707293 转载请注明出处 : http://blog.csdn.net/shulianghan VLC 二次开发 视频教程 : http://edu.csdn.net/course/detail/355 博客总结 : -- 本博客目的 : 让 Android 开发者通过看本博客能够掌握独立移植 VLC Media Player 核心框架到自己的 app 中,

2016年最牛逼的分类Android项目源码免费一次性打包下载!

之前发过一个帖子,但是那个帖子有点问题我就重新发一个吧,下面的源码是我从今年开始不断整理源码区和其他网站上的安卓例子源码,目前总共有810套左右,根据实现的功能被我分成了100多个类,总共接近2.5G,还在不断更新.初学者可以快速方便的找到自己想要的例子,大神也可以看一下别人的方法实现.虽然的例子都是我一个人辛辛苦苦花了很多时间和精力整理的,但是既然这些例子是来自于社区那就让他们免费回归社区吧,(是的!特么的不要一分钱!最看不起那些挂羊头卖狗的)你可以在本帖里面按Ctrl+F查找你需要的关键字,

最新app源码下载:200款优秀Android项目源码

200款优秀Android项目源码!菜鸟必备!Android开发又将带来新一轮热潮,很多开发者都投入到这个浪潮中去了,创造了许许多多相当优秀的应用.其中也有许许多多的开发者提供了应用开源项目,贡献出他们的智慧和创造力.学习开源代码是掌握技术的一个最佳方式. 下载地址: http://bbs.aiyingli.com/forum.php?mod=viewthread&tid=13120