SharedPreference 初始化源码解析

初始化

sp 内部将数据放到 XML 文件中,加载时首先会将硬盘中文件读取到内存中,这样加快了访问速度

这次从源码开始,看看里面具体做了什么

  1.      // 初始化
  2.      SharedPreferencesImpl(File file, int mode) {
  3.          // 文件
  4.          mFile = file;
  5.          //备份文件 .bak 结尾,看看什么时候排上作用,比如恢复数据
  6.          mBackupFile = makeBackupFile(file);
  7.          mMode = mode;
  8.          mLoaded = false;
  9.          mMap = null;
  10.          mThrowable = null;
  11.          // 从硬盘中读取
  12.          startLoadFromDisk();
  13.      }

硬盘中读取文件开了新线程,主要将文件中的内容,转换为Map

  1.      private void loadFromDisk() {
  2.          synchronized (mLock) {
  3.              if (mLoaded) {
  4.                  return;
  5.              }
  6.              // 存在备份文件,删除 file,为什么
  7.              if (mBackupFile.exists()) {
  8.                  mFile.delete();
  9.                  mBackupFile.renameTo(mFile);
  10.              }
  11.          }
  12.          Map<String, Object> map = null;
  13.          StructStat stat = null;
  14.          Throwable thrown = null;
  15.              stat = Os.stat(mFile.getPath());
  16.                  // 读取流
  17.                  BufferedInputStream str = null;
  18.                  try {
  19.                      str = new BufferedInputStream(
  20.                      new FileInputStream(mFile), 16 * 1024);
  21.                      // 转为 map
  22.                      map = (Map<String, Object>) XmlUtils.readMapXml(str);
  23.                  } catch (Exception e) {
  24.                      Log.w(TAG, “Cannot read “ + mFile.getAbsolutePath(), e);
  25.                  } finally {
  26.                      // 关闭流
  27.                      IoUtils.closeQuietly(str);
  28.                  }
  29.      }

流程很简单,就是读取硬盘,转换为一个 Map

apply,commit 区别

首先 apply,commit 分别是异步/同步的写入操作,它们都会先写入内存中,也就是更新 Map,不同在于写入到硬盘的时机不同

  • commit 先看 commit 做了什么 ,commit 方法将返回一个布尔值,表示结果
  1.          @Override
  2.          public boolean commit() {
  3.              // 先提交到内存中
  4.              MemoryCommitResult mcr = commitToMemory();
  5.              // 执行硬盘中的更新
  6.              SharedPreferencesImpl.this.enqueueDiskWrite(
  7.                  mcr, null /* sync write on this thread okay */);
  8.              try {
  9.                  mcr.writtenToDiskLatch.await();
  10.              } catch (InterruptedException e) {
  11.                  // 提交异常,返回 false
  12.                  return false;
  13.              }
  14.              // 通知监听
  15.              notifyListeners(mcr);
  16.              // 返回结果
  17.              return mcr.writeToDiskResult;
  18.          }
  • apply
  1.          @Override
  2.          public void apply() {
  3.              final long startTime = System.currentTimeMillis();
  4.              // 都是一样的,先写到内存
  5.              final MemoryCommitResult mcr = commitToMemory();
  6.              final Runnable awaitCommit = new Runnable() {
  7.                      @Override
  8.                      public void run() {
  9.                      //
  10.                      mcr.writtenToDiskLatch.await();
  11.                      }
  12.                  };
  13.              // 往 sFinishers 队列中添加,等待执行
  14.              Queuedwork.addFinisher(awaitCommit);
  15.              // 在写完后执行 postWriteRunnable
  16.              Runnable postWriteRunnable = new Runnable() {
  17.                      @Override
  18.                      public void run() {
  19.                      // 执行 awaitCommit
  20.                      awaitCommit.run();
  21.                      // sFinishers 队列中移除
  22.                      QueuedWork.removeFinisher(awaitCommit);
  23.                      }
  24.                  };
  25.              // 写入硬盘
  26.              SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  27.          }

