当密钥包含换行符时,Android 中的 SharedPreferences 不会保存到磁盘
SharedPreferences in Android not persisted to disk when key contains newline
在 Android 中,我想编写 SharedPreferences
键值对,其中键是 Base64 字符串。
// get a SharedPreferences instance
SharedPreferences prefs = getSharedPreferences("some-name", Context.MODE_PRIVATE);
// generate the base64 key
String someKey = new String(Base64.encode("some-key".getBytes("UTF-8"), Base64.URL_SAFE), "UTF-8");
// write the value for the generated key
prefs.edit().putBoolean(someKey, true).commit();
在最后一行,调用提交 returns true
。所以这个键值对应该已经保存成功了
当我关闭并销毁使用这段代码的Activity
,然后重新创建Activity
(再次运行这段代码)时,返回指定的值对于我们使用的密钥。
但事实证明,当我销毁整个 application/process 时(例如,在应用程序设置中使用 "Force stop"),我们的密钥的值在 [=14 的下一次启动时丢失=].
当我不使用 Base64.URL_SAFE
但 Base64.URL_SAFE | Base64.NO_WRAP
作为 Base64 编码的标志时,它工作正常。
所以这个问题是由 Base64 密钥末尾的换行符引起的。像abc
这样的键可以毫无问题的写入。但是当key为abc\n
时,就失败了。
问题是它首先似乎可以正常工作,在 commit()
上返回 true
并在后续调用中返回正确的首选项值。但是当整个应用被销毁并重新启动时,这个值并没有被持久化。
这是有意为之的行为吗?错误?文档是否说明了有效的密钥名称?
我看了一下GrepCode,操作如下(没用的就不说了):
- android.app.SharedPreferencesImpl.commit()
- android.app.SharedPreferencesImpl.commitToMemory()
android.app.SharedPreferencesImpl.queueDiskWrite(MemoryCommitResult,Runnable)
3.1。 XmlUtils.writeMapXml(地图,输出流)
3.2。 XmlUtils.writeMapXml(映射、字符串、XmlSerializer)
3.3。 XmlUtils.writeValueXml(对象 v,字符串名称,XmlSerializer ser)
首先:您的数据是如何转换的?
方法 XmlUtils.writeValueXml
将对象值写入 XML 标记,属性 name
设置为字符串值。此字符串值 完全 您在 SharedPreference 的名称中指定的值。
(我通过对您的代码进行逐步调试确认了这一点)。
XML 将带有一个 未转义的 换行符。实际上,XmlSerializer 实例是一个 FastXmlSerializer 实例,它不会转义 \n
字符(如果您想阅读源代码,请参阅末尾的 link for this class)
一段有趣的代码:
writeValueXml(Object v, String name, XmlSerializer out) {
// -- "useless" code skipped
out.startTag(null, typeStr);
if (name != null) {
out.attribute(null, "name", name);
}
out.attribute(null, "value", v.toString());
out.endTag(null, typeStr);
// -- "useless" code skipped
}
第二:为什么结果是真的?
提交方法有以下代码:
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;
}
所以它 returns mcr.writeToDiskResult
是在 SharedPreferencesImpl.writeToFile(MemoryCommitResult)
方法中设置的。一段有趣的代码:
writeToFile(MemoryCommitResult mcr) {
// -- "useless" code skipped
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Libcore.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.
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);
}
// -- "useless" code skipped
}
正如我们在前一点看到的:XML 的写法是 "ok"(不要抛出任何东西,不要失败),所以文件中的同步也是如此(只是一个副本在另一个 Stream 中,没有检查 XML 内容!)。
当前:您的密钥已转换为(格式错误)XML 并正确写入文件。整个操作的结果是 true
,一切正常。您的更改已提交到磁盘 和内存中 。
第三个也是最后一个:为什么我第一次得到正确的值而第二次得到一个错误的值
快速浏览一下当我们在 SharedPreferences.Editor.commitToMemory(...)
方法中将更改提交到内存时会发生什么(只有有趣的部分...:)):
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this) { // magic value for a removal mutation
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
boolean isSame = false;
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
要点:更改已提交给 mMap
属性。
然后,快速看一下我们如何取回一个值:
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
我们正在从 mMap
取回密钥(暂时不读取文件中的值)。所以这次我们有了正确的值:)
当您重新加载您的应用程序时,您将从磁盘加载回数据,因此将调用 SharedPreferencesImpl
构造函数,并且它将调用 SharedPreferencesImpl.loadFromDiskLocked()
方法。这个方法会读取文件内容加载到mMap
属性中(我让你自己看代码,link最后提供)
逐步调试显示 abc\n
被写为 abc
(带有空白字符)。所以,当你试图取回它时,你永远不会成功。
最后,感谢@CommonsWare 在评论中给我关于文件内容的提示:)
链接
SharedPreferencesImpl.EditorImpl.commit()
SharedPreferencesImpl.EditorImpl.commitToMemory()
SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult, Runnable)
在 Android 中,我想编写 SharedPreferences
键值对,其中键是 Base64 字符串。
// get a SharedPreferences instance
SharedPreferences prefs = getSharedPreferences("some-name", Context.MODE_PRIVATE);
// generate the base64 key
String someKey = new String(Base64.encode("some-key".getBytes("UTF-8"), Base64.URL_SAFE), "UTF-8");
// write the value for the generated key
prefs.edit().putBoolean(someKey, true).commit();
在最后一行,调用提交 returns true
。所以这个键值对应该已经保存成功了
当我关闭并销毁使用这段代码的Activity
,然后重新创建Activity
(再次运行这段代码)时,返回指定的值对于我们使用的密钥。
但事实证明,当我销毁整个 application/process 时(例如,在应用程序设置中使用 "Force stop"),我们的密钥的值在 [=14 的下一次启动时丢失=].
当我不使用 Base64.URL_SAFE
但 Base64.URL_SAFE | Base64.NO_WRAP
作为 Base64 编码的标志时,它工作正常。
所以这个问题是由 Base64 密钥末尾的换行符引起的。像abc
这样的键可以毫无问题的写入。但是当key为abc\n
时,就失败了。
问题是它首先似乎可以正常工作,在 commit()
上返回 true
并在后续调用中返回正确的首选项值。但是当整个应用被销毁并重新启动时,这个值并没有被持久化。
这是有意为之的行为吗?错误?文档是否说明了有效的密钥名称?
我看了一下GrepCode,操作如下(没用的就不说了):
- android.app.SharedPreferencesImpl.commit()
- android.app.SharedPreferencesImpl.commitToMemory()
android.app.SharedPreferencesImpl.queueDiskWrite(MemoryCommitResult,Runnable)
3.1。 XmlUtils.writeMapXml(地图,输出流)
3.2。 XmlUtils.writeMapXml(映射、字符串、XmlSerializer)
3.3。 XmlUtils.writeValueXml(对象 v,字符串名称,XmlSerializer ser)
首先:您的数据是如何转换的?
方法 XmlUtils.writeValueXml
将对象值写入 XML 标记,属性 name
设置为字符串值。此字符串值 完全 您在 SharedPreference 的名称中指定的值。
(我通过对您的代码进行逐步调试确认了这一点)。
XML 将带有一个 未转义的 换行符。实际上,XmlSerializer 实例是一个 FastXmlSerializer 实例,它不会转义 \n
字符(如果您想阅读源代码,请参阅末尾的 link for this class)
一段有趣的代码:
writeValueXml(Object v, String name, XmlSerializer out) {
// -- "useless" code skipped
out.startTag(null, typeStr);
if (name != null) {
out.attribute(null, "name", name);
}
out.attribute(null, "value", v.toString());
out.endTag(null, typeStr);
// -- "useless" code skipped
}
第二:为什么结果是真的?
提交方法有以下代码:
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;
}
所以它 returns mcr.writeToDiskResult
是在 SharedPreferencesImpl.writeToFile(MemoryCommitResult)
方法中设置的。一段有趣的代码:
writeToFile(MemoryCommitResult mcr) {
// -- "useless" code skipped
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Libcore.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.
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);
}
// -- "useless" code skipped
}
正如我们在前一点看到的:XML 的写法是 "ok"(不要抛出任何东西,不要失败),所以文件中的同步也是如此(只是一个副本在另一个 Stream 中,没有检查 XML 内容!)。
当前:您的密钥已转换为(格式错误)XML 并正确写入文件。整个操作的结果是 true
,一切正常。您的更改已提交到磁盘 和内存中 。
第三个也是最后一个:为什么我第一次得到正确的值而第二次得到一个错误的值
快速浏览一下当我们在 SharedPreferences.Editor.commitToMemory(...)
方法中将更改提交到内存时会发生什么(只有有趣的部分...:)):
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this) { // magic value for a removal mutation
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
boolean isSame = false;
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
要点:更改已提交给 mMap
属性。
然后,快速看一下我们如何取回一个值:
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
我们正在从 mMap
取回密钥(暂时不读取文件中的值)。所以这次我们有了正确的值:)
当您重新加载您的应用程序时,您将从磁盘加载回数据,因此将调用 SharedPreferencesImpl
构造函数,并且它将调用 SharedPreferencesImpl.loadFromDiskLocked()
方法。这个方法会读取文件内容加载到mMap
属性中(我让你自己看代码,link最后提供)
逐步调试显示 abc\n
被写为 abc
(带有空白字符)。所以,当你试图取回它时,你永远不会成功。
最后,感谢@CommonsWare 在评论中给我关于文件内容的提示:)
链接
SharedPreferencesImpl.EditorImpl.commit()
SharedPreferencesImpl.EditorImpl.commitToMemory()
SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult, Runnable)