在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();

在最后一行,commit调用返回true。所以这个键值对应该已经成功保存了。
当我关闭并销毁使用这段代码的Activity,然后重新创建Activity(再次运行此代码)时,将返回我们使用的密钥的指定值。
但事实证明,当我销毁整个应用程序/进程(例如,在app settings中使用“force stop”)时,我们的密钥值在下次启动Activity时丢失。
当我不使用Base64.URL_SAFE但使用Base64.URL_SAFE | Base64.NO_WRAP作为base64编码的标志时,它可以正常工作。
所以这个问题是由base64键末尾的换行引起的。像abc这样的键可以毫无问题地编写。但是当键是abc\n时,它失败了。
问题是,它似乎首先可以正常工作,在随后的调用中返回true并返回正确的首选项值。但当整个应用程序被破坏并重新启动时,该值并没有被持久化。
这是故意的行为吗?虫子?文档中有没有提到有效的密钥名?

最佳答案

我看了一下grepcode,发现操作如下(我没有提到无用的操作):
android.app.SharedPreferencesImpl.Commit()
android.app.sharedPreferencesImpl.CommitToMemory()
android.app.sharedPreferencesImpl.QueueDiskWrite(memoryCommitResult,可运行)
3.1。xmlutils.writemapxml(映射,outputstream)
3.2。xmlutils.writemapxml(映射、字符串、xmlserializer)
3.3。xmlutils.writevaluexml(对象v,字符串名称,xmlserializer ser)
第一:如何转换数据?
方法XmlUtils.writeValueXml将对象值写入xml标记,并将属性name设置为字符串值。此字符串值正好包含您在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
}

第二:为什么结果是真的?
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;
}

所以它返回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编写是“好的”(不要抛出任何东西,不要失败),因此文件中的同步也将是(只是另一个流中的一个副本,这里没有检查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属性中(我让您自己查看代码,最后提供链接)。
一步一步的调试显示abc\n是以abc的形式编写的(带有空格字符)。所以,当你试图找回它的时候,你永远不会成功。
最后,感谢@commonware在评论中给我一个关于文件内容的提示:)
链接
XmlUtils
FastXmlSerializer
SharedPreferencesImpl
SharedPreferencesImpl.EditorImpl.commit()
SharedPreferencesImpl.EditorImpl.commitToMemory()
SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult, Runnable)
SharedPreferencesImpl.writeToFile(MemoryCommitResult)
SharedPreferencesImpl.loadFromDiskLocked()

09-10 01:44