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

时间:2015-02-07 17:26:57

标签: 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();

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

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

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

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

所以这个问题是由Base64键末尾的换行引起的。像abc这样的键可以毫无问题地编写。但是当密钥为abc\n时,它会失败。

问题是它似乎首先没有问题,在true上返回commit()并在后续调用中返回正确的首选项值。但是当整个应用程序被销毁并重新启动时,该值并未持久存在。

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

1 个答案:

答案 0 :(得分: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)


  4. 首先:如何转换数据?

    方法XmlUtils.writeValueXml将Object值写入XML标记中,并将属性name设置为String值。此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
    }
    

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

    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编写是" 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属性中(我让您自己查看代码,最后提供链接)。

    逐步调试向我显示abc\n被写为abc(带有空格字符)。所以,当你试图恢复它时,你永远不会成功。


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

    <强>链接

    XmlUtils

    FastXmlSerializer

    SharedPreferencesImpl

    SharedPreferencesImpl.EditorImpl.commit()

    SharedPreferencesImpl.EditorImpl.commitToMemory()

    SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult, Runnable)

    SharedPreferencesImpl.writeToFile(MemoryCommitResult)

    SharedPreferencesImpl.loadFromDiskLocked()