SharedPreferences源码解析

3,878 阅读7分钟

一.描述

最近项目验收遇到了SharedPreferences(以下简称SP)有关的问题,于是就去网上搜,知道了大概,这里做一次源码解析,加深理解。众所周知,SP是轻量级持久化工具,把键值对写成xml文件保存在data/data/packagename/shared_prefs路径下,注意SP这个类并不支持跨进程使用!下面来看一下如何获取SP

//Context.java
//name是xml的文件名,mode是操作模式
public abstract SharedPreferences getSharedPreferences(String name, int mode);

mode有以下几种:

  • MODE_PRIVATE 默认模式,创建的文件只能在应用内访问(或者共享相同userID的所有应用)
  • MODE_WORLD_READABLE(过时)允许其他应用访问本应用的文件,使用此模式会抛出异常
  • MODE_WORLD_WRITEABLE(过时)允许其他应用写本应用的文件,使用此模式会抛出异常
  • MODE_MULTI_PROCESS(过时)官方提示这种模式在某些版本无法可靠运行,并且未来也不会支持多进程

二.结构

//SharedPreferences.java
public interface SharedPreferences {

    /**
     * 用于修改SharedPreferences值的接口,要修改sp里的值必须通过Editor对象
     */
    public interface Editor {
   
        /**
         * 保存一个键值对
         */
        Editor putString(String key, @Nullable String value);

        /**
         * 根据key移除键值对
         */
        Editor remove(String key);

        /**
         * 清除sp文件里的内容
         */
        Editor clear();

        /**
         * 同步提交修改数据
         */
        boolean commit();

        /**
         * 异步提交修改数据
         */
        void apply();
    }

    /**
     * 获取数据
     */
    @Nullable
    String getString(String key, @Nullable String defValue);
    
    /**
     * 获取编辑器
     */
    Editor edit();
}

三.源码解析

3.1 getSharedPreferences方法

上面注释简单的介绍了SP的方法,当然还有其他类型数据的getput方法没有列举,这里只介绍一种读取和写入的方法。Context的实现类是android.app.ContextImpl,看一下getSharedPreferences的实现:

//ContextImpl.java
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 判空处理
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //根据名字取文件
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                //根据名字创建文件
                file = getSharedPreferencesPath(name);
                //保存到ArrayMap中
                mSharedPrefsPaths.put(name, file);
            }
        }
        //根据文件获取sp对象
        return getSharedPreferences(file, mode);
    }
    
        @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        //sp实现类
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //getSharedPreferencesCacheLocked根据包名获取ArrayMap<File, SharedPreferencesImpl>
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                //检查传入的mode,如果是MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE将抛SecurityException
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        // 大胆猜测,应该是判断是否是自己的应用
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                //通过一系列的校验,符合要求后实例化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) {
            // 如果是多进程模式需要重新读取文件
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

从代码可以看到SP的实现类是SharedPreferencesImpl,先看一下构造方法:

//SharedPreferencesImpl.java
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        //备份file
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }

    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        //开启一个名为SharedPreferencesImpl-load的线程从磁盘读取数据
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

    private void loadFromDisk() {
        synchronized (mLock) {
            //是否读取过
            if (mLoaded) {
                return;
            }
            //如果备份文件存在,删除mFile,将备份文件重命名给mFile
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        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);
                    //将xml文件转成map
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
            //表示已经读取过,下次调用getSharedPreferences不会再从磁盘读取
            mLoaded = true;
            if (map != null) {
                //赋值给成员变量mMap
                mMap = map;
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            //释放锁
            mLock.notifyAll();
        }
    }

整个流程的代码看起来都比较简单,这里也就不过多的阐述了,这里我有个疑问为什么要备份文件呢?且往下看。

  • 总结:

getSharedPreferences流程图
SharedPreferencesImpl构造方法中主要是开启一个子线程将xml文件读取到内存中(转成map)。

3.2 getString

//SharedPreferencesImpl.java
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            //阻塞等待sp将xml读取到内存后再get
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            //如果value为空返回默认值
            return v != null ? v : defValue;
        }
    }
    
    private void awaitLoadedLocked() {
        ...
        // sp读取完成后会把mLoaded设置为true
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

这里主要说一下awaitLoadedLocked()这个操作会阻塞主线程,所以说SP是轻量级持久化方式,如果文件过大阻塞时间就会变长,因为将xml文件读取完成后才会释放锁mLock.notifyAll();

3.3 Editor

还记得之前说过要想修改值,必须通过Editor对象,SP获取Editor:

//SharedPreferencesImpl.java
    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        //每次都会返回一个新的Editor
        return new EditorImpl();
    }

再看putString,remove,clear:

