前几天遇到了一个使用SharedPreferences(以下简写成sp)导致严格模式报警的问题,我打点计算了一下sp#getInt()的时间,发现sp还是挺慢的,大约30ms。今天有点空闲,我们来研究一下sp的原理。

(源码基于android-28)

我接下来会顺着我们使用sp的步骤,一步一步的来分析。
主要包含:sp的创建、sp的数据读取、sp的写入。

sp的创建

通话context#getSharedPreferences可以获取到sp实例。

android.content.Context#getSharedPreferences(java.io.File, int)

public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);

通过源码可以看出,这里的具体实现应该是在ContextImpl.java

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // 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) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {  //字节码 同步锁
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();  // sp path的内存缓存
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);  // 见下边的方法
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);  // 见下边的方法
}

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;  //SharedPreferencesImpl 真正的实现类
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();  // 缓存
        sp = cache.get(file);  // 从缓存中拿到sp impl
        if (sp == null) {
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            sp = new SharedPreferencesImpl(file, mode);  // 创建 sp impl
            cache.put(file, sp);
            return sp;
        }
    }
    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;
}

@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

所以sharedPreferences的真正实现在SharedPreferencesImpl。

再来看一下SharedPreferencesImpl的初始化过程

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);  // 获取备份文件
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    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里边

    synchronized (mLock) {
        mLoaded = true;   // 标记数据加载完成
        mThrowable = thrown;
        ....
        try {
            .... // 初始化mMap,正常情况下会 mMap = map
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll(); // 释放锁
        }
    }
}

从上面的代码我们可以发现,sp的创建过程虽然复杂,但是耗时操作已经放到了子线程,所以我们在获取sp实例的时候不需要担心阻塞的问题。

同时,我们注意到有两个变量比较重要:mLoaded和mLock,这两个变量是为了保证必须在数据从disk上加载完毕后,我们才能对sp进行读写操作。

sp的数据读取

sharedPreferences.getInt() // 这是一种常用的获取sp数据的方式

来看一下具体的实现:


