1. 前言

众所周知,SharedPreferences是Android平台上一个轻量级的存储类,用来保存应用的一些常用配置,比如Activity状态,Activity暂停时,将此activity的状态保存到SharedPereferences中;当Activity重载,系统回调方法onSaveInstanceState时,再从SharedPreferences中将值取出。

2.基本概念

SharedPreferences提供了常规的Long、Int、String等类型数据的保存/获取接口,并以xml方式保存在data/data/you.package.name/shared_prefs/目录下的文件。
SharedPreferences数据的操作模式
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件.
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取.
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入
特别注意:出于安全性的考虑,MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 在Android 4.2版本中已经被弃用。
MODE_MULTI_PROCESS:跨进程模式

3.提出问题

1).SharedPreferences真的支持多进程吗?
可能有些人会觉得很奇怪,上面不是写了MODE_MULTI_PROCESS模式,明确说明就是跨进程模式,为什么还问是否真的支持多进程。为了找到答案,我们来阅读MODE_MULTI_PROCESS的注释。

 /**     * SharedPreference loading flag: when set, the file on disk will     * be checked for modification even if the shared preferences     * instance is already loaded in this process.  This behavior is     * sometimes desired in cases where the application has multiple     * processes, all writing to the same SharedPreferences file.     * Generally there are better forms of communication between     * processes, though.     *     * 

This was the legacy (but undocumented) behavior in and * before Gingerbread (Android 2.3) and this flag is implied when * targetting such releases. For applications targetting SDK * versions greater than Android 2.3, this flag must be * explicitly set if desired. * * @see #getSharedPreferences * * @deprecated MODE_MULTI_PROCESS does not work reliably in * some versions of Android, and furthermore does not provide any * mechanism for reconciling concurrent modifications across * processes. Applications should not attempt to use it. Instead, * they should use an explicit cross-process data management * approach such as {@link android.content.ContentProvider ContentProvider}. */

很尴尬,英文看不懂?google翻译一下。

/ **     * SharedPreference加载标志:设置时,磁盘上的文件将     *即使共享首选项,也要检查修改     *实例已经在这个过程中加载。这是行为     *有时需要在应用程序有多个的情况下     *进程,全部写入相同的SharedPreferences文件。     *一般来说有更好的交流形式     *进程,虽然。     *     *这是在和遗传(但没有记录)的行为     *之前的姜饼(Android 2.3)和这个标志是隐含的时候     *瞄准这样的发布。针对SDK的应用程序     *版本大于 Android 2.3,这个标志必须是     *如果需要显式设置。     *     *参见#getSharedPreferences     *     * @deprecated MODE_MULTI_PROCESS不能可靠的工作     *某些版本的Android,而且不提供任何     *协调跨越并发修改的机制     *进程。应用程序不应该尝试使用它。代替,     *他们应该使用明确的跨进程数据管理     *方法,如{@link android.content.ContentProvider ContentProvider}。     * /

很明显MODE_MULTI_PROCESS是不可靠的,google推荐使用ContentProvider实现跨进程数据管理,如何使用,这个在下一篇介绍。
2).上面说到SharedPreference是以xml文件存在本地,那么读取/保存文件是不是损耗性能,需不需要每个app自己做内存缓存,还有保存数据大小是否有限制,频繁读取有没有注意点,带着这些问题我们看看SharedPreference底层是如何实现的。

4.源码分析

1)先上图,让大家直观的了解下SharedPreference相关的类以及相关关系。

2)一般使用SharedPreference的时候,都是如下调用:

SharedPreferences sp = context.getSharedPreferences(SP_NAME,                Context.MODE_MULTI_PROCESS);

那么,我们就看下getSharedPreferences(String name, int mode)方法,打开Context类可以看到该方法是个抽象方法,具体实现在子类中,打开Context的实现类ContextImpl,可以看到getSharedPreferences(String name, int mode)源码如下所示:

