美文网首页Android技术知识Android进阶之路Android开发
Android SharedPreferences 实现原理解析

Android SharedPreferences 实现原理解析

作者: Android架构 | 来源:发表于2019-02-18 21:23 被阅读7次

    序言

    Android 中的 SharedPreference 是轻量级的数据存储方式,能够保存简单的数据类型,比如 String、int、boolean 值等。其内部是以 XML 结构保存在 /data/data/包名/shared_prefs 文件夹下,数据以键值对的形式保存。下面有个例子:

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
        <float name="isFloat" value="1.5" />
        <string name="isString">Android</string>
        <int name="isInt" value="1" />
        <long name="isLong" value="1000" />
        <boolean name="isBoolean" value="true" />
        <set name="isStringSet">
            <string>element 1</string>
            <string>element 2</string>
            <string>element 3</string>
        </set>
    </map>
    

    这里不讨论 API 的使用方法,主要是从源码角度分析 SharedPreferences (以下简称 SP) 的实现方式。

    1. 初始化

    首先我们使用 context 的 getSharedPreferences 方法获取 SP 实例,它是一个接口对象。
    SharedPreferences testSp = getSharedPreferences("test_sp", Context.MODE_PRIVATE);
    Context 是一个抽象类,其核心实现类是 ContextImpl ,找到里面的 getSharedPreferences 方法。

        @Override
        public SharedPreferences getSharedPreferences(String name, int mode) {
            SharedPreferencesImpl sp;
            synchronized (ContextImpl.class) {
                if (sSharedPrefs == null) {
                    // sSharedPrefs 是 ContextImpl 的静态成员变量,通过 Map 维护着当前包名下的 SP Map 集合
                    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) {
                    // name 参数为 null 时,文件名使用 null.xml
                    if (name == null) {
                        name = "null";
                    }
                }
    
                sp = packagePrefs.get(name);
                if (sp == null) {
                    // SP 集合是一个以 SP 的名字为 key , SP 为值的 Map
                    File prefsFile = getSharedPrefsFile(name);
                    // SP 的实现类是 SharedPreferencesImpl
                    sp = new SharedPreferencesImpl(prefsFile, mode);
                    packagePrefs.put(name, sp);
                    return sp;
                }
            }
            // Android 3.0 以下或者支持 MODE_MULTI_PROCESS 模式时,如果文件被改动,就重新从文件读取,实现多进程数据同步,但是实际使用中效果不佳,可能会有很多坑。
            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;
        }
    

    首次使用 getSharedPreferences 时,内存中不存在 SP 以及 SP Map 缓存,需要创建 SP 并添加到 ContextImpl 的静态成员变量(sSharedPrefs)中。
    下面来看 SharedPreferencesImpl 的构造方法,

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

    makeBackupFile 用来定义备份文件,该文件在写入磁盘时会用到,继续看 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);
            }
    
            // 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 = Os.stat(mFile.getPath());
                if (mFile.canRead()) {
                    BufferedInputStream str = null;
                    try {
                        str = new BufferedInputStream(
                                new FileInputStream(mFile), 16*1024);
                        // 从 XML 里面读取数据返回一个 Map,内部使用了 XmlPullParser
                        map = XmlUtils.readMapXml(str);
                    } 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();
    }
    

    看到这,基本明白了 getSharedPreferences 的原理,应用首次使用 SP 的时候会从磁盘读取,之后缓存在内存中。

    2. 读数据

    下面分析 SP 读取数据的方法,就以 getString 为例。

    @Nullable
    public String getString(String key, @Nullable String defValue) {
          synchronized (this) {
              awaitLoadedLocked();
              String v = (String)mMap.get(key);
              return v != null ? v : defValue;
          }
    }
    
    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) {
              }
          }
    }
    

    首先取得 SharedPreferencesImpl 对象锁,然后同步等待从磁盘加载数据完成,最后返回数据。这里有个问题,如果单个 SP 存储的内容过多,导致我们使用 getXXX 方法的时候阻塞,特别是在主线程调用的时候,所以建议在单个 SP 中尽量少地保存数据,虽然操作时间是毫秒级别的,用户基本上感觉不到。

    3. 写数据

    SP 写入数据的操作是通过 Editor 完成的,它也是一个接口,实现类是 EditorImpl,是 SharedPreferencesImpl 的内部类。
    通过 SP 的 edit 方法获取 Editor 实例,等到加载完毕直接返回一个 EditorImpl 对象。

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

    比如我们要保存某个 String 的值,调用 putString 方法。

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

    mModified 是一个 editor 中的一个 Map,保存着要修改的数据,在将改动保存到 SP 的 Map(变量 mMap,里面保存着使用中的键值对 ) 后被清空。put 完成后就要调用 commit 或者 apply 进行保存。

            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;
            }
    
            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);
    
                // Okay to notify the listeners before it's hit disk
                // because the listeners should always get the same
                // SharedPreferences instance back, which has the
                // changes reflected in memory.
                notifyListeners(mcr);
            }
    

    可以看到,commit 和 apply 操作首先执行了 commitToMemory,顾名思义就是提交到内存,返回值是 MemoryCommitResult 类型,里面保存着本次提交的状态。然后 commit 调用 enqueueDiskWrite 会阻塞当前线程,而 apply 通过封装 Runnable 把写磁盘之后的操作传递给 enqueueDiskWrite 方法。

            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.
                    // mDiskWritesInFlight  表示准备操作磁盘的进程数
                    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);
                    }
                    mcr.mapToWriteToDisk = mMap;
                    mDiskWritesInFlight++;
                    //  把注册的 listeners 放到 mcr 中去,以便在数据写入的时候被回调
                    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.
                            // 当值是 null 时,表示移除该键值对,在 editor 的 remove 实现中,并不是真正地移除,
                            // 而是把 value 赋值为当前 editor 对象
                            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);
                            }
                        }
                        // 添加完成后把 editor 里的 map 清空
                        mModified.clear();
                    }
                }
                return mcr;
            }
    

    这是 MemoryCommitResult 类,主要用于提交到内存后返回结果,然后在写入磁盘时作为参数传递。

     private static class MemoryCommitResult {
            public boolean changesMade;  // any keys different?
            public List<String> keysModified;  // may be null
            public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
            public Map<?, ?> mapToWriteToDisk;
            public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
            public volatile boolean writeToDiskResult = false;
    
            public void setDiskWriteResult(boolean result) {
                writeToDiskResult = result;
                writtenToDiskLatch.countDown();
            }
        }
    

    下面看保存到磁盘的操作,enqueueDiskWrite 方法,参数有 MemoryCommitResult 和 Runnable,mcr 刚才说过,就看这个 Runnable 是干嘛的。在 commit 方法中调用 enqueueDiskWrite 方法是传入的 Runnable 是null,它会在当前线程直接执行写文件的操作,然后返回写入结果。而如果 Runnable 不是 null,那就使用 QueueWork 中的单线程执行。这就是 apply 和 commit 的根本区别:一个同步执行,有返回值;一个异步执行,没有返回值。大多数情况下,我们使用 apply 就够了,这也是官方推荐的做法。

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

    再看一下具体写文件的代码 writeToFile 方法,关键点在代码中文注释部分。简单说就是备份 → 写入 → 检查 → 善后,这样保证了数据的安全性和稳定性。

        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)) {
                        Log.e(TAG, "Couldn't rename file " + mFile
                              + " to backup file " + mBackupFile);
                        mcr.setDiskWriteResult(false);
                        return;
                    }
                } else {
                    mFile.delete();
                }
            }
    
            // 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 = 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);
        }
    

    4. 总结

    通过 getSharedPreferences 可以获取 SP 实例,从首次初始化到读到数据会存在延迟,因为读文件的操作阻塞调用的线程直到文件读取完毕,如果在主线程调用,可能会对 UI 流畅度造成影响。
    commit 会在调用者线程同步执行写文件,返回写入结果;apply 将写文件的操作异步执行,没有返回值。可以根据具体情况选择性使用,推荐使用 apply。
    虽然支持设置 MODE_MULTI_PROCESS 标志位,但是跨进程共享 SP 存在很多问题,所以不建议使用该模式。
    【附录】

    资料图

    需要资料的朋友可以加入Android架构交流QQ群聊:513088520

    点击链接加入群聊【Android移动架构总群】:加入群聊

    获取免费学习视频,学习大纲另外还有像高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)等Android高阶开发资料免费分享。

    相关文章

      网友评论

        本文标题:Android SharedPreferences 实现原理解析

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