SharedPreferences知识点及原理浅析

概述

SharedPreferences作为Android系统的轻量级数据存储方式之一,能够比较方便的存取一些简单的临时数据,现回顾下使用流程及实现原理。

调用方式

//Context.MODE_PRIVATE表示该文件只能被创建他的应用访问
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
//存入数据,以存入int类型数据为例
 editor.putInt(key, value);
 editor.commit();
 //editor.apply();如果不关心返回结果,可以使用该方法进行异步提交
//取出数据,如果不存在返回设置的默认数据
int value = sp.getInt(key, defValue);

对于context.getSharedPreferences()方法的注释如下:

根据文件名来获取并保存内容,返回一个SharedPreferences对象,您可以通过它来获取和修改它的值。 如果多个调用者使用同样的名字进行调用,那么只会返回一个SharedPreferences实例,意味着他们会看到彼此编辑的内容

存储方式

由于SharedPreferences本质上使用的是文件存储,存储位置在
/data/data/<程序包名>/shared_prefs/文件名.xml,能够保存基本的数据类型,基本格式如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="key">value</string>
</map>

实现原理

首先看看context.getSharedPreferences()是如何获取的,context的真正实现类为ContextImpl

class ContextImpl extends Context {

    //使用全局的静态Map对象来保存SharedPreferencesImpl的所有实现
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    //文件名和文件存储的Map对象
    private ArrayMap<String, File> mSharedPrefsPaths;

    //...省略代码
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        //...省略代码
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {//如果文件没有,生成一个
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);

 @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {//同样的如果缓存中找不到sp,那么创建一个
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, 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.
            //如果小于3.0版本或者多进程,将重新加载刷新数据
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }
        //根据包名来获取
        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {//没有的话新建一个放入进去
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }
    //...省略代码
}

上面就是获取SharedPreferences对象的主要流程了,主要使用了全局的静态Map保存相关引用。
之后来看看’SharedPreferences’的真正实现,其实SharedPreferences本身是一个接口,真正的实现类是SharedPreferencesImpl,首先看看其构造方法:

 SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//备份文件,用于备份和恢复内容
        mMode = mode;
        mLoaded = false;
        mMap = null;//内存缓存
        startLoadFromDisk();//从磁盘加载存储的内容
    }

构造方法里面创建了mMap的内存缓存,后面存放及获取值的时候都是从这个缓存里面维护的,
startLoadFromDisk()就是从文件读取存储内容赋值给mMap的,看看其实现:

    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();//在新的线程中进行内容的读取
            }
        }.start();
    }

 private void loadFromDisk() {
         synchronized (mLock) {
            if (mLoaded) {
                return;//如果正在加载就reture
            }
            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);//将内容读取出来赋值给map
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;//将内容再次赋值给mMap对象,这样mMap将负责在内存中的数据维护
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {//如果没有内容的话,就直接创建一个Map对象
                mMap = new HashMap<>();
            }
            mLock.notifyAll();
        }
    }

SharedPreferences.edit()可以返回一个Editor对象,而Editor的接口实现类为EditorImpl:

public final class EditorImpl implements Editor {
    private final Object mLock = new Object();

        //存入的内容首先会放入这个mModified的Map对象中
        @GuardedBy("mLock")
        private final Map<String, Object> mModified = Maps.newHashMap();
        //clear标志
        @GuardedBy("mLock")
        private boolean mClear = false;

        public Editor putString(String key, @Nullable String value) {
            synchronized (mLock) {
                mModified.put(key, value);//将key,value放入
                return this;
            }
        }
        //...省略代码
        public Editor remove(String key) {
            synchronized (mLock) {
                mModified.put(key, this);//移除key的话,先存入自身对象
                return this;
            }
        }

        public Editor clear() {
            synchronized (mLock) {
                mClear = true;//clear内容的话,首先将mClear置为true
                return this;
            }
        }
}

之后看看commit()方法的实现:

