SharedPreferences的分析与实践

Android 专栏收录该内容
44 篇文章 0 订阅

SharedPreferences 是 Android 里面一个轻量级别的存储方案,不过随着项目的发展,SharedPreferences 使用不当,也很容易引发一些问题,甚至会导致 Crash 的发生

因此,我们有必要搞清楚 SharedPreferences 的基本原理

SharedPreferences

SharedPreferences 的本质是一个基于xml文件存储的key-value键值对数据的文件操作工具类,如下:

<?xml version="1.0" encoding="utf-8"?>
<map>
    <String name="name">fritz</String>
</map>

它的基本使用如下:

SharedPreferences sp = context.getSharedPreferences(“sp”, Context.MODE_PRIVATE);
sp.edit().putString(“name”, fritz).commit();

直接在这里开始阅读吧:

 @Override
 public SharedPreferences getSharedPreferences(String name, int mode) {
       //....省去部分代码
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                //生成缓存xml文件file的map
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //获取缓存
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                //生成本地xml文件并写入缓存中
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

这里看到一个关键的对象 mSharedPrefsPaths :

/**
 * 用于存储xml文件的file的map
 */    
private ArrayMap<String, File> mSharedPrefsPaths;

好了,接着往下看:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //获取sp的cache
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            //得到sp的实现类
            sp = cache.get(file);
            //如果为null
            if (sp == null) {
                //...省去部分代码
                //生成sp的实现类
                sp = new SharedPreferencesImpl(file, mode);
                //存入缓存中
                cache.put(file, sp);
                return sp;
            }
        }
        //...省去部分代码
        return sp;
    }

可以看到另一个值得注意的对象 cache ,它是 File 映射到 SharedPreferences 的关键:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            //生成缓存的map
            sSharedPrefsCache = new ArrayMap<>();
        }
        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            //按照应用报名来做key,存入缓存中
            sSharedPrefsCache.put(packageName, packagePrefs);
        }
        return packagePrefs;
    }

sSharedPrefsCache 其实很简单:

 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

可以知道,它其实是针对包名来缓存 App 下面的多个 SharedPreferences

结合上面的代码,缓存流程如下:

在这里插入图片描述

SharedPreferencesImpl

SharedPreferencesImplSharedPreferences 的实现类,它在构造方法里面读取本地的 xml 文件并将其内容转为 map :

SharedPreferencesImpl(File file, int mode) {
        //sp操作的本地xml文件对象
        mFile = file;
   			//一个容灾文件,用于异常恢复,后缀为.bak
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
  			//sp全部的键值对数据,通过解析mFile得到数据
        mMap = null;
        mThrowable = null;
  			//异步解析本地的mFile并对mMap进行赋值
        startLoadFromDisk();
    }

可以看到 SharedPreferencesImpl 一开始就准备了一个异常恢复文件并且读取本地磁盘,追踪一下 startLoadFromDisk

private void loadFromDisk() {
        synchronized (mLock) {
          	//标记位来记录是否读取完成
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
              	//如果容灾文件存在就同步到mFile
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    //通过XmlUtils将内容赋值给map
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
           	//完成读取
            mLoaded = true;
            mThrowable = thrown;
            try {
                if (thrown == null) {
                    if (map != null) {
                        //map的赋值,这里就等于将xml内容转为了map
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                //激活其他正在等待的线程
                mLock.notifyAll();
            }
        }
    }

可以看出,构造方法这里准备了一个容灾文件并且通过 XmlUtils.readMapXml 将本地 xml 文件里面的数据全部存储到静态 mMap 里面去了

这也是为什么 Google 反复强调 SharedPreferences 只能存储轻量级别的数据,因为 App 下的全部 SharedPreferences 以及对应的 File 都已经给静态存储起来了,也就是说 xml 数据会一直存在内存之中了,试想下如果在 xml 里面存入大量的数据,那么对应也会消耗非常大的内存了,这点必须注意

不过将数据都存入内存中,优势也很明显,那就是读取速度非常快。唯一需要注意的就是每次读操作都要通过 mLoaded 判断 mMap 是否初始化完成了:

@Override
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
       awaitLoadedLocked();
       String v = (String)mMap.get(key);
       eturn v != null ? v : defValue;
    }
}

