Android——SharedPreferences

Posted 叫我金城武也行

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——SharedPreferences相关的知识,希望对你有一定的参考价值。

1. 场景

首先我们来看看,SharedPreferences在什么场景比较适用:

  • SharedPreferences是一种轻量级的存储,数据会写在本地的一个xml文件中,以键值对的形式存在,如果程序卸载,此文件也跟着卸载
  • 以Map形式存放简单的配置参数
  • 可以保存的数据类型有:int、boolean、float、long、String、StringSet。
  • 保存位置:data/data/程序包名/share_prefs
  • 主要用途:
    • 1.保存应用的设置 例如:设置静音,下次进入还是静音
    • 2.判断是否是第一次登陆

性能:

  • ShredPreferences是单例对象,第一次打开后,之后获取都无需创建,速度很快。
  • 当第一次获取数据后,数据会被加载到一个缓存的Map中,之后的读取都会非常快。
  • 当由于是XML<->Map的存储方式,所以,数据越大,操作越慢,get、commit、apply、remove、clear都会受影响,所以尽量把数据按功能拆分成若干份。

2. SharedPreferences的使用

  • SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过SharedPreferences.edit()获取的内部接口Editor对象实现
  • 使用Preference来存取数据,用到了SharedPreferences接口和SharedPreferences的一个内部接口SharedPreferences.Editor,这两个接口在android.content包中

2.1 存储数据:

  • 使用Activity类的getSharedPreferences方法获得SharedPreferences对象;
  • 使用SharedPreferences接口的edit获得SharedPreferences.Editor对象;
  • 通过SharedPreferences.Editor接口的putXXX方法保存key-value对;
  • 通过过SharedPreferences.Editor接口的commit方法保存key-value对。

2.2 读取数据:

  • 使用Activity类的getSharedPreferences方法获得SharedPreferences对象;
  • 通过SharedPreferences对象的getXXX方法获取数据

2.3 方法:

获取SharedPreferences对象:

//根据name查找SharedPreferences,若已经存在则获取,若不存在则创建一个新的
public abstract SharedPreferences getSharedPreferences (String name, int mode)

参数:
name:命名
mode:模式,包括

  • MODE_PRIVATE(只能被自己的应用程序访问)
  • MODE_WORLD_READABLE(除了自己访问外还可以被其它应该程序读取)
  • MODE_WORLD_WRITEABLE(除了自己访问外还可以被其它应该程序读取和写入)

若该Activity只需要创建一个SharedPreferences对象的时候,可以使用getPreferences方法,不需要为SharedPreferences对象命名,只要传入参数mode即可

public SharedPreferences getPreferences (int mode)

获取Editor对象(由SharedPreferences对象调用):

abstract SharedPreferences.Editor edit()

写入数据(由Editor对象调用):

参数:
key:指定数据对应的key
value:指定的值

//写入boolean类型的数据
abstract SharedPreferences.Editor   putBoolean(String key, boolean value)
//写入float类型的数据
abstract SharedPreferences.Editor   putFloat(String key, float value)
//写入int类型的数据
abstract SharedPreferences.Editor   putInt(String key, int value)
//写入long类型的数据
abstract SharedPreferences.Editor   putLong(String key, long value)
//写入String类型的数据
abstract SharedPreferences.Editor   putString(String key, String value)
//写入Set<String>类型的数据
abstract SharedPreferences.Editor   putStringSet(String key, Set<String> values)

移除指定key的数据(由Editor对象调用):

abstract SharedPreferences.Editor   remove(String key)

清空数据(由Editor对象调用):

abstract SharedPreferences.Editor   clear()

提交数据(由Editor对象调用):

abstract boolean    commit()

读取数据(由SharedPreferences对象调用):

参数
key:指定数据的key
defValue:当读取不到指定的数据时,使用的默认值defValue

//读取所有数据
abstract Map<String, ?> getAll()
//读取的数据为boolean类型
abstract boolean    getBoolean(String key, boolean defValue)
//读取的数据为float类型
abstract float  getFloat(String key, float defValue)
//读取的数据为int类型
abstract int    getInt(String key, int defValue)
//读取的数据为long类型
abstract long   getLong(String key, long defValue)
//读取的数据为String类型
abstract String getString(String key, String defValue)
//读取的数据为Set<String>类型
abstract Set<String>    getStringSet(String key, Set<String> defValues)