public boolean commit() {
            //...省略代码
            //首先是提交到内存,也就是将内容先提交到mMap中
            MemoryCommitResult mcr = commitToMemory();
            //之后再写入文件
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } finally {
             //...省略代码
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

接着看看commitToMemory()方法的实现:

private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                if (mDiskWritesInFlight > 0) {
                    // We can't modify our mMap as a currently
                    // in-flight write owns it.  Clone it before
                    // modifying it.
                    // noinspection unchecked
                    //这里如果mDiskWritesInFlight>0说明正在进行写入文件操作,此时 
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;//表明要进入文件写入了,写入完毕后会还原

                boolean hasListeners = mListeners.size() > 0;//是否注册了监听
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mLock) {
                    boolean changesMade = false;
                    //如果之前调用了clear()方法,那么先清空mMap
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            changesMade = true;
                            mMap.clear();
                        }
                        mClear = false;
                    }
                    //从mModified中循环取出值开始对比
                    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.
                        //如果设定的null或者this对象,那么说明是移除这个Key-value了
                        if (v == this || v == null) {
                            if (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                            if (mMap.containsKey(k)) {//如果包含的值相同那么跳过
                                Object existingValue = mMap.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mMap.put(k, v);//更新mMap
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();//清空

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

apply()方法的实现区别:

 public void apply() {
            final long startTime = System.currentTimeMillis();
            //同样是先将数据同步到内存缓存
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {//这里主要是等待文件写入完成
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);
            //该任务在文件写入完毕后才执行,主要利用了mcr对象中的CountDownLatch
            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(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);
        }

最后都调用到的SharedPreferences.enqueueDiskWrite()方法实现:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //根据postWriteRunnable参数来判断是同步提交还是异步提交
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                    //这是真正的写入文件的方法
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                    //写入完毕后还原这个值
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {//执行此任务
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {//如果是同步提交,也就是调用的commit()提交的话,那么会走到这里
            boolean wasEmpty = false;
            synchronized (mLock) {//之前commitToMemory()方法中mDiskWritesInFlight++了已经
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {//这里直接在当前线程运行任务,之后reture
                writeToDiskRunnable.run();
                return;
            }
        }
        //而调用的apply()方法会使用异步提交
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

再看下writeToFile()方法,这里面实现了真正的写入文件方法:

 // Note: must hold mWritingToDiskLock
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        long startTime = 0;
        long existsTime = 0;
        long backupExistsTime = 0;
        long outputStreamCreateTime = 0;
        long writeTime = 0;
        long fsyncTime = 0;
        long setPermTime = 0;
        long fstatTime = 0;
        long deleteTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        boolean fileExists = mFile.exists();

        if (DEBUG) {
            existsTime = System.currentTimeMillis();

            // Might not be set, hence init them to a default value
            backupExistsTime = existsTime;
        }

        // Rename the current file so it may be used as a backup during the next read
        if (fileExists) {
            boolean needsWrite = false;//判断是不是需要写入

            // Only need to write if the disk state is older than this commit
            //判断文件比当前的提交更旧才会提交
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
            //如果不需要提交那么直接reture
            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
            //判断备份文件是否存在
            boolean backupFileExists = mBackupFile.exists();

            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }

            if (!backupFileExists) {
            //如果备份文件不存在,那么就备份一个文件,这样写入失败后,可以从备份文件恢复内容
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, 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 (DEBUG) {
                outputStreamCreateTime = System.currentTimeMillis();
            }

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            //写入文件
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

            writeTime = System.currentTimeMillis();
            //同步文件
            FileUtils.sync(str);

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

            if (DEBUG) {
                setPermTime = System.currentTimeMillis();
            }

            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

            if (DEBUG) {
                fstatTime = System.currentTimeMillis();
            }

            // Writing was successful, delete the backup file if there is one.
            //写入成功就删除备份
            mBackupFile.delete();

            if (DEBUG) {
                deleteTime = System.currentTimeMillis();
            }

            mDiskStateGeneration = mcr.memoryStateGeneration;
            //只有走到这里标识写入成功
            mcr.setDiskWriteResult(true, true);

            if (DEBUG) {
                Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                        + (backupExistsTime - startTime) + "/"
                        + (outputStreamCreateTime - startTime) + "/"
                        + (writeTime - startTime) + "/"
                        + (fsyncTime - startTime) + "/"
                        + (setPermTime - startTime) + "/"
                        + (fstatTime - startTime) + "/"
                        + (deleteTime - startTime));
            }

            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;

            if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
            }

            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, false);
    }

上面就是整个提交过程,主要流程就是先提交到内存,然后区别是同步还是异步提交。
取值的话就非常简单了,看看SharedPreferencesImpl中的实现:

    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();//主要就是根据mLoaded来判断,等待数据从文件加载到内存之后才能读取
            Integer v = (Integer)mMap.get(key);//直接从mMap中取出值
            return v != null ? v : defValue;
        }
    }

存储对象

由于SharedPreferences只能保存基本数据类型,那么对于对象的保存,可以将对象转换为json字符串再进行保存,参考文章:
Android本地最简单的数据存储,没有之一(让SharedPreferences存取JavaBean对象或List)
但是不建议保存复杂数据对象。

修改存储位置

由于SharedPreferences保存在应用程序的安装路径下,那么随着软件的卸载,保存的内容也随之删除,如果想自定义SharedPreferences的保存路径,可以参考如下实现,本质上使用了反射修改了文件的保存路径:
SharedPreferences自定义XML文件保存位置

总结

1.尽量不存入体积庞大的数据,数据从文件加载到内存中后将会一直占用内存;
2.使用commit()方法也尽量不存入提交庞大的数据,不需要同步的话尽量使用apply()方法异步提交;
3.使用的时候尽量将变化的数据put完毕后再进行commit或者apply操作;

参考文章:
工匠若水:Android应用Preference相关及源码浅析(SharePreferences篇)
其中涉及了如何进行跨进程使用SharedPreferences

猜你喜欢

转载自blog.csdn.net/franky814/article/details/81327129