【问题标题】:Android SharedPreferences Best PracticesAndroid SharedPreferences 最佳实践
【发布时间】:2012-02-09 22:07:40
【问题描述】:

在我一直在构建的应用程序中,我们相当依赖 SharedPreferences,这让我思考在访问 SharedPreferences 时什么是最佳实践。例如,许多人说访问它的适当方式是通过以下调用:

PreferenceManager.getDefaultSharedPreferences(Context context)

但是,这似乎很危险。如果您有一个依赖 SharedPreferences 的大型应用程序,您可能会有密钥重复,尤其是在使用一些也依赖 SharedPreferences 的第三方库的情况下。在我看来,更好的使用方法是:

Context.getSharedPreferences(String name, int mode)

这样,如果您有一个严重依赖 SharedPreferences 的类,您可以创建一个仅由您的类使用的首选项文件。您可以使用类的完全限定名称来确保该文件很可能不会被其他人复制。

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

Android 开发人员在他们的应用程序中使用 SharedPreferences 时是否应该注意其他最佳做法?

【问题讨论】:

    标签: java android


    【解决方案1】:

    我写了一篇小文章,也可以在here 找到。它描述了SharedPreferences 是什么:

    最佳实践:SharedPreferences

    Android 提供了多种存储应用程序数据的方法。其中一种方法将我们引向 SharedPreferences 对象,该对象用于将私有原始数据存储在键值对中。

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

    共享首选项

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

    • 要创建SharedPreferences,您需要Context 对象(可以是应用程序Context
    • getSharedPreferences 方法解析 Preference 文件并为其创建 Map 对象
    • 您可以在 Context 提供的几种模式下创建它。您应该始终使用 MODE_PRIVATE,因为所有其他模式自 API 级别 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

    • 使用简单的界面将值放入Editor
    • commit() 同步保存值或与apply 异步保存值,后者更快。事实上,使用commit() 使用不同的线程更安全。这就是为什么我更喜欢使用 commit()
    • 使用remove() 删除单个值或使用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 是一个Singleton 对象,因此您可以轻松获取任意数量的引用,它仅在您第一次调用getSharedPreferences 时打开文件,或者只为其创建一个引用。

      // 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
      
    • 由于SharedPreferencesSingleton 对象,您可以更改任何它的实例,而不必担心它们的数据会有所不同

      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
      
    • 请记住,Preference 对象越大,getcommitapplyremoveclear 操作的时间就越长。因此强烈建议将您的数据分隔在不同的小对象中。

    • 您的偏好不会在应用程序更新后被删除。因此,在某些情况下,您需要创建一些迁移方案。例如,您有在应用程序启动时解析本地 JSON 的应用程序,要在第一次启动后执行此操作,您决定保存布尔标志 wasLocalDataLoaded。一段时间后,您更新了该 JSON 并发布了新的应用程序版本。用户将更新他们的应用程序,但他们不会加载新的 JSON,因为他们已经在第一个应用程序版本中完成了。

      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 存储在应用数据文件夹中的 xml 文件中

      // 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 guide.

    示例代码

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

    【讨论】:

    • 你的意思是什么记住,Preference 对象越大,get、commit、apply、remove 和 clear 操作的时间就越长。因此强烈建议将您的数据分隔在不同的小对象中。 小的单独的 preferences 对象?
    • @Semanticer 首选项是一个文件,它很大 - 执行所有文件操作所需的时间更长。单独的首选项文件。不要将所有数据保存在一个`context.getSharedPreferences("com.example.app", Context.MODE_PRIVATE);`中。实际上,您可以尝试根据自己的情况进行比较。也许一个文件不会影响您的性能。但是,在可理解的小和平上分离逻辑总是更好
    • 这个答案应该被标记。先生,你赢得了我的支持。
    • @YakivMospan,您说“事实上,使用 commit() 使用不同的线程更安全。这就是我更喜欢使用 commit() 的原因。”因此,如果您要从不同的Threads 保存到SharedPreferences,我们应该使用commit() 而不是apply()?使用commit() 是否更安全?线程试图在之后得到它?
    • @Sakiboy 来自文档 developer.android.com/reference/android/content/… :“由于 SharedPreferences 实例是进程中的单例,如果您已经忽略了返回值,则可以安全地将任何 commit() 实例替换为 apply()。”我前段时间写了这篇文章和文章,可能我的意思是它更安全,因为你不知道apply() 是否成功完成。如果失败,您的更改将不会保存到磁盘。
    【解决方案2】:

    如果您有一个依赖 SharedPreferences 的大型应用程序,您可能会有密钥重复,尤其是在使用一些也依赖 SharedPreferences 的第三方库的情况下。

    库不应使用特定的SharedPreferences。默认的SharedPreferences 只能由应用程序使用。

    这样,如果您有一个严重依赖 SharedPreferences 的类,您可以创建一个仅供您的类使用的首选项文件。

    当然欢迎您这样做。在应用程序级别,我不会,因为SharedPreferences 的主要原因是让它们在应用程序中的组件之间共享。开发团队管理这个命名空间应该没有问题,就像他们管理类、包、资源或其他项目级内容的名称应该没有问题一样。此外,默认的SharedPreferences 是您的PreferenceActivity 将使用的。

    但是,回到您的库这一点,可重用库应该只为其库使用单独的SharedPreferences。我不会基于一个类名,因为这样你就可以重构你的应用程序了。相反,选择一个唯一的名称(例如,基于库名称,例如 "com.commonsware.cwac.wakeful.WakefulIntentService")但稳定。

    似乎访问 SharedPreferences 应该在 UI 线程之外完成,这是有道理的。

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

    Android 开发人员在他们的应用程序中使用 SharedPreferences 时是否应该注意其他最佳做法?

    不要过度依赖它们。它们存储在 XML 文件中并且不是事务性的。数据库应该是您的主要数据存储,尤其是对于您确实不想丢失的数据。

    【讨论】:

    • 这可能看起来很业余,但仍然想问网络上缺乏资源。 android如何处理共享首选项,即应用程序的首选项文件是加载一次还是每次调用SharedPreferences都是一个文件i/o。
    • @root: SharedPreferences 被缓存。特定SharedPreferences 的首次访问会加载 XML;后续读取将在缓存中起作用。
    • @CommonsWare 我想知道存储密钥和默认值的最佳方式是什么。我可以从代码中请求特定的共享首选项并提供默认值(然后它将是 java 类中的字符串常量),但我也可以在首选项的 xml 定义中定义默认值(而不是 strings.xml 中的字符串)。密钥相同(可以将其存储在 java 类和 xml 中)。我的目标是避免在两个地方都写它,但我需要它。不知何故,将它存储在 strings_preferences.xml 中感觉太不安全了
    • 如果我正在编写一个库,我应该使用什么来代替 SharedPreferences 来存储几个简单的 key-val 对?在这一行中,“库不应使用特定的 SharedPreferences。”你的意思是SharedPreferences settings = getSharedPreferences("my_lib_pref_name", 0); 是不好的做法,因为my_lib_pref_name 可能会与应用程序的首选项名称冲突?
    • @WeishiZeng:不,你提出的是一个好的做法。我指的是 PreferenceManager.getDefaultSharedPreferences() 是错误的 SharedPreferences 使用。对于任何混淆,我深表歉意。
    【解决方案3】:

    在 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核心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)
    }
    

    其中,App 扩展了 Application 并应包含在 AndroidManifest.xml

    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")
    

    【讨论】:

      【解决方案4】:

      这是我的方式

      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 时的最佳做法。
      • 我更正它:)。 . .对不起,如果我错过了重点,但这就是我使用它们的方式
      【解决方案5】:

      让我们假设在一个项目中,有多个开发人员在开发它,他们在这样的 Activity 中定义 SharedPreference:

      SharedPreferences sharedPref = context.getSharedPreferences("prefName", 0);
      

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

      解决方案依赖两个选项,是否使用;

      1. SharedPreferences Singleton 使用字符串键。

      2. SharedPreferences Singleton 使用 Enum 键。

      就个人而言,根据Sharepreference Documentation,我更喜欢使用 Enum 键,因为当有多个程序员在一个项目上工作时,它会实施更严格的控制。程序员别无选择,只能在适当的枚举类中声明一个新键,因此所有键都在同一个位置。

      为了避免编写样板代码,请创建 SharedPreference 单例。 SharedPreferences singleton Class 有助于集中和简化 Android 应用中 SharedPreferences 的读取和写入。

      提供的两个解决方案的源代码可以在GitHub找到

      【讨论】:

        猜你喜欢
        • 2014-04-21
        • 2013-08-25
        • 2013-09-01
        • 1970-01-01
        • 2016-08-29
        • 2019-04-26
        • 1970-01-01
        • 2017-03-21
        • 2011-11-11
        相关资源
        最近更新 更多