美文网首页Android开源框架源码研习
Android SharedPreferences 源码分析

Android SharedPreferences 源码分析

作者: 看我眼前007 | 来源:发表于2017-11-17 18:01 被阅读421次
    从源码角度,了解 SharedPreferences 的实现原理,明白为什么 SharedPreferences 会触发 ANR

    简介

    SharedPreferences 是 Android 中比较常用的数据存储方式,主要用来保存相对较小的『键值集合』。
    SharedPreferences 的 API 使用简单方便,如果不了解 SharedPreferences 的实现原理,
    难免会在使用的时候产生各种莫名其妙的问题。本文将从源码的角度,剖析 SharedPreferences 实现原理。
    

    使用方式

    1. 获得 SharedPreferences 对象

       context.getSharedPreferences(String name, int mode);
      

      也可以在 Activity 中使用

       getSharedPreferences(String name, int mode)
      
    2. 获得 Editor 对象

       SharedPreferences.Editor editor = sp.edit();
      
    3. 写入数据

       editor.putInt(key, value);
      
    4. 提交数据

       editor.commit();
       editor.apply();
      
    5. 读取数据

       sp.getString(key,defValue)
      

    源码分析

    先从获得 SharedPreferences 入手

    Activity.getSharedPreferences(String name, int mode) 继承自 ContextWrapper

        public SharedPreferences getSharedPreferences(String name, int mode) {
            return mBase.getSharedPreferences(name, mode);
        }
    

    其中 mBase 是 Context 对象。

    所以我们最后还是从 context.getSharedPreferences(String name, int mode) 入手

    Context 是一个抽象类,具体的实现类是ContextImpl.java,不了解部分的知识可以参考 Context都没弄明白,还怎么做Android开发?

    public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }
            ……
            sp = packagePrefs.get(name);
            if (sp == null) {
                File prefsFile = getSharedPrefsFile(name);
                sp = new SharedPreferencesImpl(prefsFile, mode);
                packagePrefs.put(name, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            ……
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }
    

    看下 getSharedPrefsFile(name) 方法

    public File getSharedPrefsFile(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    
    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
            }
            return mPreferencesDir;
        }
    }
    

    这里看出

    1. getSharedPreferences() 返回的是 SharedPreferencesImpl 对象。
    2. 首次调用 getSharedPreferences() 会初创建 SharedPreferencesImpl 对象,并缓存。
    3. getSharedPreferences() 方法里面有 synchronized 锁。
    4. SharedPreferences 存储数据的文件为 xml 文件。
    

    通过上面三条可以得出结论

    getSharedPreferences() 首次调用需要做文件操作,比较耗时。此时执行 SharedPreferences 其他方法可能会出现问题。
    

    SharedPreferencesImpl 对象

    先看一下构造函数

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

    makeBackupFile 只是创建了一个文件

    private static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    

    再看 startLoadFromDisk()

    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }
    
    private void loadFromDiskLocked() {
       if (mLoaded) {
           return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
        ……
        Map map = null;
        StructStat stat = null;
        
        ……
                    map = XmlUtils.readMapXml(str);
        ……
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<String, Object>();
        }
        notifyAll();
    }
    

    这里就可以看到,通过 XmlUtils.readMapXml(str) 把 xml 文件转换为一个 HashMap。

    总结一下流程图如下

    sharePreferences_01.png

    获得 Editor 对象

    public Editor edit() {
        synchronized (this) {
            awaitLoadedLocked();
        }
    
        return new EditorImpl();
    }
    
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }
    

    这里可以看出 edit() 返回的其实是一个 EditorImpl 对象,如果执行 edit() 时如果 SharedPreferencesImpl 没有加载完成,就会阻塞。

    EditorImpl 构造函数没有什么特殊之处。

    写入数据

    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            mModified.put(key, value);
            return this;
        }
    }
    

    写入数据比较简单,就是把数据存储到 mModified 中

    private final Map<String, Object> mModified = Maps.newHashMap();
    

    提交数据

    写入数据以后并不会立刻被存储到文件之中,需要经过『提交』操作才能写入文件。

    SharedPreferences 有两种提交方式 editor.commit()editor.apply()

    editor.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() 方法

    // Returns true if any changes were made
    private MemoryCommitResult commitToMemory() {
        MemoryCommitResult mcr = new MemoryCommitResult();
        synchronized (SharedPreferencesImpl.this) {
            ……
            if (mDiskWritesInFlight > 0) {
                ……
                mMap = new HashMap<String, Object>(mMap);
            }
            ……
            synchronized (this) {
              ……
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    ……
                    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;
    }
    

    这里可以看出 commitToMemory 是把 EditorImpl 中的 mModified 做一个深层拷贝给 SharedPreferencesImpl 的 mMap 对象。

    再看

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

    可以看到因为 postWriteRunnable ==null ,所以 任务会在当前线程直接提交。

    private void writeToFile(MemoryCommitResult mcr) {
        // Rename the current file so it may be used as a backup during the next read
        if (mFile.exists()) {
            ……
        }
            ……
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            ……
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            FileUtils.sync(str);
        ……
    }
    

    writeToFile 及时把 map 对象存储的信息写入到文件中。

    editor.apply()
       public void apply() {
           final MemoryCommitResult mcr = commitToMemory();
           final Runnable awaitCommit = new Runnable() {
                   public void run() {
                       try {
                           mcr.writtenToDiskLatch.await();
                       } catch (InterruptedException ignored) {
                       }
                   }
               };
    
          QueuedWork.add(awaitCommit);
    
          Runnable postWriteRunnable = new Runnable() {
                   public void run() {
                       awaitCommit.run();
                       QueuedWork.remove(awaitCommit);
                   }
               };
    
          SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
          ……
          notifyListeners(mcr);
       }
    

    apply() 和 commit() 的区别就是执行 enqueueDiskWrite(mcr, postWriteRunnable) 方法的时候 postWriteRunnable 不为空,

    所以 apply() 方法提交数据的时候是在 QueuedWork 维护的单线程池中。

    读取数据

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

    读取数据比较简单,就是从 SharedPreferencesImpl 的 mMap 中获得数据。

    最后用一个集合以上操作的时序图总结。

    sharePreferences_02.png sharePreferences_03.png

    参考资料

    SharedPreferencesImpl.java 源码

    ContextImpl.java 源码

    Android SharedPreferences源码视角

    Context都没弄明白,还怎么做Android开发?

    相关文章

      网友评论

        本文标题:Android SharedPreferences 源码分析

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