private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

可以看到,如果 mMap 没有完成初始化,那么线程就会一直卡在直到初始化完成为止,如果当前线程是主线程,而 xml 数据量也比较大的情况下,就可能会发生 ANR 了

因此,我们使用 SharedPreferences 时,文件必须足够小,我们可以针对不同的功能需求来创建复数以上的 xml 文件;比分说我们需要在启动页里面通过 SharedPreferences 配置是否打开 App 的引导页面,就创建一个只用于 app 初始化的 SharedPreferences ,更小的 SharedPreferences 可以更快完成初始化,加快 app 的启动速度

Editor

SharedPreferences 的读取操作就只是简单的 map 取值,但是写入操作就不是了,全部的写入修改操作都是通过 Editor 对象,而 Editor 的实质是 EditorImpl :

 public final class EditorImpl implements Editor {
   			//锁
        private final Object mEditorLock = new Object();
				//一个用于diff的 map
        private final Map<String, Object> mModified = new HashMap<>();
				//一个是否清空数据的 flag
        private boolean mClear = false;
   
   			@Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        //......
}

写入操作时,不是直接对 mMap 进行操作,而是先写入 mModified 中,如果忘记调用 commit 或者 apply 方法,数据其实并没有写入磁盘,在写入磁盘后将会清空 mModified ,因此可以考虑将其单例化

mModified只是个用来修改 mMap 的diff 工具,真正用于修改本地数据的操作是 commit()apply() 这两个方法;它们的区别就是 apply() 是异步将数据写入本地存储,commit() 则是直接写入本地存储,但是会返回一个是否操作成功的布尔值

从效率考虑的话, apply() 的效率是比 commit() 要高的;如果你很重视写入操作是否成功,需要做一些补救措施,那就使用 commit()

下面来分析下它们的源码实现吧

commit
@Override
public boolean commit() {
    //提交到内存中
    MemoryCommitResult mcr = commitToMemory();
    //执行写入本地存储到操作,传入 mcr 对象
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        //开启 mcr 里面的闭锁
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } 
    //锁释放了,发送通知
    notifyListeners(mcr);
    //锁释放后,返回操作结果
    return mcr.writeToDiskResult;
}
apply
@Override
public void apply() {
   //提交到内存中
   final MemoryCommitResult mcr = commitToMemory();
   final Runnable awaitCommit = new Runnable() {
             @Override
             public void run() {
                try {
                  //开启 mcr 里面的闭锁
                   mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
              } 
            };
   //添加一个Finisher
   QueuedWork.addFinisher(awaitCommit);
   Runnable postWriteRunnable = new Runnable() {
             @Override
             public void run() {
                   //启动闭锁
                   awaitCommit.run();
                   // 任务完成后移除Finisher
                   QueuedWork.removeFinisher(awaitCommit);
                }
             };
   //执行写入本地存储到操作,传入 mcr 对象和 postWriteRunnable
   SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
   //发送通知
   notifyListeners(mcr);
}

关于 QueuedWork ,推荐阅读这篇文章 QueuedWork 的源码分析 ,在这里 QueuedWork 就是个安全锁, QueuedWork 有个 waitToFinish 方法在 Activity 进入 onStop() 时回调,而 waitToFinish 会执行队列中的 Runnable 方法,触发了闭锁来阻塞主线程,这就可以确保 Activity 销毁之前将内容写到本地存储里面去

写入本地的操作

不管是 commit 还是 apply ,其核心方法都是 commitToMemoryenqueueDiskWrite ,下面我们来看下源码;

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

            synchronized (SharedPreferencesImpl.this.mLock) {
                //说明至少有1个线程在使用 mMap
                if (mDiskWritesInFlight > 0) {
                    //当前是多线程的情况,直接修改 mMap 会出现写入混乱的情况
                    //此时的内存修改需要克隆获得新拷贝,然后再修改拷贝的内容
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                 //写入任务数递增,多线程的话会大于1
                mDiskWritesInFlight++;
                //是否有监听器
                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                  	//获取所有被修改的keys
                    keysModified = new ArrayList<String>();
                    //获取所有注册的修改事件监听器
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    //是否有数据修改的flag
                    boolean changesMade = false;
                    //如果有清空设置,那么清空本地缓存
                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            //把本地存储数据全部置空
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }
  									//对比新旧数据进行更新修改
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        if (v == this || v == null) {
                          	//检查 mapToWriteToDisk 中是否包含这个 key,没有则检查下一个 key
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                          	//删除空数据
                            mapToWriteToDisk.remove(k);
                        } else {
                             //检查是否需要修改原有的数据
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                  	//相同的数据,不要修改本地存储
                                    continue;
                                }
                            }
                          	//更新数据
                            mapToWriteToDisk.put(k, v);
                        }
                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }
										//清空存储diff数据的map
                    mModified.clear();
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
  					//返回一个 MemoryCommitResult 对象
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
}