2.4 例子

 1)写入数据:
     //步骤1:创建一个SharedPreferences对象
     SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
     //步骤2: 实例化SharedPreferences.Editor对象
     SharedPreferences.Editor editor = sharedPreferences.edit();
     //步骤3:将获取过来的值放入文件
     editor.putString("name",Tom);
     editor.putInt("age", 28);
     editor.putBoolean("marrid",false);
     //步骤4:提交               
     editor.commit();


 2)读取数据:
     SharedPreferences sharedPreferences= getSharedPreferences("data", Context .MODE_PRIVATE);
     String userId=sharedPreferences.getString("name","");

3)删除指定数据
     editor.remove("name");
     editor.commit();


4)清空数据
     editor.clear();
     editor.commit();

3 Tips

tip 1:

是否在Acitivty里执行?

在Acitivty中执行的:

getSharedPreferences (String name, int mode)
getPreferences (int mode) 
MODE_PRIVATE

不在Activity中:

context.getSharedPreferences (String name, int mode)
context.getPreferences (int mode)
Conetxt.MODE_PRIVATE 

tip 2:

  • 所保存的SharedPreferences数据将一直存在,除非被覆盖、移除、清空或文件被删除。
  • SharedPreferences保存的数据会随着应用的卸载而被删除

tip 3:

同时执行这两句代码的时候,第一行代码所写的内容会被第二行代码取代。

editor.putInt("age", 20);
//覆盖key为age的数据,得到的结果:age = 32
editor.putInt("age", 32);

editor.putString("age", "20");
//覆盖key为age的数据,得到的结果:age = 32 (int类型)
editor.putInt("age", 32);

tip 4:

执行以下代码会出现异常。
(指定key所保存的类型和读取时的类型不同)

editor.putInt("age", 32);//保存为int类型
String age = userInfo.getString("age", "null");//读取时为String类型,出现异常

tip 5:

在这些动作之后,记得commit

editor.putInt("age", 20);//写入操作
editor.remove("age");   //移除操作
editor.clear();     //清空操作
editor.commit();//记得commit

4. SharedPreferences的问题

其实SharedPreferences作为一种轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,初始化 SharedPreference 的时候,会将整个文件内容加载内存中,因此会带来以下问题:

  • getXXX()获取值的形式有可能会导致主线程阻塞
  • SharedPreferences不能保证类型安全, 加载的数据会一直留在内存中,浪费内存
  • apply()方法虽然是异步的,可能会发生 ANR
  • apply() 方法无法获取到操作成功或者失败的结果

4.1 为什么getXXX()方法会导致主线程阻塞

因为getXXX()都是同步的,在主线程调用 get 方法时,同步方法内调用了 wait() 方法,会一直等待 getSharedPreferences() 方法开启的线程读取完数据才能继续往下执行,如果数据量读取的小,并没有什么影响,如果读取的文件较大会导致主线程阻塞

具体大家可以查看haredPreferences源码:
frameworks/base/core/java/android/app/SharedPreferencesImpl.java

4.2 SharedPreferences不能保证类型安全,并且一直会留在内存中

 	val key = "DataStore"
    val sp = getSharedPreferences("文件名", Context.MODE_PRIVATE) 
    sp.edit  putInt(key, 0)  // 使用 Int 类型的数据覆盖相同的 key
    sp.getString(key, ""); // 使用相同的 key 读取 Sting 类型的数据

使用 Int 类型的数据覆盖掉相同的 key,然后使用相同的 key 读取 Sting 类型的数就会造成ClassCastException异常
而通过 getSharedPreferences() 方法加载的数据,最后会将数据存储在静态的成员变量中。

4.3 apply()方法是异步的,可能会发生ANR

异步的提交为什么会发生ANR呢?

apply 方法中:

@Override
        public void apply() 
            final long startTime = System.currentTimeMillis();
            //将修改先写入内存 
            final MemoryCommitResult mcr = commitToMemory(); 
            //这里只是创建了一个 Runnable ,并不是一个线程
            final Runnable awaitCommit = new Runnable() 
                    @Override
                    public void run() 
                        try 
                           //注意这里会进行等待也就是 需要 MemoryCommitResult 的 setDiskWriteResult 方法执行后
                           //才能返回 

                            mcr.writtenToDiskLatch.await();
                         catch (InterruptedException ignored) 
                        

                        if (DEBUG && mcr.wasWritten) 
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        
                    
                ;

            //添加到队列中
            QueuedWork.addFinisher(awaitCommit);

            //这里也只创建一个 Runnable 
            Runnable postWriteRunnable = new Runnable() 
                    @Override
                    public void run() 
                    //这里执行了上面的 awaitCommit 的 run 方法
                    //不是 start 
                    //并将队列中的 awaitCommit 移除
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    
                ;
            //添加到队列中
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
        
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) 
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        //创建一个 Runnable ,同样也没有 start 
        final Runnable writeToDiskRunnable = new Runnable() 
                @Override
                public void run() 
                    synchronized (mWritingToDiskLock) 
                        writeToFile(mcr, isFromSyncCommit);
                    
                    synchronized (mLock) 
                        mDiskWritesInFlight--;
                    
                    if (postWriteRunnable != null) 
                        postWriteRunnable.run();
                    
                
            ;
         //如果是 commit 则执行这里并返回
        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) 
            boolean wasEmpty = false;
            synchronized (mLock) 
                wasEmpty = mDiskWritesInFlight == 1;
            
            if (wasEmpty) 
                writeToDiskRunnable.run();
                return;
            
        
         //如果是 apply 就执行这里
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    