硬盘中是如何更新的呢

  1.      private void enqueueDiskWrite(final MemoryCommitResult mcr,
  2.                      final Runnable postWriteRunnable) {
  3.          final boolean isFromSyncCommit = (postWriteRunnable == null);
  4.          // 创建 Runnable 对象
  5.          final Runnable writeToDiskRunnable = new Runnable() {
  6.                  @Override
  7.                  public void run() {
  8.                      // 写到文件,这里的锁是 mWritingToDiskLock 对象
  9.                      synchronized (mWritingToDiskLock) {
  10.                      writeToFile(mcr, isFromSyncCommit);
  11.                      }
  12.                      synchronized (mLock) {
  13.                      mDiskWritesInFlight–;
  14.                      }
  15.                      // 执行 postWriteRunnable, commit 这里为 null
  16.                      // apply 时不为i而空
  17.                      if (postWriteRunnable != null) {
  18.                      postWriteRunnable.run();
  19.                      }
  20.                  }
  21.              };
  22.          // Typical #commit() path with fewer allocations, doing a write on
  23.          // the current thread.
  24.          // 是否为同步提交
  25.          // 根据 postWriteRunnable 是否为空, commit 这里为 true
  26.          // apply
  27.          if (isFromSyncCommit) {
  28.              boolean wasEmpty = false;
  29.              synchronized (mLock) {
  30.                  wasEmpty = mDiskWritesInFlight == 1;
  31.              }
  32.              if (wasEmpty) {
  33.                  writeToDiskRunnable.run();
  34.                  return;
  35.              }
  36.          }
  37.          // 放到队列中执行,内部是一个 HandlerThread,按照队列逐个执行任务
  38.          QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
  39.      }

这里用队列来放任务,应该是要应对多个 commit 情况,这里将所有 commit 往队列里面放,放完后就会执行硬盘的写,apply 也会调用到这里

  1.      public static void queue(Runnable work, boolean shouldDelay) {
  2.          Handler handler = getHandler();
  3.          synchronized (sLock) {
  4.              // 添加到 sWork 队列中
  5.              sWork.add(work);
  6.              // 异步 apply 走这个
  7.              if (shouldDelay && sCanDelay) {
  8.                  handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
  9.              } else {
  10.              // 同步 commit 走这个
  11.                  handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
  12.              }
  13.          }
  14.      }

apply 的硬盘写入,需要等待 Activity.onPause() 等时机才会执行

读取

读取比写入就简单很多了

  • 先查看是否从硬盘加载到了内存,没有就先去加载
  • 从内存中读取
  1.      public String getString(String key, @Nullable String defValue) {
  2.          synchronized (mLock) {
  3.              // 检查是否从硬盘加载到了内存,没有就先去加载
  4.              awaitLoadedLocked();
  5.              String v = (String)mMap.get(key);
  6.              return v != null ? v : defValue;
  7.          }
  8.      }

如何保证线程安全的

通过 sync 加对象锁,内存读写都是用的同一把锁,所以读写都是线程安全的

数据恢复

存在备份机制

  • 对文件进行写入操作,写入成功时,则将备份文件删除
  • 如果写入失败,之后重新初始化时,就使用备份文件恢复

SP 与 ANR

由于 Activity.onPause 会执行 apply 的数据落盘,里面是有等待锁的,如果时间太长就会 ANR

小结

从 sp 的初始化,读写方面简单分析了流程,sp 有数据恢复机制这是 mmkv 所欠缺的,但在多进程环境下,sp 是不可靠的

相关链接

  • 官方也无力回天?android SharedPreferences的设计与实现
  • 剖析 SharedPreference apply 引起的 ANR 问题

以上就是SharedPreference 初始化源码解析的详细内容,更多关于SharedPreference 初始化的资料请关注我们其它相关文章!

标签

发表评论