从源码角度深入理解SharedPreferences

转载请注明出处:http://blog.csdn.net/kiddTeb/article/details/52712383

前言

  • SharedPreferences作为Android中数据存储方式的一种,官方的API说明如下
  • 这里写图片描述

    想要更加深入理解 SharedPreferences,就必须从源码角度去分析~~ 看源码的时候带着问题带着目的去看,效果会事半功倍,因为源码的量很庞大,很难做到每一行都去精读。

  • SharedPreferences底层是靠什么实现存储键值对的?commit和apply为什么是同步与异步的区别?它为什么不建议用来存储大量的数据?它会不会阻塞UI线程?。。。等等问题,带着这些问题,我们开始从getSharedPreferences读操作入手。

读操作

public abstract SharedPreferences getSharedPreferences(String name,
            int mode);

可以看到在Context里面它只是个抽象的方法,那么它的实现方法肯定在它的实现类ContextImpl当中

@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);//通过指定的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;
    }

很明显可以看到的是单例模式,再看看sSharedPrefs,它是一个static变量,也就是说一个类当中只有一个实例,到这里就知道,平时我们getSharedPreferences的时候,对于同一个name,获取到的都是同一个对象。接下来我们看看sSharedPrefs这个ArrayMap存储的key和value是什么。在第13行当中可以看到,它的key就是包名,value是packagePrefs,而packagePrefs又是一个ArrayMap,那么我们又来看看packagePrefs的key和value是什么。在第30行可以看到,它的key就是我们传进来的参数name,value就是SharedPreferencesImpl对象(ps:我们通过getSharedPreferences得到的其实就是SharedPreferencesImpl对象,而不是SharedPreferences)。那么如何实例化这个对象?我们从它的构造方法入手看看

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//根据传入的文件,生成一个.bat后缀的备份文件
        mMode = mode;//指定模式,就是我们传进来的对文件操作的模式参数
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }

我们从最后一行的那个startLoadFromDisk()开始看

private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }

根据方法的名字大概能猜出这方法的作用主要是一个从磁盘开始加载数据。加载之前mLoaded肯定是为false的。然后后面开了一个线程去执行loadFromDiskLocked()方法,这里特别主要下,这里有一把锁锁住了SharedPreferencesImpl这个对象,也就是说,如果此时此刻去调用SharedPreferencesImpl对象的getString或者getInt等方法的时候,肯定会阻塞,接下来继续看看这个方法

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文件读取数据到map
                } 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();//唤醒等待中的线程
    }

重点来看看第24行,这里会从底层XML文件中读取数据到map当中,这也就说明了SharedPreference底层是通过XML文件进行键值对存储的。看看第37行和45行代码的注释。接下来看看get…的方法

public long getLong(String key, long defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            Long v = (Long)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

看看awaitLoadedLocked()

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) {
            }
        }
    }

看第8-13行就知道,这个方法主要作用是阻塞等到数据读取完成为止。假设我们用SharedPreference存储了大量的数据,那么我们在loadFromDiskLocked()当中的XmlUtils.readMapXml(str);将会读取比较久的时间,而在这段时间内SharedPreference的getString,getInt等方法将会一直阻塞,知道loadFromDiskLocked()当中的XmlUtils.readMapXml(str)读取完成后将mLoaded = true;再notifyAll();的时候awaitLoadedLocked()中的循环就会开启,而这个时候mLoaded=true,所以循环将终止,这个时候就可以顺利通过getString,getInt等方法获取到我们想要的值。
* ps:如果我们真的用SharedPreference存储大量的数据,那么我们在UI线程调用getString,getInt等方法的时候将会阻塞UI线程,那么这个时候就有可能产生ANR。也就说明了,这就是为什么SharedPreference不适合用来存储大量的数据。

写操作

  • 看完了读操作,接下来来分析一下写操作,我们从edit()方法着手
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.
        synchronized (this) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

和明显看到返回的是一个EditorImpl对象,那么我们进入这个实现类当中看看

public final class EditorImpl implements Editor {
        private final Map<String, Object> mModified = Maps.newHashMap();
        private boolean mClear = false;

