Android SharedPreferences 最佳实践

69

我正在开发一个应用程序,我们相当依赖SharedPreferences。这让我想到了在访问SharedPreferences时最佳实践是什么。例如,许多人说通过以下方式访问它是适当的:

PreferenceManager.getDefaultSharedPreferences(Context context)

然而,这似乎可能存在危险。如果你有一个大型应用程序依赖于SharedPreferences,那么可能会出现键重复的情况,特别是在使用一些依赖SharedPreferences的第三方库的情况下。在我看来,更好的调用方式是:

Context.getSharedPreferences(String name, int mode)

如果您有一个在SharedPreferences上严重依赖的类,可以创建一个仅由该类使用的首选项文件。您可以使用该类的完整限定名,以确保文件不会被其他人重复。

此外,基于这个Stack Overflow的问题:Should accessing SharedPreferences be done off the UI Thread?,似乎应该在UI线程之外访问SharedPreferences,这是有道理的。

当Android开发人员在其应用程序中使用SharedPreferences时,是否还有其他最佳实践需要注意?

5个回答

106
我已经写了一篇关于SharedPreferences的小文章,也可以在这里找到。它描述了SharedPreferences是什么:

最佳实践:SharedPreferences

Android提供了许多存储应用程序数据的方法。其中之一是使用SharedPreferences对象,用于以键值对的形式存储私有基本数据类型。

所有逻辑都基于三个简单的类:

SharedPreferences

SharedPreferences 是其中的主要组成部分。它负责获取(解析)存储的数据,提供了获取 Editor 对象以及添加和删除 OnSharedPreferenceChangeListener 接口的界面。

  • To create SharedPreferences you will need Context object (can be an application Context)
  • getSharedPreferences method parses Preference file and creates Map object for it
  • You can create it in few modes provided by Context. You should always use MODE_PRIVATE, as all the other modes are deprecated since API level 17.

    // parse Preference file
    SharedPreferences preferences = context.getSharedPreferences("com.example.app", Context.MODE_PRIVATE);
    
    // get values from Map
    preferences.getBoolean("key", defaultValue)
    preferences.get..("key", defaultValue)
    
    // you can get all Map but be careful you must not modify the collection returned by this
    // method, or alter any of its contents.
    Map<String, ?> all = preferences.getAll();
    
    // get Editor object
    SharedPreferences.Editor editor = preferences.edit();
    
    //add on Change Listener
    preferences.registerOnSharedPreferenceChangeListener(mListener);
    
    //remove on Change Listener
    preferences.unregisterOnSharedPreferenceChangeListener(mListener);
    
    // listener example
    SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener
        = new SharedPreferences.OnSharedPreferenceChangeListener() {
      @Override
      public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
      }
    };
    

编辑器

SharedPreferences.Editor是一个接口,用于修改SharedPreferences对象中的值。您在编辑器中进行的所有更改都是批处理的,并且只有在调用commit()或apply()之后,才会将更改复制回原始的SharedPreferences

  • Use simple interface to put values in Editor
  • Save values synchronous with commit() or asynchronous with apply which is faster. In fact of using different threads using commit() is safer. Thats why I prefer to use commit().
  • Remove single value with remove() or clear all values with clear()

    // get Editor object
    SharedPreferences.Editor editor = preferences.edit();
    
    // put values in editor
    editor.putBoolean("key", value);
    editor.put..("key", value);
    
    // remove single value by key
    editor.remove("key");
    
    // remove all values
    editor.clear();
    
    // commit your putted values to the SharedPreferences object synchronously
    // returns true if success
    boolean result = editor.commit();
    
    // do the same as commit() but asynchronously (faster but not safely)
    // returns nothing
    editor.apply();
    

性能和技巧

  • SharedPreferences is a Singleton object so you can easily get as many references as you want, it opens file only when you call getSharedPreferences first time, or create only one reference for it.

    // There are 1000 String values in preferences
    
    SharedPreferences first = context.getSharedPreferences("com.example.app", Context.MODE_PRIVATE);
    // call time = 4 milliseconds
    
    SharedPreferences second = context.getSharedPreferences("com.example.app", Context.MODE_PRIVATE);
    // call time = 0 milliseconds
    
    SharedPreferences third = context.getSharedPreferences("com.example.app", Context.MODE_PRIVATE);
    // call time = 0 milliseconds
    
  • As SharedPreferences is a Singleton object you can change any of It's instances and not be scared that their data will be different

    first.edit().putInt("key",15).commit();
    
    int firstValue = first.getInt("key",0)); // firstValue is 15
    int secondValue = second.getInt("key",0)); // secondValue is also 15
    
  • Remember the larger the Preference object is the longer get, commit, apply, remove and clear operations will be. So it's highly recommended to separate your data in different small objects.

  • Your Preferences will not be removed after Application update. So there are cases when you need to create some migration scheme. For example you have Application that parse local JSON in start of application, to do this only after first start you decided to save boolean flag wasLocalDataLoaded. After some time you updated that JSON and released new application version. Users will update their applications but they will not load new JSON because they already done it in first application version.

    public class MigrationManager {
     private final static String KEY_PREFERENCES_VERSION = "key_preferences_version";
     private final static int PREFERENCES_VERSION = 2;
    
     public static void migrate(Context context) {
         SharedPreferences preferences = context.getSharedPreferences("pref", Context.MODE_PRIVATE);
         checkPreferences(preferences);
     }
    
     private static void checkPreferences(SharedPreferences thePreferences) {
         final double oldVersion = thePreferences.getInt(KEY_PREFERENCES_VERSION, 1);
    
         if (oldVersion < PREFERENCES_VERSION) {
             final SharedPreferences.Editor edit = thePreferences.edit();
             edit.clear();
             edit.putInt(KEY_PREFERENCES_VERSION, currentVersion);
             edit.commit();
         }
     }
    }
    
  • SharedPreferences are stored in an xml file in the app data folder

    // yours preferences
    /data/data/YOUR_PACKAGE_NAME/shared_prefs/YOUR_PREFS_NAME.xml
    
    // default preferences
    /data/data/YOUR_PACKAGE_NAME/shared_prefs/YOUR_PACKAGE_NAME_preferences.xml
    

