美文网首页
Android SharedPreferences 全面分析

Android SharedPreferences 全面分析

作者: 简书汪 | 来源:发表于2019-04-01 22:01 被阅读0次

    我们经常用SharedPreferences用来存储一些比较小的键值对集合,适合保存应用的配置参数, 我们将会带着以下几个问题来分析SharedPreferences的源码实现:

    • 数据是如何保存到磁盘的
    • commit() 和apply()的区别
    • 为什么会造成ANR
    • SharedPreferences有哪些缺点

    源码分析

    本文参照Android-26的源码,并不介绍SharedPreferences的基础使用,而是从源码角度来分析它的原理

    获取SharedPreferences

    我们通过以下方法来获取SharedPreferences实例

    1. context.getSharedPreferences
    2. 在Activity中getSharedPreferences
    3. PreferenceManager.getDefaultSharedPreferences
      这三种方法最终都会调用到 ContextImpl.getSharedPreferences
      @Override
        public SharedPreferences getSharedPreferences(String name, int mode) {
            //SharedPreferences对应的xml文件,数据保存在其中
            File file;
            synchronized (ContextImpl.class) {
                ...//省略
                file = mSharedPrefsPaths.get(name);
                if (file == null) { 
                    //如果没有该name命名的文件,则新建一个并放入缓存
                    file = getSharedPreferencesPath(name);
                    mSharedPrefsPaths.put(name, file);
                }
            }
            return getSharedPreferences(file, mode);
        }
    
     @Override
        public SharedPreferences getSharedPreferences(File file, int mode) {
            ...//省略
            SharedPreferencesImpl sp;
            synchronized (ContextImpl.class) {
                final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
                sp = cache.get(file);
                if (sp == null) {
                    sp = new SharedPreferencesImpl(file, mode);
                    cache.put(file, sp);
                    return sp;
                }
            }
           //mode设置为多进程模式时会检测SP文件最后修改的时间和大小,如果文件被其他进程改变时,则会重新加载
            if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
                getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
                sp.startReloadIfChangedUnexpectedly();
            }
            return sp;
        }
    

    可以看到最终返回的是一个SharedPreferencesImpl对象,首先getSharedPreferencesCacheLocked()从一个静态的ArrayMap中获取SharedPreferences 缓存,如果有缓存中有SharedPreferencesImpl对象则返回,没有的话则创建一个并存入缓存中,同时synchronized 包裹可以保证多线程同步,由此可见无论getSharedPreferences调用多少次,返回的都是一个SharedPreferencesImpl对象

    SharedPreferencesImpl

    SharedPreferencesImpl 实现了SharedPreferences这个接口,是我们通过getSharedPreferences得到的实体对象,所有存取操作都由该类来实现

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

    mBackupFile 代表发生异常时, 可通过备份文件来恢复数据.
    mLoaded 表示是否已经将mFile中的数据都读取到mMap 中
    mMap 用于在内存中缓存我们的配置数据, 也就是 getXxx 数据的来源
    startLoadFromDisk()从方法名即可看出是从硬盘中读取数据,看一下源码

     private void startLoadFromDisk() {
            synchronized (mLock) {
                mLoaded = false;
            }
            new Thread("SharedPreferencesImpl-load") {
                public void run() {
                    loadFromDisk();
                }
            }.start();
        }
     private void loadFromDisk() {
            synchronized (mLock) {
                if (mLoaded) {
                    return;
                }
                if (mBackupFile.exists()) {
                    mFile.delete();
                    mBackupFile.renameTo(mFile);
                }
            }
            //...省略
            Map map = null;
            BufferedInputStream    str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
            map = XmlUtils.readMapXml(str);
           //...省略
            synchronized (mLock) {
                mLoaded = true;
                if (map != null) {
                    mMap = map;
                } else {
                    mMap = new HashMap<>();
                }
                mLock.notifyAll();
            }
        }
    

    开启一个子线程来从硬盘读取数据,如果备份文件存在则直接使用灾备文件回滚,使用XmlUtils把文件所有的数据读取到内存中的mMap中,mLoaded = true 标志SharedPreferencesImpl已经将数据读取完成,notifyAll()唤醒getXXX系列方法等待状态的线程,由于已经将数据中磁盘读取到内存中,此时调用getXXX系列方法就可以获取值了

    getString分析

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

    synchronized 关键字保证了线程安全,然后直接从mMap中获取对应的键值对就可以了,当我们调用getSharedPreferences 之后马上调用getString方法有可能SharedPreferencesImpl在子线程中还没有将文件中的数据读取完,此时mMap 还没有被赋值,所以awaitLoadedLocked()将会阻塞当前线程,直到读取完毕

    private void awaitLoadedLocked() {
            while (!mLoaded) {
                try {
                    mLock.wait();
                } catch (InterruptedException unused) {
                }
            }
        }
    

    mLoaded为false表示尚未读取完成,其他的getXXX系列方法和getString如出一辙,都是先等待文件读取完毕,然后从mMap中获取相应的value

    数据保存

    我们通过getSharedPreferences().edit()来put各种值,看一下.edit()获取的是一个什么对象

    public Editor edit() {
            synchronized (mLock) {
                awaitLoadedLocked();
            }
            return new EditorImpl();
        }
    

    保证磁盘读取完毕后,返回了一个新的EditorImpl对象

      public final class EditorImpl implements Editor {
            private final Object mLock = new Object();
            private final Map<String, Object> mModified = Maps.newHashMap();
            private boolean mClear = false;
    
            public Editor putString(String key, @Nullable String value) {
                synchronized (mLock) {
                    mModified.put(key, value);
                    return this;
                }
            }
          
            public Editor putInt(String key, int value) {
                synchronized (mLock) {
                    mModified.put(key, value);
                    return this;
                }
            }
    
         public Editor remove(String key) {
                synchronized (mLock) {
                    mModified.put(key, this);
                    return this;
                }
            }
          ...//省略
    }
    

    EditorImpl 中有两个重要属性,mModified 用来暂时保存put方法提供的值,当调用commit()或者apply()才会将mModified中的数据存储到mMap,进而保存到磁盘中,mClear标志是否要清空文件中所有数据。接下来需要注意看remove()方法,调用getSharedPreferences().edit().remove()时是将当前key的value置为this,删除数据时检测到value为this即可删除
    总结:调用put()后,数据只是暂存到了EditorImpl 的mModified** 对象中,并没有回写到磁盘,调用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;
                } 
                return mcr.writeToDiskResult;
            }
    

    主要有三步

    • commitToMemorymModified 中的数据写到内存mMap
    • SharedPreferencesImpl.this.enqueueDiskWrite 将内存中mMap的数据回写到磁盘中
    • mcr.writtenToDiskLatch.await() 线程等待,直到回写磁盘完毕
    1. commitToMemory()
      我们逐个分析,首先分析commitToMemory()返回一个MemoryCommitResult对象,代表了提交到内存的返回结果
     private static class MemoryCommitResult {
               //...省略代码
            final Map<String, Object> mapToWriteToDisk;
            //此处初始换CountDownLatch 的计数器为1
            final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
    
            volatile boolean writeToDiskResult = false;
            boolean wasWritten = false;
    
            void setDiskWriteResult(boolean wasWritten, boolean result) {
                this.wasWritten = wasWritten;
                writeToDiskResult = result;
                writtenToDiskLatch.countDown();
            }
        }
    

    其中关键有 writtenToDiskLatch 是一个 CountDownLatch 对象,它允许一个或多个线程一直等待,直到回写磁盘线程的操作执行完后再执行,mapToWriteToDisk引用内存中的mMapwriteToDiskResult代表回写磁盘是否成功,接下来继续分析commitToMemory()

    private MemoryCommitResult commitToMemory() {
                Map<String, Object> mapToWriteToDisk;
                synchronized (SharedPreferencesImpl.this.mLock) {
                    mapToWriteToDisk = mMap;
                    //需要写入磁盘次数+1
                    mDiskWritesInFlight++;
                    synchronized (mLock) {
                        if (mClear) {
                            //...省略代码,
                            //如果调用了edit().clear()则清空内存中的数据
                            mMap.clear();
                            mClear = false;
                        }
                        
                        //将putXXX()的数据提交到内存中
                        for (Map.Entry<String, Object> e : mModified.entrySet()) {
                            String k = e.getKey();
                            Object v = e.getValue();
                            //value为this则删除,与之前的getSharePreferences().edit().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;
                                    }
                                }
                                mMap.put(k, v);
                            }
                        }
                        mModified.clear();
                    }
                }
                return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                        mapToWriteToDisk);
            }
    

    mDiskWritesInFlight代表写入磁盘这个操作的次数,也是由synchronized 保证线程安全,首先判断是否需要clear,如果需要这把mMap中的数据清空,需要注意此时mModified中数据还没有复制到mMap中,所以以下代码并不能将"foo" clear掉

    sharedPreferences.edit()
            .putBoolean("foo";, true)        // foo 无法被 clear 掉
            .clear()
            .putBoolean("bar", true)
            .commit()
    

    然后通过for循环将put到mModified中的数据添加到mMap中,mModified.clear()之后返回MemoryCommitResult
    总结commitToMemory()只是将数据都写入到内存中

    1. SharedPreferencesImpl.this.enqueueDiskWrite
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable 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();
                        }
                    }
                };
            if (isFromSyncCommit) {
                boolean wasEmpty = false;
                synchronized (mLock) {
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                if (wasEmpty) {
                    writeToDiskRunnable.run();
                    return;
                }
            }
            //异步执行任务
            QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
        }
    

    commit() 时postWriteRunnable参数为null,所以isFromSyncCommit == true,进入到if (isFromSyncCommit) 语句中,如果此时只有一个commit()操作,则直接在当前线程执行writeToFile()将内存中的数据回写到磁盘中,如果此时有多个commit()则,排队进入QueuedWork中等待执行,看一下writeToFile()的实现

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
            //...省略
            boolean fileExists = mFile.exists();
            boolean backupFileExists = mBackupFile.exists();
                if (!backupFileExists) {
                    if (!mFile.renameTo(mBackupFile)) {
                        mcr.setDiskWriteResult(false, false);
                        return;
                    }
                } else {
                    mFile.delete();
                }
            }
            try {
                FileOutputStream str = createFileOutputStream(mFile);
                if (str == null) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
                XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
                str.close();
                mBackupFile.delete();
                mcr.setDiskWriteResult(true, true);
                return;
            } catch (Exception e) {
            }
            //如果写入操作出现异常,则将半成品删掉
            if (mFile.exists()) {
                if (!mFile.delete()) {
                    Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
                }
            }
            mcr.setDiskWriteResult(false, false);
        }
    
    • 将之前的配置文件mFile备份为buckup文件,然后删除
    • mcr.mapToWriteToDisk即内存中数据,全部写入到新的mFile
    • 写入成功,删掉备份文件,如果写入失败则把半成品mFile删掉
    1. mcr.writtenToDiskLatch.await()
      CountDownLatch.await()会阻塞当前线程,直到CountDownLatch.countDown()使计数器值到达0时,它表示磁盘写入线程已经完成了任务,然后在锁上等待的线程就可以恢复执行任务。在writeToFile()中,写入完成之后会调用mcr.setDiskWriteResult()中的writtenToDiskLatch.countDown()
     void setDiskWriteResult(boolean wasWritten, boolean result) {
                this.wasWritten = wasWritten;
                writeToDiskResult = result;
                writtenToDiskLatch.countDown();
            }
    

    writtenToDiskLatch初始时计数器为1,countDown()之后为0,此时磁盘已经回写完毕,commit()方法继续执行,返回结果
    commit()总结

    • 流程是先写入内存写入磁盘
    • 写入磁盘完成之前调用线程会一直等待,直到内存和磁盘都已经同步完毕
    • 每次写入磁盘时都会从内存中将所有数据都全量写入,效率并不高

    apply()

           public void apply() { 
                //第一步:提交到内存
                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);
    
                Runnable postWriteRunnable = new Runnable() {
                        public void run() {
                            awaitCommit.run();
                            QueuedWork.removeFinisher(awaitCommit);
                        }
                    };
                // 第三步:写入磁盘
                SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            }
    
    • 第一步提交到内存和commit()是一样的
    • 第二步中将任务mcr.writtenToDiskLatch.await()提交到QueuedWork之中,该任务的作用是让线程等待,而释放的时机跟commit()一样(详细代码看上述commit()),但是QueuedWork.addFinisher()将线程等待的任务提交之后并没有立即运行,而是保存在了一个队列之中,当应用收到系统广播,或者被调用 onPause 等一些时机才会运行(详情查看QueuedWork源码,在ActivityThread中可以找到调用任务的方法waitToFinish())
    • 第三步同commit,不同点在于enqueueDiskWrite(mcr, postWriteRunnable)传递了Runnable,在异步线程中写入磁盘
      apply总结
    • 异步写入磁盘,没有等待结果,直接返回
    • 应用收到系统广播,或者被调用 onPause等时机,如果磁盘写入未完成则主线程会等待其完成
    • commit()写入过程一样,都是全量写入

    SharedPreferences总结

    通过上文对SharedPreferences分析,我们已经可以对开头的几个问题进行回答并总结了

    • 数据是如何保存到磁盘的
      答:通过putXXX系列方法将数据先保存到内存中,调用commit()或者apply(之后将所有数据全量写入磁盘文件中
    • commit() 和apply()的区别
      答:commit()线程同步写入,写入完成时才会返回,如果在主线程调用,写入过程比较费时可能会阻塞主线程
      apply异步线程写入,但是应用收到系统广播,或者被调用 onPause等时机,未完成写入任务时主线程会等待其完成
    • commit()和apply()相同点
      答:都是全量写入,如果SharedPreferences中数据量很多,则每次写入都会很慢
    • 为什么会造成ANR
      答:commit()和apply()都可能在成ANR,分析如上
    • SharedPreferences有哪些缺点
      答:1. 全量写入:commit() 还是 apply(),即使我们只改动其中一条数据,都会把整个数据写入到文件中
      2. 卡顿:commit() 还是 apply()都有可能造成ANR
      3. 跨进程不安全:MODE_MULTI_PROCESS已被谷歌标为Deprecated
      总之:系统提供的 SharedPreferences 的应用场景是用来存储一些简单、轻量的数据,例如配置文件等,不适合json、html等,并且每个SharedPreference不宜过大,考虑将频繁修改的配置项单独隔离

    相关文章

      网友评论

          本文标题:Android SharedPreferences 全面分析

          本文链接:https://www.haomeiwen.com/subject/hxtlixtx.html