        public Editor putString(String key, String value) {
            synchronized (this) {
                mModified.put(key, value);
                return this;
            }
        }
    。。。//省略
        。。。//省略
}

这个类是SharedPreferenceImpl的内部类,看到这个类我们从第2行和第7行可以知道mModified是个Map变量,拿来存储的是我们put进去的key和value的值,当我们还没有调用commit或者apply的时候,它就还不会写入磁盘当中。那接下来看看commit方法

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()方法,看方法名字,我们也大概能知道它主要是将数据提交到内存。有时候源码的方法和变量的命名都是很规范的,让人一看名字就能知道你想干嘛。这也是提高代码可读性的一个方法,值得我们去学习。

private MemoryCommitResult commitToMemory() {
            MemoryCommitResult mcr = new MemoryCommitResult();
            synchronized (SharedPreferencesImpl.this) {
                // 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
                    mMap = new HashMap<String, Object>(mMap);//有一个以上的未写完的操作数,就copy一份mMap
                }
                mcr.mapToWriteToDisk = mMap;//将mMap传给mapToWriteToDisk,它将会在writeToFile方法中写到磁盘
                mDiskWritesInFlight++;//未写完操作数+1

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    mcr.keysModified = new ArrayList<String>();
                    mcr.listeners =
                            new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (this) {
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            mcr.changesMade = true;
                            mMap.clear();
                        }
                        mClear = false;
                    }

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

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

                    mModified.clear();
                }
            }
            return mcr;
        }

我们直接来看看33到58行,这里通过循环遍历mModified将数据放到mMap当中去(我们调用getString等方法的时候就是从mMap当中获取的),这里也说明了我们对整个文件进行了数据读取到内存当中,也就是说这个时候数据将会占据内存,数据大量的时候,将会占据大量内存。仔细一看循环遍历当中有种情况(39到44行)就是当我们传进来的值是null并且当前含有相同的key值的时候,就会从mMap当中删除此键值对。
我们往上看25到30行,当我们调用clear方法的时候,会将mMap的数据清空,但是这是在调用循环遍历之前,也就是说,当我们在调用putString等方法的时候再调用clear方法的时候,新put进去的数据也不会被清空,清空的只是之前的数据。接下来再看看下一个方法

SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);

这里调用了enqueueDiskWrite方法,并且传入了两个参数,第一个是我们在第一个方法返回来的MemoryCommitResult对象,第二个是null,第二个参数我们看看官方给的注释,意思是传入这个参数表示在当前线程同步写入。也就是说apply和commit的区别就会体现在第二个参数的传递上的不同而不同。

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--;//未写完的操作数-1
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        final boolean isFromSyncCommit = (postWriteRunnable == null);

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

一开始定义了一个Runnable任务,里面有个writeToFile(mcr)方法,先不看这个方法往下看,第17行可以知道传入的第二个参数是null的话就为true,那么将会一直走到第27行开启一个线程执行Runnable任务并直接返回,如果使用apply的话,将会执行最后一行代码,这里可以看到是开了一个单线程的线程池去执行Runnable任务,这就是apply异步的关键所在。

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)) {//将mFile重新命名为备份文件名
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false);
                    return;
                }
            } else {
                mFile.delete();//如果备份文件存在,那么直接删除mFile文件,因为接下来将要重新写一个mFile
            }
        }

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

357行是将数据写入磁盘XML文件的关键方法。为什么需要备份文件的存在?因为将mFile进行一个备份操作,当我们对新的mFile(本次写操作)进行操作失败(失败数据可能会丢失)的时候,还能通过备份下来的文件进行数据恢复。


  • 综上所述,我们在进行读写操作的时候都是对整个文件的数据拿出来再进行操作的,而不是每次都对文件进行单条单条数据进行追加进文件里的,也就是说SharedPrefrences的操作是基于全局的,而不是增量的。

总结

  • SharedPrefrences源码当中还有许多还没探讨到的问题,大家可以按照自己的思路再进行梳理一遍流程加深印象。对文中没说到的地方根据自己的兴趣与不足去研究,这样就能更加理解透彻SharedPrefrences了。最后祝大家国庆快乐!
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值