在 QueuedWork.java 中:

public static void queue(Runnable work, boolean shouldDelay) 
       //getHandler 获取的是一个 handlerThread 的hanlder ,也就是一个子线程
        Handler handler = getHandler();

        synchronized (sLock) 
            sWork.add(work);

            if (shouldDelay && sCanDelay) 
            //发送一个消息 MSG_RUN 到 handler 所在线程,也就是 handlerThread 子线程中去
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
             else 
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            
        
    

    private static Handler getHandler() 
        synchronized (sLock) 
            if (sHandler == null) 
            //创建一个 handlerThread ,并执行 start 方法
            //这就是 apply 写到磁盘的线程
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();

                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            
            return sHandler;
        
    
// Handler 的处理
private static class QueuedWorkHandler extends Handler 
        static final int MSG_RUN = 1;

        QueuedWorkHandler(Looper looper) 
            super(looper);
        

        public void handleMessage(Message msg) 
            if (msg.what == MSG_RUN) 
                processPendingWork();
            
        
    

    private static void processPendingWork() 
        long startTime = 0;


        synchronized (sProcessingWork) 
            LinkedList<Runnable> work;

            synchronized (sLock) 
             //复制前面的工作队列 
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            
            //一个一个执行 run 方法,
            if (work.size() > 0) 
                for (Runnable w : work) 
                    w.run();
                


                
            
        
    

在上面的方法中注意到 对每个 apply 都会创建一个相应的 awaitCommit,并添加到 QueuedWork 的一个队列中,但是在 QueuedWork 注意到有这样一个方法 waitToFinish

/**
     * Trigger queued work to be processed immediately. The queued work is processed on a separate
     * thread asynchronous. While doing that run and process all finishers on this thread. The
     * finishers can be implemented in a way to check weather the queued work is finished.
     *
     * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
     * after Service command handling, etc. (so async work is never lost)
     */
    public static void waitToFinish() 

这个方法会在 Activity 暂停时或者 BroadcastReceiver 的 onReceive 方法调用后或者 service 的命令处理后被调用,并且调用这个方法的目的是为了确保异步任务被及时完成。
可以看到 waitToFinish 都是在 ActivityThread 中,也就是主线程调用的

public static void waitToFinish() 

        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) 
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) 
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);

            

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try 
        //这个方法就会执行 所有的 Runnable 的run 返回
        //这个时候 processPendingWork 是执行在主线程中

            processPendingWork();
         finally 
            StrictMode.setThreadPolicy(oldPolicy);
        

        try 
            while (true) 
                Runnable finisher;

                synchronized (sLock) 
                    finisher = sFinishers.poll();
                

                if (finisher == null) 
                    break;
                

                finisher.run();
            
         finally 
            sCanDelay = true;
        

       ...
    

这种设计是为了保证 SP 可靠的、保证写入完成的存储机制。

总结一下就是:

  • apply 没有返回值
  • apply 是在主线程将修改数据提交到内存, 然后再子线程(HandleThread)提交到磁盘
  • apply 会将 Runnble 添加到 QueueWork 中,如果主线程 Activity 暂停时或者 BroadcastReceiver 的 onReceive 方法调用后或者 service 的- 命令处理,就会在主线程执行 提交到硬盘的方法,这个过程就会造成主线程 ANR

5 Google推荐使用DataStore 替换 SharedPreferences

Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。

如果您当前在使用 SharedPreferences 存储数据,请考虑迁移到 DataStore。

DataStore | Android开发者平台

以上是关于Android——SharedPreferences的主要内容,如果未能解决你的问题,请参考以下文章

[android] sharedPreference入门

Android中如何设置SharedPreference文件名称?

如何在android中使用SharedPreference存储动态表值[重复]

Android 如何从 SharedPreference 设置 EditTextPreference 的默认值?

Android [SharedPreference轻量级存储]

Android入门第51天-使用Android的SharedPreference存取信息