Android指南。

示例代码

public class PreferencesManager {

    private static final String PREF_NAME = "com.example.app.PREF_NAME";
    private static final String KEY_VALUE = "com.example.app.KEY_VALUE";

    private static PreferencesManager sInstance;
    private final SharedPreferences mPref;

    private PreferencesManager(Context context) {
        mPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    public static synchronized void initializeInstance(Context context) {
        if (sInstance == null) {
            sInstance = new PreferencesManager(context);
        }
    }

    public static synchronized PreferencesManager getInstance() {
        if (sInstance == null) {
            throw new IllegalStateException(PreferencesManager.class.getSimpleName() +
                    " is not initialized, call initializeInstance(..) method first.");
        }
        return sInstance;
    }

    public void setValue(long value) {
        mPref.edit()
                .putLong(KEY_VALUE, value)
                .commit();
    }

    public long getValue() {
        return mPref.getLong(KEY_VALUE, 0);
    }

    public void remove(String key) {
        mPref.edit()
                .remove(key)
                .commit();
    }

    public boolean clear() {
        return mPref.edit()
                .clear()
                .commit();
    }
}

3
@Semanticer preferences是一个文件,它越大,执行所有文件操作所需的时间就越长。应该使用分开的偏好设置文件,而不是将所有数据存储在一个context.getSharedPreferences(“com.example.app”,Context.MODE_PRIVATE);中。实际上,您可以尝试并比较适用于您情况的方式。也许对您来说一个文件不会影响性能。但是,将逻辑分为小而易懂的部分始终是更好的选择。 - Yakiv Mospan
这个答案应该被标记。您赢得了我的点赞,先生。 - Jakub S.
@YakivMospan,你说:“实际上,使用commit()比使用不同的线程更安全。这就是为什么我更喜欢使用commit()。”那么,如果我们要从不同的线程保存到SharedPreferences,我们应该使用commit()而不是apply()吗?使用commit()更安全吗?如果其他线程尝试获取它后,它会确保值被正确更新吗? - Sakiboy
根据文档 https://developer.android.com/reference/android/content/SharedPreferences.Editor.html#apply(),由于SharedPreferences实例在进程内是单例的,如果您已经忽略了返回值,则可以安全地将commit()的任何实例替换为`apply()`。我之前写过这篇文章,我的意思可能是使用apply()更安全,因为您不知道它是否成功完成。如果失败,您的更改将不会保存到磁盘。 - Yakiv Mospan
@Sakiboy 但是在文档中,他们告诉我们,如果我们不处理返回值(成功或失败),则可以将每个commit()替换为apply()。对于不同的线程,根据文档,最后调用的commit()apply()将被视为有效值,无论您在一个进程中使用哪个线程,所有的commit()apply()调用都会排队,并且只有最后一个将被应用。 - Yakiv Mospan
显示剩余4条评论

46
如果您有一个依赖于SharedPreferences的大型应用程序,您可能会遇到键重复的问题,特别是在使用某些第三方库时,该库也依赖于SharedPreferences。
库不应该使用那个特定的SharedPreferences。默认的SharedPreferences应该只被应用程序使用。
这样,如果您有一个严重依赖于SharedPreferences的类,您可以创建一个仅由您的类使用的首选项文件。
当然,您可以这样做。但我不会在应用程序级别上这样做,因为SharedPreferences的主要目的是让它们在应用程序的组件之间共享。开发团队应该没有问题管理这个命名空间,就像他们应该没有问题管理类、包、资源或其他项目级别的东西的名称一样。此外,默认的SharedPreferences是您的PreferenceActivity将使用的内容。

然而,回到你提到的库的问题,可重用的库应该仅使用一个独立的 SharedPreferences。我不会以类名为基础,因为这样一来,你只需要重构一次就有可能破坏你的应用。相反,选择一个唯一且稳定的名称(例如基于库名称,如 "com.commonsware.cwac.wakeful.WakefulIntentService")。

看起来访问 SharedPreferences 应该在 UI 线程之外进行,这很有道理。

理想情况下,是的。我最近发布了一个 SharedPreferencesLoader 来帮助解决这个问题。

除了使用 SharedPreferences,Android 开发者还应该注意哪些最佳实践?

不要过度依赖它们。它们存储在 XML 文件中,并且没有事务处理机制。数据库应该是你的主要数据存储方式,特别是对于那些你不想丢失的数据。


3
SharedPreferences 是被缓存的。第一次访问特定的 SharedPreferences 会加载 XML,后续读取将从缓存中进行。 - CommonsWare
如果我正在编写一个库,存储多个简单的键值对,那么我应该使用什么来替代SharedPreferences?在这一行中,“库不应使用特定的SharedPreferences。”你的意思是SharedPreferences settings = getSharedPreferences("my_lib_pref_name", 0);是不好的实践,因为my_lib_pref_name可能会与应用程序的首选项名称冲突吗? - Weishi Z
@CommonsWare 非常感谢! - Weishi Z
@garnet:我无法真正回答这个问题。你是正确的,额外的缓存并不是必要的。避免依赖框架类通常通过具有多个实现的接口或通过在测试中使用的具体实现加上模拟来处理。 - CommonsWare
请查看以下链接,了解简单的实现方式:https://arkapp.medium.com/how-to-use-shared-preferences-the-easy-and-fastest-way-98ce2013bf51 - abdul rehman
显示剩余4条评论

7
在Kotlin中,使用SharedPreferences可以通过以下方式简化。
class Prefs(context: Context) {