上面代码也很简单,就是把 mMap 暂存的修改提交到 mapToWriteToDisk 并且让 mapToWriteToDiskmModified 进行对比合并差异数据,最终存入到 MemoryCommitResult 里面:

在这里插入图片描述

接着看下这个 MemoryCommitResult 的源码:

private static class MemoryCommitResult {
        final long memoryStateGeneration;
        final List<String> keysModified;
        final Set<OnSharedPreferenceChangeListener> listeners;
        //写入本地存储的 Map 
        final Map<String, Object> mapToWriteToDisk;
        //闭锁,用于阻塞线程直到写入操作结束后解锁
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
         //写入操作是否成功
        volatile boolean writeToDiskResult = false;
  			// 是否有实际内容写入本地存储
        boolean wasWritten = false;

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

        void setDiskWriteResult(boolean wasWritten, boolean result) {
          	//是否有写入内容的结果
            this.wasWritten = wasWritten;
            //得到是否写入操作成功的结果
            writeToDiskResult = result;
          	//解锁
            writtenToDiskLatch.countDown();
        }
    }

可以看出, MemoryCommitResult 主要是用来记录操作结果,保管写入磁盘的数据以及给写入操作加一把锁和解锁,下面我们来看下具体的写入操作了

enqueueDiskWrite

参数 postWriteRunnable 非空说明是 apply() 的执行逻辑,反之是 commit() 的执行逻辑

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
        //flag,用于判断是否 commit 调用
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        //执行写入操作的 Runnable
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //写入本地存储
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        //写入结束后,操作写入的线程数减1
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                      	//上锁直到写入操作完成后解锁
                        //apply 的执行逻辑
                        postWriteRunnable.run();
                    }
                }
            };
				
        if (isFromSyncCommit) {
            //当前是 commit 的执行逻辑
            boolean wasEmpty = false;
            synchronized (mLock) {
                //判断当前是否处于多线程的环境
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                //如果不是,那么commit 就直接执行写入操作
                writeToDiskRunnable.run();
                return;
            }
        }
				//如果是多线程,使用队列确保写入任务的执行顺序
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

可以看出,不管是 commit() 还是 apply() 都是利用 CountDownLatch 来确保写入操作可以顺利完成的,同时让在多线程中执行写入操作不影响写入任务的执行顺序,同时看下 QueuedWork.queue 的逻辑:

private static final long DELAY = 100;

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();
						//得到一个异步的Handler
            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

