返回介绍

3.3 SharePreferences 内部类 Editor 实现 EditorImpl 分析

发布于 2024-12-23 21:33:35 字数 11104 浏览 0 评论 0 收藏 0

还记不记得 set 是在 SharePreference 接口的 Editor 接口中定义的,而 SharePreference 提供了 edit() 方法来获取 Editor 实例,我们先来看下这个 edit() 方法吧,如下:

  public Editor edit() {
    //握草!这也和异步 load 用的一把锁
    synchronized (this) {
      //阻塞等待,不解释吧,向上看。。。
      awaitLoadedLocked();
    }
  //异步加载 OK 以后通过 EditorImpl 创建 Editor 实例
    return new EditorImpl();
  }

可以看见,SharePreference 的 edit() 方法其实就是阻塞等待返回一个 Editor 的实例(Editor 的实现是 EditorImpl),那我们就顺藤摸瓜一把,来看下这个 EditorImpl 这个类,如下:

  public final class EditorImpl implements Editor {
    //创建一个 mModified 的 key-value 集合,用来在内存中暂存数据
    private final Map<String, Object> mModified = Maps.newHashMap();
    //一个是否清除 preference 的 flag
    private boolean mClear = false;

    ......//省略类似的 putXXX 方法
    public Editor putBoolean(String key, boolean value) {
      //同步锁操作
      synchronized (this) {
        //将我们要存储的数据放入 mModified 集合中
        mModified.put(key, value);
        //返回当前对象实例,方便这种模式的代码写法:putXXX().putXXX();
        return this;
      }
    }
    //不用过多解释,同步删除 mModified 中包含 key 的数据
    public Editor remove(String key) {
      synchronized (this) {
        mModified.put(key, this);
        return this;
      }
    }
    //不解释,要清楚所有数据则直接置位 mClear 标记
    public Editor clear() {
      synchronized (this) {
        mClear = true;
        return this;
      }
    }
    ......
  }

好了,到此你可以发现 Editor 的 setXXX 及 clear 操作仅仅只是将相关数据暂存到内存中或者设置好标记为,也就是说调运了 Editor 的 putXXX 后其实数据是没有存入 SharePreference 的。那么通过我们一开始的实例可以知道,要想将 Editor 的数据存入 SharePreference 文件需要调运 Editor 的 commit 或者 apply 方法来生效。所以我们接下来先来看看 Editor 类常用的 commit 方法实现原理,如下:

public boolean commit() {
  //1.先通过 commitToMemory 方法提交到内存
  MemoryCommitResult mcr = commitToMemory();
  //2.写文件操作
  SharedPreferencesImpl.this.enqueueDiskWrite(
      mcr, null /* sync write on this thread okay */);
  try {
    //阻塞等待写操作完成,UI 操作需要注意!!!所以如果不关心返回值可以考虑用 apply 替代,具体原因等会分析 apply 就明白了。
    mcr.writtenToDiskLatch.await();
  } catch (InterruptedException e) {
    return false;
  }
  //3.通知数据发生变化了
  notifyListeners(mcr);
  //4.返回写文件是否成功状态
  return mcr.writeToDiskResult;
}

我去,小小一个 commit 方法做了这么多操作,主要分为四个步骤,我们先来看下第一个步骤,通过 commitToMemory 方法提交到内存返回一个 MemoryCommitResult 对象。分析 commitToMemory 方法前先看下 MemoryCommitResult 这个类,具体如下:

// Return value from EditorImpl#commitToMemory()
//也是内部类,只是为了组织数据结构而诞生,也就是 EditorImpl.commitToMemory() 的返回值
private static class MemoryCommitResult {
  public boolean changesMade;  // any keys different?
  public List<String> keysModified;  // may be null
  public Set<android.content.SharedPreferences.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();
  }
}

回过头现在来看 commitToMemory 方法,具体如下:

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
  //啥也不说,先整一个实例化对象
  MemoryCommitResult mcr = new MemoryCommitResult();
  //和 SharedPreferencesImpl 共用一把锁
  synchronized (SharedPreferencesImpl.this) {
    // We optimistically don't make a deep copy until
    // a memory commit comes in when we're already
    // writing to disk.
    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);
    }
    //构造数据结构,把通过 SharedPreferencesImpl 构造函数里异步加载的文件 xml 解析结果 mMap 赋值给要写到 disk 的 Map
    mcr.mapToWriteToDisk = mMap;
    //增加一个未完成的写 opt
    mDiskWritesInFlight++;
  //判断有没有监听设置
    boolean hasListeners = mListeners.size() > 0;
    if (hasListeners) {
      //创建监听队列
      mcr.keysModified = new ArrayList<String>();
      mcr.listeners =
          new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
    }
    //再加一把自己的锁
    synchronized (this) {
      //如果调运的是 Editor 的 clear 方法,则这里 commit 时这么处理
      if (mClear) {
        //如果从文件里加载出来的 xml 不为空
        if (!mMap.isEmpty()) {
          //设置数据结构中数据变化标志为 true
          mcr.changesMade = true;
          //清空内存中 xml 数据
          mMap.clear();
        }
        //处理完毕,标记复位,程序继续执行,所以如果这次 Editor 中如果有写数据且还未 commit,则执行完这次 commit 之后不会清掉本次写操作的数据,只会 clear 以前 xml 文件中的所有数据
        mClear = false;
      }
    //mModified 是调运 Editor 的 setXXX 零时存储的 map
      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.
        //删除需要删除的 key-value
        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;
            }
          }
          //把变化和新加的数据更新到 SharePreferenceImpl 的 mMap 中
          mMap.put(k, v);
        }
      //设置数据结构变化标记
        mcr.changesMade = true;
        if (hasListeners) {
          //设置监听
          mcr.keysModified.add(k);
        }
      }
      //清空 Editor 中零时存储的数据
      mModified.clear();
    }
  }
  //返回重新更新过 mMap 值封装的数据结构
  return mcr;
}   