//SharedPreferencesImpl.EditorImpl.java
    public Editor putString(String key, @Nullable String value) {
        synchronized (mLock) {
            //将键值对写入mModified
            mModified.put(key, value);
            return this;
        }
    }
    public Editor remove(String key) {
        synchronized (mLock) {
            //移除操作很鸡贼,后面会根据value是否是this来判断是put还是remove
            mModified.put(key, this);
            return this;
        }
    }
    public Editor clear() {
        synchronized (mLock) {
            //后面会根据这个判断是否clear
            mClear = true;
            return this;
        }
    }

这里可以看到修改值只会将其存到mModifiedmap中去,所以在编辑器中所做的所有更改都会批处理,直到我们调用commitapply才会设置到mMap和xml文件中去,这里先说个结论当我们调用putxxxremoveclear时,无论谁在前谁在后都会先执行removeclear,接下来就该分析commitapply了。

3.4 commit

这里先说个结论当我们调用putxxxremoveclear时,无论谁在前谁在后都会先执行removeclear,具体说明看后面代码。

//SharedPreferencesImpl.EditorImpl.java
    public boolean commit() {
        //提交到内存中,并返回一个MemoryCommitResult对象,具体看3.5
        MemoryCommitResult mcr = commitToMemory();
        //放入写入队列,具体看3.6
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
        try {
            //线程等待,直到写入文件操作后
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
        //通知监听
        notifyListeners(mcr);
        //返回提交结果
        return mcr.writeToDiskResult;
    }

3.5 commitToMemory

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

        synchronized (SharedPreferencesImpl.this.mLock) {
            if (mDiskWritesInFlight > 0) {
                // 这个时候说明正在写入文件,无法修改,写入文件之后会重置为0,具体看3.6
                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方法的操作,也验证了前面的结论,会先执行clear操作
                if (mClear) {
                    if (!mMap.isEmpty()) {
                        changesMade = true;
                        mMap.clear();
                    }
                    mClear = false;
                }

                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    // 还记得前面remove方法里面存放this就是为了判断remove的,
                    // 也验证了前面的结论,其次会执行remove
                    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;
                            }
                        }
                        //发生修改或者修改存入map
                        mMap.put(k, v);
                    }
                    // 发生改变
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
                //将修改的map置空
                mModified.clear();

                if (changesMade) {
                    // 当前提交到内存的次数
                    mCurrentMemoryStateGeneration++;
                }

                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        //将提交内存次数,发生修改的集合,监听,整个map封装成MemoryCommitResult
        return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                mapToWriteToDisk);
    }

3.6 enqueueDiskWrite

//SharedPreferencesImpl.java
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        // commit操作为true,apply为false
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        // 写入文件
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        // 写入完成后减1,和3.5节相照应来确保写入状态是否完成
                        mDiskWritesInFlight--;
                    }
                    //apply操作才会执行,具体看3.7节
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
        };

        // 前面也说了commit操作时才为true
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                // 和3.5节的这行代码mDiskWritesInFlight++相照应
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                // 执行写入文件
                writeToDiskRunnable.run();
                return;
            }
        }
        //apply操作才会执行,具体看3.8节
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

到这里也就看出来了commit是同步的。writeToFile方法就不贴出来了,简单说说它的作用:

  1. 当没有key发生改变,则直接返回;否则执行step2;
  2. mMap全部信息写入文件,如果写入成功则删除备份文件,如果写入失败则删除mFile,同时也就解答了之前为什么要备份文件的问题;
  3. 调用mcr.setDiskWriteResult设置写入文件是否成功,并让线程结束等待状态。

3.7 apply

//SharedPreferencesImpl.EditorImpl.java
    public void apply() {
        // 3.5节已讲过
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                public void run() {
                    try {
                        // 线程等待
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }
                }
            };
        // 将awaitCommit加入QueuedWork
        QueuedWork.addFinisher(awaitCommit);
        Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    awaitCommit.run();
                    QueuedWork.removeFinisher(awaitCommit);
                }
        };
        // 放入写队列
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        // 通知监听
        notifyListeners(mcr);
    }

3.8 queue

//QueuedWork.java
    // apply才会执行这个方法,并且第二个参数为true
    public static void queue(Runnable work, boolean shouldDelay) {
        // HandlerThread的handler
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

这个方法主要作用是延迟执行Runnable,从这也就看出来了apply是异步操作。结合3.6,3.7,3.8总结一下apply,不然看起来有点乱。

优化

  • SP文件不宜过大,如果SP文件需要存储的内容过多,可以根据不同的功能划分成多个文件;
  • 如果可以的话尽可能早的调用getSharedPreferences,这样在调用putget操作时,文件已经被读取到内存中了;
  • 不要多次调用edit(), 应该调用一次edit(),因为每次调用edit()都会新建一个Editor;
  • 不要多次调用commit()apply(),如果多次存入值,应该在最后一次调用。