public static void queue(Runnable work, boolean shouldDelay) {
        //获取一个异步的Handler对象
        Handler handler = getHandler();

        synchronized (sLock) {
          	//存储 Runnable 任务的队列
            sWork.add(work);
            if (shouldDelay && sCanDelay) {
                //如果是 apply ,那么就延时 100 ms 执行 Runnable
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
              	//如果是 commit ,那么立即执行 Runnable
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

这里就知道了 applay 是怎么异步进行写入操作了,也可以看出 commit()apply() 执行的各自不同的执行逻辑了:

在这里插入图片描述

最终写入到本地的方法都是 writeToFile :

 private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        
        // 原文件是否存在
        boolean fileExists = mFile.exists();
        // 重命名当前文件,以便在下次读取时将其用作备份
        if (fileExists) {
            //是否需要执行写入操作的标记位
            boolean needsWrite = false;
            //如果磁盘状态值小于此提交的状态值,则只需要写入
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                // commit()写入
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    // apply()异步写入
                    synchronized (mLock) {
                        // 中间状态不需要持久化,仅持久化最新状态
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
						//不需要执行写入操作
            if (!needsWrite) {
              	//提示结束了,设置结果值并解锁
                mcr.setDiskWriteResult(false, true);
                return;
            }
            //容灾文件是否存在
            boolean backupFileExists = mBackupFile.exists();
            //容灾文件不存在
            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();
            }
        }
    		//尝试写入文件、删除备份和返回true时,尽可能做到原子性
    		//如果出现任何异常则删除新文件,并在下一次从容灾文件中恢复
        try {
            //创建文件输出流
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
              	//文件输出流创建失败,无法执行写入操作
                mcr.setDiskWriteResult(false, false);
                return;
            }
            //把内存中需要写入的数据按照 xml 格式写入到 str 流
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
						//同步流,也就是执行写入操作
            FileUtils.sync(str);
						//关闭流
            str.close();
          	//修改文件权限模式
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            //写入操作顺利完成,删除容灾文件
            mBackupFile.delete();
						//磁盘状态值更新
            mDiskStateGeneration = mcr.memoryStateGeneration;
						//设置结果并解锁
            mcr.setDiskWriteResult(true, true);
            //同步次数+1
            mNumSync++;
            //结束函数
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }
        // 写入操作出现异常,删除已写入文件
    		// 下次读取内容发现mFile不存在,会检查容灾文件并恢复mFile
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
   			//设置结果值并解锁
        mcr.setDiskWriteResult(false, false);
    }

从上面代码可以看出每次执行写入本地存储时,流程如下:

  1. 将之前的 File 文件备份为容灾文件( .bak 格式的文件)
  2. 拿到输出流进行写入操作
  3. 写入操作完成,删除容灾文件并解锁
  4. 如果出现写入操作异常,那么删除原来的 xml 文件,下次写入直接从容灾文件开始恢复

也就是说写入操作成果与否全看容灾文件是否存在,如果需要百分百确定写入操作是否成功了,可以检查下容灾文件( .bak 格式的文件)是否存在

使用的建议

上面我们已经对 SharedPreferences 的源码有了深刻的了解,也明白如果使用不当的话会造成各种异常,因此这里就 SharedPreferences 提出下面的使用建议:

  1. 避免在 SharedPreferences 中存储大量的数据,因为你存储的全部数据都是保存到 App 的内存里面;而且每一次 commit()apply() 都是创建一个空的文件并一次性写入到本地,因此数据越大耗时就越久,就越容易产生 ANR
  2. 每一次 commit()apply() 都有一定的性能消耗,因此不要多次提交修改,将全部的修改集中在一起,一次性提交
  3. 针对使用频率和功能模块来划分不同的 SharedPreferences 文件,更小的文件意味着更快的加载速度
  4. 要注意到 SharedPreferences 的第一次初始化也是个耗时操作,我们可以将它放在子线程中或者启动页面的 onCtreate 方法中,尽可能地提前初始化,这样子 SharedPreferences 就会建立好缓存了
  5. 每次调用 edit() 都会创建一个新的 EditorImpl 对象,不要频繁调用 edit() ,如果一个需要多次用到 EditorImpl 对象,可以将其变成全局变量,也可以考虑将其变成单例

参考内容
1.SharedPreferences最佳实践
2. Android工程化最佳实践 书中的SharedPreferences 章节

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值