    companion object {
        private const val PREFS_FILENAME = "app_prefs"

        private const val KEY_MY_STRING = "my_string"
        private const val KEY_MY_BOOLEAN = "my_boolean"
        private const val KEY_MY_ARRAY = "string_array"
    }

    private val sharedPrefs: SharedPreferences =
        context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE)

    var myString: String
        get() = sharedPrefs.getString(KEY_MY_STRING, "") ?: ""
        set(value) = sharedPrefs.edit { putString(KEY_MY_STRING, value) }

    var myBoolean: Boolean
        get() = sharedPrefs.getBoolean(KEY_MY_BOOLEAN, false)
        set(value) = sharedPrefs.edit { putBoolean(KEY_MY_BOOLEAN, value) }

    var myStringArray: Array<String>
        get() = sharedPrefs.getStringSet(KEY_MY_ARRAY, emptySet())?.toTypedArray()
            ?: emptyArray()
        set(value) = sharedPrefs.edit { putStringSet(KEY_MY_ARRAY, value.toSet()) }

这里提供的 sharedPrefs.edit{...} 是由Android Core KTX库提供的,并且应该通过在应用程序级别 build.gradle中添加依赖项implementation "androidx.core:core-ktx:1.0.2"来实现。

您可以使用以下代码获取 SharedPreferences 的实例:

val prefs = Prefs(context)

此外,您可以创建PrefsSingleton对象,并在应用程序的任何地方使用。
val prefs: Prefs by lazy {
    Prefs(App.instance)
}

AndroidManifest.xml中应包含App,该类扩展自Application

App.kt

class App:Application() {
    companion object {
        lateinit var instance: App
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest .....

   <application
        android:name=".App"
        ....

例子用法:

// get stored value
val myString = prefs.myString

// store value
prefs.myString = "My String Value"

// get stored array
val myStringArray = prefs.myStringArray

// store array
prefs.myStringArray = arrayOf("String 1","String 2","String 3")

5
这是我写作的方式
关于IT技术的相关内容
SharedPreferences settings = context.getSharedPreferences("prefs", 0);
SharedPreferences.Editor editore = settings.edit();
editore.putString("key", "some value");
editore.apply();

阅读

SharedPreferences settings = getSharedPreferences("prefs", 0);
Strings value = settings.getString("key", "");

我认为你把这两个搞反了。第一部分是写入,第二部分是读取。此外,你的答案并没有回答我的问题。我已经知道如何使用SharedPreferences了。我的问题是在使用SharedPreferences时的最佳实践。 - Kam Sheffield
我修改了它 :) . . . 如果我有所偏差,请原谅,但这是我使用它们的方式。 - Lukap

0
假设在一个项目中,有多个开发人员正在工作,他们像这样在Activity中定义SharedPreference:
SharedPreferences sharedPref = context.getSharedPreferences("prefName", 0);

在某个时候,两个开发者可能会定义相同名称的SharedPreference或插入等效的键值对,这将导致使用键时出现问题。

解决方案有两个选项:

  1. 使用String类型键的SharedPreferences单例。

  2. 使用Enum类型键的SharedPreferences单例。

根据这份Sharepreference文档,我个人更喜欢使用Enum类型键,因为当多个程序员共同开发一个项目时它能够强制执行更严格的控制。程序员必须在适当的枚举类中声明一个新的键,从而使所有的键都在同一个位置。

为了避免重复编写代码,创建SharedPreference单例是一个不错的选择。这个SharedPreferences单例类有助于在Android应用程序中集中和简化SharedPreferences的读写操作。

这两个解决方案的源代码可以在GitHub中找到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接