到此我们 Editor 的 commit 方法的第一步已经完成,根据写操作组织内存数据,返回组织后的数据结构。接下来我们继续回到 commit 方法看下第二步—-写到文件中,其核心是调运 SharedPreferencesImpl 类的 enqueueDiskWrite 方法实现。具体如下:

//按照队列把内存数据写入磁盘,commit 时 postWriteRunnable 为 null,apply 时不为 null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                final Runnable postWriteRunnable) {
  //创建一个 writeToDiskRunnable 的 Runnable 对象
  final Runnable writeToDiskRunnable = new Runnable() {
    public void run() {
      synchronized (mWritingToDiskLock) {
        //真正的写文件操作
        writeToFile(mcr);
      }
      synchronized (SharedPreferencesImpl.this) {
        //写完一个计数器-1
        mDiskWritesInFlight--;
      }
      if (postWriteRunnable != null) {
        //等会 apply 分析
        postWriteRunnable.run();
      }
    }
  };
  //判断是同步写还是异步
  final boolean isFromSyncCommit = (postWriteRunnable == null);

  // Typical #commit() path with fewer allocations, doing a write on
  // the current thread.
  //commit 方式走这里
  if (isFromSyncCommit) {
    boolean wasEmpty = false;
    synchronized (SharedPreferencesImpl.this) {
      //如果当前只有一个写操作
      wasEmpty = mDiskWritesInFlight == 1;
    }
    if (wasEmpty) {
      //一个写操作就直接在当前线程中写文件,不用另起线程
      writeToDiskRunnable.run();
      //写完文件就返回
      return;
    }
  }
  //如果是 apply 就在线程池中执行
  QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

可以发现,commit 从内存写文件是在当前调运线程中直接执行的。那我们再来看看这个写内存到磁盘方法中真正的写方法 writeToFile,如下:

  // Note: must hold mWritingToDiskLock
  private void writeToFile(MemoryCommitResult mcr) {
    if (mFile.exists()) {
      if (!mcr.changesMade) {
        //如果文件存在且没有改变的数据则直接返回写 OK
        mcr.setDiskWriteResult(true);
        return;
      }

      if (!mBackupFile.exists()) {
        //如果要写入的文件已经存在,并且备份文件不存在时就先把当前文件备份一份,因为如果本次写操作失败时数据可能已经乱了,所以下次实例化 load 数据时可以从备份文件中恢复
        if (!mFile.renameTo(mBackupFile)) {
          Log.e(TAG, "Couldn't rename file " + mFile
              + " to backup file " + mBackupFile);
          //命名失败直接返回写失败了
          mcr.setDiskWriteResult(false);
          return;
        }
      } else {
        //备份文件存在就把源文件删掉,因为要写新的
        mFile.delete();
      }
    }

    try {
      //创建 mFile 文件
      FileOutputStream str = createFileOutputStream(mFile);
      if (str == null) {
        //创建失败直接返回写失败了
        mcr.setDiskWriteResult(false);
        return;
      }
      //把数据写入 mFile 文件
      XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
      //彻底同步到磁盘文件中
      FileUtils.sync(str);
      str.close();
      //设置文件权限 mode      
      ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
      //和刚开始实例化 load 时一样,更新文件时间戳和大小
      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.
      //写成功了 mFile 那就把备份文件直接删掉,没用了。
      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()) {
      //上面如果出错了就删掉,因为写之前已经备份过数据了,下次 load 时 load 备份数据
      if (!mFile.delete()) {
        Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
      }
    }
    //写失败了
    mcr.setDiskWriteResult(false);
  }

回过头可以发现,上面 commit 的第二步写磁盘操作其实是做了类似数据库的事务操作机制的(备份文件)。接着可以继续分析 commit 方法的第三四步,很明显可以看出,第三步就是回调设置的监听方法,通知数据变化了,第四步就是返回 commit 写文件是否成功。

总体到这里你可以发现,一个常用的 SharePreferences 过程已经完全分析完毕。接下来我们就再简单说说 Editor 的 apply 方法原理,先来看下 Editor 的 apply 方法,如下:

public void apply() {
  //有了上面 commit 分析,这个雷同,写数据到内存,返回数据结构
  final MemoryCommitResult mcr = commitToMemory();
  final Runnable awaitCommit = new Runnable() {
    public void run() {
      try {
        //等待写文件结束
        mcr.writtenToDiskLatch.await();
      } catch (InterruptedException ignored) {
      }
    }
  };

  QueuedWork.add(awaitCommit);
  //一个收尾的 Runnable
  Runnable postWriteRunnable = new Runnable() {
    public void run() {
      awaitCommit.run();
      QueuedWork.remove(awaitCommit);
    }
  };
  //这个上面 commit 已经分析过的,这里 postWriteRunnable 不为 null,所以会在一个新的线程池调运 postWriteRunnable 的 run 方法
  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 类似,只不过他是异步写的,没在当前线程执行写文件操作,还有就是他不像 commit 一样返回文件是否写成功状态。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文