当密钥包含换行符时,Android中的SharedPreferences不会持久保存到磁盘

caw*_*caw 3 android sharedpreferences

在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();
Run Code Online (Sandbox Code Playgroud)

在最后一行中,对commit的调用返回true.所以这个键值对应该已经成功保存.

当我关闭并销毁使用Activity此代码的位置然后重新创建Activity(再次运行此代码)时,将为我们使用的密钥返回指定的值.

但事实证明,当我销毁整个应用程序/进程时(例如在应用程序设置中使用"强制停止"),我们的密钥值将在下次启动时丢失Activity.

当我不使用Base64.URL_SAFEBase64.URL_SAFE | Base64.NO_WRAP作为Base64编码的标志时,它工作正常.

所以这个问题是由Base64键末尾的换行引起的.像这样的键abc可以毫无问题地编写.但是当关键是abc\n,它失败了.

问题是,它似乎没有问题,工作第一,返回truecommit()并返回后续调用正确的优先级.但是当整个应用程序被销毁并重新启动时,该值并未持久存在.

这是预期的行为吗?一个bug?文档是否说明了有效的密钥名称?

mit*_*rop 7

我看了看GrepCode并看到操作将如下(我没有提到无用的):

  1. android.app.SharedPreferencesImpl.commit()
  2. android.app.SharedPreferencesImpl.commitToMemory()
  3. android.app.SharedPreferencesImpl.queueDiskWrite(MemoryCommitResult,可运行)

    3.1.XmlUtils.writeMapXml(Map,OutputStream)

    3.2.XmlUtils.writeMapXml(Map,String,XmlSerializer)

    3.3.XmlUtils.writeValueXml(对象v,字符串名称,XmlSerializer ser)


第一:你的数据如何转换?

该方法XmlUtils.writeValueXml将Object值写入XML标记,并将属性name设置为String值.该字符串值包含正是你在SharedPreference名指定的值.

(我通过对您的代码进行逐步调试来证实这一点).

XML将使用未转义的换行符.实际上,XmlSerializer实例是一个FastXmlSerializer实例,它不会转义该\n字符(如果要读取源代码,请参阅最后一个类的链接)

有趣的一段代码:

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
}
Run Code Online (Sandbox Code Playgroud)

第二:为什么结果是真的?

commit方法具有以下代码:

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;
}
Run Code Online (Sandbox Code Playgroud)

所以它返回mcr.writeToDiskResultSharedPreferencesImpl.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
}
Run Code Online (Sandbox Code Playgroud)

正如我们在前面所看到的那样: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

重要的一点:更改将提交到mMap属性.

然后,快速查看我们如何获取值:

public boolean getBoolean(String key, boolean defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        Boolean v = (Boolean)mMap.get(key);
        return v != null ? v : defValue;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们正在从中取回密钥mMap(暂时不读取文件中的值).所以我们这次有正确的价值:)

当您重新加载应用程序时,您将从磁盘加载数据,因此SharedPreferencesImpl将调用构造函数,它将调用该SharedPreferencesImpl.loadFromDiskLocked()方法.此方法将读取文件内容并将其加载到mMap属性中(我让您自己查看代码,最后提供链接).

一步一步的调试向我展示了它abc\n被写成abc(带有空白字符).所以,当你试图恢复它时,你永远不会成功.


完成后,感谢@CommonsWare给我一个关于评论文件内容的提示:)

链接

XmlUtils

FastXmlSerializer

SharedPreferencesImpl

SharedPreferencesImpl.EditorImpl.commit()

SharedPreferencesImpl.EditorImpl.commitToMemory()

SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult,Runnable)

SharedPreferencesImpl.writeToFile(MemoryCommitResult)

SharedPreferencesImpl.loadFromDiskLocked()