@Override    public SharedPreferences getSharedPreferences(String name, int mode) {        SharedPreferencesImpl sp;        synchronized (ContextImpl.class) {            if (sSharedPrefs == null) {                sSharedPrefs = new ArrayMap>();            }            final String packageName = getPackageName();            ArrayMap packagePrefs = sSharedPrefs.get(packageName);            if (packagePrefs == null) {                packagePrefs = new ArrayMap();                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;    }

从5-14行可以看出,先根据应用包名,从本地map中取到该应用的所有sp文件,在根据传进来的name,获取需要读取的sp文件。
从27-32行可以看出,第一次先拿到sp文件,然后初始化出SharedPreferencesImpl实现类,在SharedPreferencesImpl的构造方法中从本地读取了该name的sp文件,后面会分析。
从34-41行看出,如果是MODE_MULTI_PROCESS模式或者版本小于11,会调用sp.startReloadIfChangedUnexpectedly()方法,该方法从磁盘把文件重新读取到内存,源码如下:

void startReloadIfChangedUnexpectedly() {        synchronized (this) {            // TODO: wait for any pending writes to disk?            if (!hasFileChangedUnexpectedly()) {                return;            }            startLoadFromDisk();        }    }

刚刚说到SharedPreferencesImpl的构造方法中从本地读取了该name的sp文件,源码如下所示:

SharedPreferencesImpl(File file, int mode) {        mFile = file;        mBackupFile = makeBackupFile(file);        mMode = mode;        mLoaded = false;        mMap = null;        startLoadFromDisk();    }
private void startLoadFromDisk() {        synchronized (this) {            mLoaded = false;        }        new Thread("SharedPreferencesImpl-load") {            public void run() {                synchronized (SharedPreferencesImpl.this) {                    loadFromDiskLocked();                }            }        }.start();    }
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 = Os.stat(mFile.getPath());            if (mFile.canRead()) {                BufferedInputStream str = null;                try {                    str = new BufferedInputStream(                            new FileInputStream(mFile), 16*1024);                    map = XmlUtils.readMapXml(str);                } 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;            mStatTimestamp = stat.st_mtime;            mStatSize = stat.st_size;        } else {            mMap = new HashMap();        }        notifyAll();    }

从上面代码可以看出,从sp文件读出数据后,会赋值给mMap对象,该对象通过键值对保存sp文件,实现二级缓存的目的是为了提高读取效率,所以app应用中就不需要单独再自己做缓存。既然数据都保存在内存缓存mMap中,这样也就说明sp文件不适合存储大数据,会十分浪费内存。

下面再看下,如何保存数据,我们一般使用两种方法:apply(),commit()

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);            // 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);        }

第二行可以看出,先保存到内存,通过第十二行,可以看出,通过异步线程保存sp文件到磁盘。

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;        }

通过commit的源码可以看出,该保存到内存跟磁盘是同步保存,所以,如果频繁保存数据的话,apply肯定要高效,优先推荐使用apply。

看完保存数据,下面我们来看如何读取数据,拿getString来做示例:

  @Nullable    public String getString(String key, @Nullable String defValue) {        synchronized (this) {            awaitLoadedLocked();            String v = (String)mMap.get(key);            return v != null ? v : defValue;        }    }
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) {            }        }    }

可以看到数据是从mMap中读取,也就是从内存缓存中取,这样省去io操作,提高性能。
里面还有一个boolean变量mLoaded,该变量默认为false,这样会一直阻塞住读取,在什么地方设置为true的呢,从mLoaded的字面可以猜到是加载,那应该是从本地磁盘读成功赋值为true,我们看下源码:

 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 = Os.stat(mFile.getPath());            if (mFile.canRead()) {                BufferedInputStream str = null;                try {                    str = new BufferedInputStream(                            new FileInputStream(mFile), 16*1024);                    map = XmlUtils.readMapXml(str);                } 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;            mStatTimestamp = stat.st_mtime;            mStatSize = stat.st_size;        } else {            mMap = new HashMap();        }        notifyAll();    }

很明显,最后读取成功后,mLoaded 被赋值为true,而loadFromDiskLocked这个方法是在SharedPreferencesImpl的构造方法中就调用,所以为了不造成阻塞,我们可以提前创建出SharedPreferences对象,而不是在使用的时候再去创建。

总结:
1.sp数据都保存在内存缓存mMap中,这样也就说明sp文件不适合存储大数据,会十分浪费内存。
2.apply()先保存到内存,再通过异步线程保存sp文件到磁盘,commit保存到内存跟磁盘是同步保存,所以,如果频繁保存数据的话,apply肯定要高效,优先推荐使用apply。
3.从sp中读取数据,需要等sp构造方法调用从磁盘读取数据成功后才继续往下执行,所以为了不造成阻塞,我们可以提前创建出SharedPreferences对象,而不是在使用的时候再去创建。


如有错误欢迎指出来,一起学习。

更多相关文章

  1. 一句话锁定MySQL数据占用元凶
  2. android jni 包裹文件(jni wrapper) 以 speex 库为例
  3. Mobile Services批量提交数据
  4. Android入门笔记 - 数据存储 - SharedPreferences
  5. 在Android(安卓)2.3中如何使用native_activity.h编写本地应用
  6. android 在adapter中更改Activity中的数据 (自定义接口)
  7. android post数据到服务器端工具类(包括postjson字符串、键值对)
  8. android studio关联源码
  9. Android下载图片到相册

随机推荐

  1. Android(安卓)单击图片切换效果
  2. Android使用Application总结
  3. TabWidget/TabHost的两种使用方法
  4. Android之一个简单计算器源代码
  5. Android(安卓)SDK安装失败处理办法
  6. 自制android1.5的源码包
  7. Android中三级缓存实现原理及LruCache 源
  8. 百度地图2.1获取密钥配置的Android签名证
  9. Android最新优秀而实用的开源组件1
  10. android jni开发详细步骤