android.app.SharedPreferencesImpl#getInt

    @Override
    public int getInt(String key, int defValue) {
        synchronized (mLock) {  // 这里用了mLock,保证上边的初始化和现在的读取互斥
            awaitLoadedLocked(); // 检查或等待数据加载完成
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }


    @GuardedBy("mLock")
    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 {
                mLock.wait(); // 等待加载完成,这里会阻塞线程
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

所以,因为awaitLoadedLocked的存在,我们在getInt的时候,可能会有一定的耗时。只要你不是刚初始化sp,就立刻去get数据,一般也不会阻塞。

sp的写入

sharedPreferences.edit().putInt().apply();
sharedPreferences.edit().putInt().commit();

sp的写入有两种方式,apply和commit。

这里有个新的类:Editor,他的实现在android.app.SharedPreferencesImpl.EditorImpl,我们重点关注apply和commit方法。

      @Override
      public void apply() {
          final long startTime = System.currentTimeMillis();

          final MemoryCommitResult mcr = commitToMemory();  // 提交到内存
          final Runnable awaitCommit = new Runnable() {
                  @Override
                  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);  //这是个内部工具类,用于跟踪那些未完成的或尚未结束的全局任务,新任务通过方法 queue 加入。添加 finisher 的runnables,由 waitToFinish 方法保证执行,用于保证任务已被处理完成。

          Runnable postWriteRunnable = new Runnable() {
                  @Override
                  public void run() {
                      awaitCommit.run();
                      QueuedWork.removeFinisher(awaitCommit);
                  }
              };

          SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 真正的异步写入disk

          // 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); // 通知观察者
      }

      // Returns true if any changes were made
      private MemoryCommitResult commitToMemory() {
          long memoryStateGeneration;
          List<String> keysModified = null;
          Set<OnSharedPreferenceChangeListener> listeners = null;
          Map<String, Object> mapToWriteToDisk;

          synchronized (SharedPreferencesImpl.this.mLock) { // 使用sp中的那个数据访问锁,保证线程安全
              // 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);
              }
              mapToWriteToDisk = mMap; // 拿到sp中的map
              mDiskWritesInFlight++;

              boolean hasListeners = mListeners.size() > 0;
              if (hasListeners) {
                  keysModified = new ArrayList<String>();
                  listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
              }

              synchronized (mEditorLock) {
                  boolean changesMade = false;

                  if (mClear) {
                      if (!mapToWriteToDisk.isEmpty()) {
                          changesMade = true;
                          mapToWriteToDisk.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.
                      if (v == this || v == null) {
                          if (!mapToWriteToDisk.containsKey(k)) {
                              continue;
                          }
                          mapToWriteToDisk.remove(k);  //写入数据到map中,即内存中
                      } else {
                          if (mapToWriteToDisk.containsKey(k)) {
                              Object existingValue = mapToWriteToDisk.get(k);
                              if (existingValue != null && existingValue.equals(v)) {
                                  continue;
                              }
                          }
                          mapToWriteToDisk.put(k, v); //写入数据到map中,即内存中 
                      }

                      changesMade = true;
                      if (hasListeners) {
                          keysModified.add(k);
                      }
                  }

                  mModified.clear();

                  if (changesMade) {
                      mCurrentMemoryStateGeneration++;
                  }

                  memoryStateGeneration = mCurrentMemoryStateGeneration;
              }
          }
          return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                  mapToWriteToDisk);
      }

      @Override
      public boolean commit() {
          long startTime = 0;

          if (DEBUG) {
              startTime = System.currentTimeMillis();
          }

          MemoryCommitResult mcr = commitToMemory();

          SharedPreferencesImpl.this.enqueueDiskWrite(
              mcr, null /* sync write on this thread okay */); //这里的写入是异步的
          try {
              mcr.writtenToDiskLatch.await();  // 但是这里会阻塞等待写入完成
          } catch (InterruptedException e) {
              return false;
          } finally {
              if (DEBUG) {
                  Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                          + " committed after " + (System.currentTimeMillis() - startTime)
                          + " ms");
              }
          }
          notifyListeners(mcr);
          return mcr.writeToDiskResult;
      }

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
      final boolean isFromSyncCommit = (postWriteRunnable == null);

      final Runnable writeToDiskRunnable = new Runnable() {
              @Override
              public void run() {
                  synchronized (mWritingToDiskLock) {
                      writeToFile(mcr, isFromSyncCommit); // 异步写入文件
                  }
                  synchronized (mLock) {
                      mDiskWritesInFlight--;
                  }
                  if (postWriteRunnable != null) {
                      postWriteRunnable.run();
                  }
              }
          };

      // Typical #commit() path with fewer allocations, doing a write on
      // the current thread.
      if (isFromSyncCommit) {
          boolean wasEmpty = false;
          synchronized (mLock) {
              wasEmpty = mDiskWritesInFlight == 1;
          }
          if (wasEmpty) {
              writeToDiskRunnable.run();
              return;
          }
      }

      QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
  }

  @GuardedBy("mWritingToDiskLock")
  private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
      .... //写入相关的

          if (!backupFileExists) {
              if (!mFile.renameTo(mBackupFile)) {
                  Log.e(TAG, "Couldn't rename file " + mFile
                        + " to backup file " + mBackupFile);
                  mcr.setDiskWriteResult(false, false); // 对CountDownLatch进行countDown,释放阻塞
                  return;
              }
          } else {
              mFile.delete();
          }
      }
  }

所以apply是先提交到内存,异步存储到disk。而commit是先提交到内存后,然后阻塞线程到存储disk完成。

QueuedWork

这里需要补充一下QueuedWork,这个类我们用不到,因为是@hide,我们需要知道的是这个queue是异步的,是通过Handler实现的,线程名称是queued-work-looper。感觉这个代码比较关键:

/**
 * Lazily create a handler on a separate thread.
 *
 * @return the handler
 */
private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

以上是所有内容,The End.


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

OkHttp源码阅读 上一篇
WDYDT-19-使用systrace查找掉帧问题 下一篇