DirectBoot模式是什么
DirectBoot(简称DB)是Android N新引入的一个特性,本质上是对数据访问做了限制。在用户开机但未解锁之前,应用只能访问这个安全区内的数据,从而保护用户隐私安全。
Android N上把数据分成了两块,分别是:
- 凭据保护存储区(credential-protected),这是所有应用的默认存储位置,仅在用户解锁设备后可用。
- 设备保护存储区(device-protected),这是一个新的存储位置,当设备启动后(包括DB阶段)随时都可访问该位置。
SharedPreference简述
SharedPreference(简称SP)是Android原生提供的一种数据持久化方式,因为其API友好而收到开发者的青睐。
先来看下SP是怎么存储数据的吧。
SP的实现在SharedPreferenceImpl
中,直接在Android Studio中无法查找到这个类,可以进入目录/Android/sdk/source/android-xx/android/app
中找到这个类。
SP的getInt
方法:
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
可以看到,所谓的SharedPreference
其实就是维护了一个map,所有数据的存储和读取都通过操作这个map来实现。而且,这个map是常驻内存的。这就带来了一个问题:内存泄漏!当存储的数目过多或者其中一个kay-value键值对过大的时候,就很可能造成OOM!
那么这个map是怎么来的呢?看看下面这个方法
private void loadFromDisk() {
synchronized (mLock) {
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);
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
其中的关键方法是下面这行:
map = XmlUtils.readMapXml(str);
也就是说,在SharedPreferenceImpl
的构造函数中,启动了一个线程,异步把外存上的数据(其实就是一个xml文件,每一行是一个key-value键值对)读入内存中,并以map的形式保存起来。
那么将数据写回外存呢?SP提供了两个方法供开发者调用,分别是
Editor#apply()
和Editor#commit
,区别是,apply
是异步的,而commit
则是同步保存数据,在UI线程中调用,会阻塞主线程。
看一下commit
方法做了些什么:
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;
}
关键方法commitToMemory()
和 enqueueDiskWrite(MemoryCommitResult, Runnable postWriteRunnable)
。前者把提交到editor中的新键值对提交到内存中(即前面提到的map中)。后者把map中的数据保存到xml文件中。
DB与SP的矛盾
说到这里,SP的消失之谜大概也有答案了:其实就是在DB模式中,由于没有访问凭据保护存储区的权限,因此无法将外存中的数据读取到内存中。在用户解除DB模式后,由于缓存了SP的实例,因此内存中的空白数据覆盖了xml文件,导致所有的键值对都消失。
这里顺带附上Context#getSharedPreference(File file, int mode)
的代码,看下Android源码中是怎么对SP对象进行缓存的:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(StorageManager.class).isUserKeyUnlocked(
UserHandle.myUserId())
&& !isBuggy()) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
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) {
// 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;
}
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
// 这里的sSharedPrefsCache缓存了sp的实例
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}