一,如何使用?
先从简单的使用示例开始
写入数据
SharedPreferences sharedPreferences = this.getSharedPreferences("settings", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("isOpen", true);
editor.putString("name", "coder");
editor.putInt("number", 10);
editor.commit();
读取数据
boolean isOpen = sharedPreferences.getBoolean("isOpen", false);
String name = sharedPreferences.getString("name", "");
int years = sharedPreferences.getInt("years", 0);
使用很简单,接下来我们一句一句的来分析源码实现。
二, 源码分析
1. 获取 SharedPreferences
/frameworks/base/core/java/android/content/ContextWrapper.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
mode可以选以下四种,:
MODE_PRIVATE 默认模式,创建的文件只能在应用内访问(或者共享相同userID的所有应用)
MODE_WORLD_READABLE(过时)允许其他应用访问本应用的文件,使用此模式会抛出异常
MODE_WORLD_WRITEABLE(过时)允许其他应用写本应用的文件,使用此模式会抛出异常
MODE_MULTI_PROCESS(过时)官方提示这种模式在某些版本无法可靠运行,并且未来也不会支持多进程
但目前除了第一种其他的都不建议使用,并且从 Android N 开始MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE会直接抛出异常,后面我们在分析源码的时候会详细说明这块;
上面 mBase 是 Context 类型的实例,Context是一个抽象类,它有两个直接子类,一个是ContextWrapper,一个是ContextImpl,ContextWrapper 是上下文功能的封装类,而ContextImpl则是上下文功能的实现类, Activity,Service, Application都是继承自ContextWrapper的,因此这里的mBase其实最终指向的就是 ContextImpl;
ContextImpl 是何时创建的?
ContextImpl 是主线程ActivityThread 的成员变量,ActivityThread 是管理应用进程的主线程的执行,ActivityThread 是在App冷启动main(String[] args)中初始化的,说明ActivityThread只有一个,从而对应一个ContextImpl ,分析这个有利于我们接下来分析SharedPreferences 的一些代码;
public ContextImpl getSystemContext() {
synchronized (this) {
if (mSystemContext == null) {
mSystemContext = ContextImpl.createSystemContext(this);
}
return mSystemContext;
}
}
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null, null);
context.setResources(packageInfo.getResources());
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
context.mResourcesManager.getDisplayMetrics());
return context;
}
/frameworks/base/core/java/android/app/ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
name 传 null 是被允许的,它会生成一个以null.xml为名的文件, 我用targetSdkVersion为29,在Android 10的机器上也验证了确实是可以的:
/data/user/0/com.cjl.loadingview/shared_prefs/null.xml
我们知道一个App可以对SharedPreferences设置不同name,这样最终也就对应着不同的xml文件,mSharedPrefsPaths 是一个map,它就是用来保存不同name的文件的;如果mSharedPrefsPaths
里没有该name对应的文件,那么就 通过getSharedPreferencesPath(name)获取一个,代码如下:
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
上述代码最终会生成一个下面路径的.xml文件:
/data/user/0/com.cjl.loadingview/shared_prefs/settings.xml
/data/user/0 是一个 /data/data 的 link,
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
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);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
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;
}
sSharedPrefsCache 存储不同packageName 的 SharedPreferencesImpl, 冷启动进来SharedPreferencesImpl是为null的,因此回去新建一个,在新建之前会检查最初传进来的mode,从如下代码可以看到Android N 后已强制不能在使用 MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE 。
如果sp不为null,mode 是 多进程模式 MODE_MULTI_PROCESS, 此时需要重新读取文件;
private void checkMode(int mode) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
我们来看看SharedPreferencesImpl 到底是什么?
final class SharedPreferencesImpl implements SharedPreferences {
···
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
SharedPreferences 是一个接口类型的,SharedPreferencesImpl 是它的实现类,因此SharedPreferencesImpl 才是我们分析的重点,这里面有SharedPreferences 各种操作的具体实现:
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
创建.bak备份文件,接下来会开启一个名为 SharedPreferencesImpl-load
的子线程从磁盘读取文件,并且读取到文件后立即将其转换成了map文件保存在内存中,为什么转换成map保存在内存中呢,这里留一个伏笔,看到后面你自然会明白;
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<String, Object> map = null;
StructStat stat = null;
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
}
mLoaded 这个成员变量得留意一下,在首次读取完磁盘文件后,下次调用getSharedPreferences就不会再从磁盘读取了;
2. 写入数据
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name", "coder");
editor.commit();
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {}
}
...
}
写入数据需要使用Editor 实例,Editor 是一个接口,它的实现类是EditorImpl,在获取Editor 之前如果mLock锁没有被释放则会处于等待状态, 等待什么呢,从上面分析我们不难看出其实是在等待getSharedPreferences时从磁盘中读取文件,如果文件都没有读取完成,我们拿到Editor 去写数据肯定是不行的,加载成功后的 notifyAll 要结合 awaitLoadedLocked 来分析。在准备读、写 SP 的时候,都会先调用 awaitLoadedLocked 等待 loadFromDisk loadFromDisk ,在读取磁盘文件结束后会调用mLock.notifyAll()
唤醒这些等待数据加载完成的线程,接下来我们就可以去获取EditorImpl去写文件了
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
private final Map<String, Object> mModified = new HashMap<>();
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
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;
}
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await();
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
...
}
写入操作大致可分为两步完成:
第一步是用EditorImpl直接更改内存mMap的值;
第二步是将内存mMap中的键值对写入到磁盘文件中;
我们putString为例来分析下写文件的操作,在调用完putString之后我们必须要调用commit()或者apply()去保存数据;
这两个方法都会调用commitToMemory()将数据写入内存map,接着都会add一个写入文件的任务,等待后续系统执行
commit()或者apply()不同的地方在于:
apply将文件写入操作放到一个Runnable对象中,等待系统在子线程中调用, 此时不会阻碍主线程;
commit 是直接在主线程中同步进行写入操作, 因此使用commit是会阻塞主线程的,这点得注意;
关于上述不同点可以详细追一下enqueueDiskWrite()方法, 如果是apply()方法会使用writeToDiskRunnable , commit会在主线程写入
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);
}
3. 数据的读取
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}