Android SharedPreference 源码分析

1.前言

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

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.
     *
     * <p>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 <em>greater than</em> 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翻译一下。bash

/ **
     * SharedPreference加载标志:设置时,磁盘上的文件将
     *即便共享首选项,也要检查修改
     *实例已经在这个过程当中加载。这是行为
     *有时须要在应用程序有多个的状况下
     *进程,所有写入相同的SharedPreferences文件。
     *通常来讲有更好的交流形式
     *进程,虽然。
     *
     *这是在和遗传(但没有记录)的行为
     *以前的姜饼(Android 2.3)和这个标志是隐含的时候
     *瞄准这样的发布。针对SDK的应用程序
     *版本<em>大于</ em> 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相关的类以及相关关系。 app

这里写图片描述

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)源码以下所示:ide

@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;
    }
复制代码

从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文件,源码以下所示:post

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<String, Object>();
        }
        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<String, Object>();
        }
        notifyAll();
    }
复制代码

很明显,最后读取成功后,mLoaded 被赋值为true,而loadFromDiskLocked这个方法是在SharedPreferencesImpl的构造方法中就调用,因此为了避免形成阻塞,咱们能够提早建立出SharedPreferences对象,而不是在使用的时候再去建立。

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


若有错误欢迎指出来,一块儿学习。

在这里插入图片描述
相关文章
相关标签/搜索