SharedPreferences详解

因为Jetpack组件出现了一个DataStore组件,来准备替换SharedPreference。所以在此分类下讲解一下SharedPreferences。

一.基本使用

1.简介

SharedPreferences是Android平台上轻量级数据存储方式,用来保存App的各种配置信息。其本质是一个以 键值对(key-value)的方式保存数据的xml文件,其保存在/data/data/shared_prefs目录下。使用SharedPreferences最常用到的是两个接口。SharedPreferences接口和Editor接口。

源码

public interface SharedPreferences {


   public interface Editor {

     
     ...
     

   }



   ...

}

官网

SharedPreferences  |  Android Developers

2.简单使用

代码

package com.wjn.rxdemo.datastore;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.wjn.rxdemo.R;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class SharedPreferenceActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView textView1;
    private TextView textView2;
    private TextView textView3;
    private SharedPreferences sharedPreferences;
    private SharedPreferences.Editor editor;
    private final static String SHARED_PREFERENCES_NAME = "MySharedPreferences";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sharedpreference);
        findView();
    }

    /**
     * 初始化各种View
     */

    private void findView() {
        textView1 = findViewById(R.id.activity_sharedperference_textview1);
        textView2 = findViewById(R.id.activity_sharedperference_textview2);
        textView3 = findViewById(R.id.activity_sharedperference_textview3);
        textView1.setOnClickListener(this);
        textView2.setOnClickListener(this);
        textView3.setOnClickListener(this);

        //获取SharedPreferences对象
        sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    }

    /**
     * 各种点击事件的方法
     */

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.activity_sharedperference_textview1://存数值
                saveSharedPreferences();
                break;
            case R.id.activity_sharedperference_textview2://取数值
                getSharedPreferences();
                break;
            case R.id.activity_sharedperference_textview3://清空数值
                clearSharedPreferences();
                break;
            default:
                break;
        }
    }


    /**
     * 存数值
     */

    private void saveSharedPreferences() {
        //存数值时 首先获取SharedPreferences.Editor对象
        editor = sharedPreferences.edit();
        //存Int类型
        editor.putInt("intType", 12);
        //存Long类型
        editor.putLong("longType", 123456);
        //存Float类型
        editor.putFloat("floatType", 12.34f);
        //存Boolean类型
        editor.putBoolean("booleanType", false);
        //存String类型
        editor.putString("stringType", "张三");
        //存StringSet类型
        Set<String> strings = new HashSet<>();
        strings.add("Set111");
        strings.add("Set222");
        strings.add("Set333");
        editor.putStringSet("setType", strings);
        //最终提交 apply异步提交
        editor.apply();
    }

    /**
     * 取数值
     */

    private void getSharedPreferences() {
        int i = sharedPreferences.getInt("intType", -1);
        long l = sharedPreferences.getLong("longType", -1);
        float f = sharedPreferences.getFloat("floatType", -1);
        boolean b = sharedPreferences.getBoolean("booleanType", false);
        String s = sharedPreferences.getString("stringType", "");
        Set<String> set = sharedPreferences.getStringSet("setType", null);

        Log.d("TAG", "SharedPreferences取值 Int类型 ----:" + i);
        Log.d("TAG", "SharedPreferences取值 Long类型 ----:" + l);
        Log.d("TAG", "SharedPreferences取值 Float类型 ----:" + f);
        Log.d("TAG", "SharedPreferences取值 Boolean类型 ----:" + b);
        Log.d("TAG", "SharedPreferences取值 String类型 ----:" + s);

        Log.d("TAG", "SharedPreferences取值 Set类型 ----:" + set);
        if (null != set) {
            Iterator<String> iterator = set.iterator();
            while (iterator.hasNext()) {
                String s1 = iterator.next();
                Log.d("TAG", "SharedPreferences取值 Set类型 ----:" + s1);
            }
        }
    }

    /**
     * 清空数值
     */

    private void clearSharedPreferences() {
        editor = sharedPreferences.edit();
        editor.clear();
        editor.apply();
    }
}

结果

存值 然后 取值

D/TAG: SharedPreferences取值 Int类型 ----:12


D/TAG: SharedPreferences取值 Long类型 ----:123456


D/TAG: SharedPreferences取值 Float类型 ----:12.34


D/TAG: SharedPreferences取值 Boolean类型 ----:false


D/TAG: SharedPreferences取值 String类型 ----:张三


D/TAG: SharedPreferences取值 Set类型 ----:[Set222, Set333, Set111]


D/TAG: SharedPreferences取值 Set类型 ----:Set222


D/TAG: SharedPreferences取值 Set类型 ----:Set333


D/TAG: SharedPreferences取值 Set类型 ----:Set111

此时 MySharedPreferences.xml文件内容

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <float name="floatType" value="12.34" />
    <boolean name="booleanType" value="false" />
    <int name="intType" value="12" />
    <string name="stringType">张三</string>
    <set name="setType">
        <string>Set222</string>
        <string>Set333</string>
        <string>Set111</string>
    </set>
    <long name="longType" value="123456" />
</map>

结果

清空  然后 取值

D/TAG: SharedPreferences取值 Int类型 ----:-1


D/TAG: SharedPreferences取值 Long类型 ----:-1


D/TAG: SharedPreferences取值 Float类型 ----:-1.0


D/TAG: SharedPreferences取值 Boolean类型 ----:false


D/TAG: SharedPreferences取值 String类型 ----:


D/TAG: SharedPreferences取值 Set类型 ----:null

此时 MySharedPreferences.xml文件内容

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map />

说明

上面是清空全部xml文件的数据。使用

editor.clear();

方法。当然也可以清除单独Key对应的数据。使用

editor.remove("intType");//清除Key:intType对应的 Value值

二.方法说明

由上述代码可知,使用SharedPreferences。一般的步骤如下。

1.获取SharedPreferences对象

SharedPreferences sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);

用的是 getSharedPreferences方法

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}

参数name:生成xml文件的文件名。

参数mode:模式 有四种取值

<1> Context.MODE_PRIVATE

默认操作模式 代表该文件是私有数据只能被应用本身访问。在该模式下 写入的内容会覆盖原文件的内容。


<2> Context.MODE_APPEND

该模式会检查文件是否存在 存在就往文件追加内容 否则就创建新文件。


<3> Context.MODE_WORLD_READABLE

该模式下 当前文件可以被其他应用读取。


<4> Context.MODE_WORLD_WRITEABLE

该模式下 当前文件可以被其他应用写入。

一般使用 Context.MODE_PRIVATE  即默认模式即可。

2.获取SharedPreferences.Editor 对象

SharedPreferences.Editor editor = sharedPreferences.edit();

利用SharedPreferences对象的edit()方法获取SharedPreferences.Editor 对象。

3.用SharedPreferences.Editor对象添加 各种类型的值 共支持6种类型。下面的6种类型举例。

//存Int类型
editor.putInt("intType", 12);

//存Long类型
editor.putLong("longType", 123456);

//存Float类型
editor.putFloat("floatType", 12.34f);

//存Boolean类型
editor.putBoolean("booleanType", false);

//存StringSet类型
editor.putString("stringType", "张三");

//存String类型
Set<String> strings = new HashSet<>();
strings.add("Set111");
strings.add("Set222");
strings.add("Set333");
editor.putStringSet("setType", strings);

4.用SharedPreferences.Editor对象 提交 存储的内容到xml文件

//异步提交
editor.apply();

//同步提交
editor.commit();

有两个方法,分别支持异步提交和同步提交。具体分别下面讲解。

5.用SharedPreferences对象获取存储Key对应的Value值

int i = sharedPreferences.getInt("intType", -1);

long l = sharedPreferences.getLong("longType", -1);

float f = sharedPreferences.getFloat("floatType", -1);

boolean b = sharedPreferences.getBoolean("booleanType", false);

String s = sharedPreferences.getString("stringType", "");

Set<String> set = sharedPreferences.getStringSet("setType", null);

getXXX方法都是两个参数

参数1:Key。

参数2:默认值 即对应的Key取不到内容时默认值。

6.清空全部或者移除指定Key对应的Value值

//清空整个xml文件的内容
editor.clear();


//移除指定Key对应的Value
editor.remove("Key");

注意:无论是clear清空还是remove移除指定Key对应的Value最后都要apply提交否则无效

三.源码分析

上述讲解了 SharedPreferences的基础使用。比如

存储数值使用putXXX()方法

获取数值使用getXXX()方法

提交内容使用apply()方法

清空全部数值使用clear()方法

删除单独Key对应的Value使用 remove("XXXKey")方法。等等。

截图

那么SharedPreferences有何缺点呢?为什么会有DataStore替换它呢。下面从SharedPreferences源码解析说明。

由于SharedPreferencesEditor都是接口。所以要看源码需要找到两个接口的实现类SharedPreferencesImpl类和EditorImpl

(Editor接口SharedPreferences接口内部接口。同样EditorImpl实现类SharedPreferencesImpl实现类内部类)

1.SharedPreference缺点一:不能保证类型安全

众所周知SharedPreference是以键值对的形式存取值的。那么在使用的过程中,如果没有合理的管理项目中众多的Key-Value。可能就会出现问题。

比如 项目中 同一个.xml文件中 某个地方使用Key:age 存放Int类型的字段 表示年龄。如果没有合理的管理Key。其他地方可能使用Key:age 存放的是String类型的字段 也表示年龄。

这样以Int类型存值的地方如果不知道 还是按照Int类型取数据就会有问题。

存数据

editor = sharedPreferences.edit();


editor.putInt("age", 12);//某个地方



editor.putString("age", "12岁");//其他地方修改



editor.apply();

取数据 还是按照原来Int类型取值

 int age1 = sharedPreferences.getInt("age", -1);

报错 ClassCastException 

2.SharedPreference缺点二:写入方式是全量更新方式 效率低下 

由于SharedPreference使用xml格式来保存数据。所以每次更新数据只能全量替换更新数据。这意味着如果我们有1000个数据,如果只更新一项数据,也需要将所有数据转化成xml格式,然后再通过IO写入文件中。

这也就是SharedPreference效率比较低的原因。

源码分析

SharedPreferencesImpl实现类 构造方法中

SharedPreferencesImpl(File file, int mode) {
  
    ...
    
   startLoadFromDisk();
}

startLoadFromDisk()方法源码 (这个方法就是看是读取磁盘中的xml文件) 也就是获取 SharedPreferences对象时调用的

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

此方法中新开了一个线程,执行 loadFromDisk()方法。

 loadFromDisk()方法源码

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
                mThrowable = t;
        } finally {
                mLock.notifyAll();
        }
    }
}

源码中,也可以看出。由于SharedPreference的xml保存数据方式,导致每次都要重新读取磁盘中xml文件的流。也就是全量更新

3.SharedPreference缺点三:commit()提交容易造成ANR

 /**
 * Commit your preferences changes back from this Editor to the
 * {@link SharedPreferences} object it is editing.  This atomically
 * performs the requested modifications, replacing whatever is currently
 * in the SharedPreferences.
 *
 * <p>Note that when two editors are modifying preferences at the same
 * time, the last one to call commit wins.
 *
 * <p>If you don't care about the return value and you're
 * using this from your application's main thread, consider
 * using {@link #apply} instead.
 *
 * @return Returns true if the new values were successfully written
 * to persistent storage.
 */
boolean commit();

翻译

如果是在主线程使用建议使用apply()方法替换。因为commit()方法是同步的。 而apply()方法是异步的

commit()方法源码

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

     
    //向内存中写入数据
    MemoryCommitResult mcr = commitToMemory();

    /* sync write on this thread okay */
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null );
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

看到源码中有一行代码

mcr.writtenToDiskLatch.await();

通过await()暂停主线程直到写入磁盘操作完成。也就是说使用commit()方法提交时,会堵塞主线程。一般不会察觉有什么问题,是因为写入的xml文件比较小。如果文件过大,导致读取时间变长。堵塞主线程时间也就变长。这时候就会导致ANR了。

综上:SharedPreference文件不易过大容易造成ANR。且一般使用apply()方法异步提交

commit()方法因为是同步的,需要让主线程等待。所以xml文件过大时容易造成ANR。官网也建议使用apply异步的方法提交。那么一步的apply就没有问题了吗。下面看一下apply()方法的源码。

apply()方法源码

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    //向内存中写入数据
    final MemoryCommitResult mcr = commitToMemory();

    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {

                }

             }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                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);
}

可以,看出apply()方法。也是首先 向内存中写入数据。也会执行

mcr.writtenToDiskLatch.await();

通过await()暂停主线程直到写入磁盘操作完成。但是区别于commit()方法apply()方法是在一个新的Runnable中(也就是新线程)执行的这行代码。所以相比commit()方法在主线程执行要好的多。

但是毕竟也是有可能堵塞主线程,所以apply()方法也有可能造成ANR。不过相比commit()方法。要好的多。

4.SharedPreference缺点四:getXXX() 导致ANR

源码中也可以看出。SharedPreference 获取内容时的getXXX()方法都是同步的。比如getString()方法。

getString()方法源码

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

getLong()方法源码

@Override
public long getLong(String key, long defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        Long v = (Long)mMap.get(key);
        return v != null ? v : defValue;
    }
}

getFloat()方法源码

@Override
public float getFloat(String key, float defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        Float v = (Float)mMap.get(key);
        return v != null ? v : defValue;
    }
}

可以看出所有的getXXX()方法。大致都是一样的都是在同步方法中执行

awaitLoadedLocked();

然后在mMap中通过Key取不同类型的数值。

awaitLoadedLocked方法源码

@GuardedBy("mLock")
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}
@GuardedBy("mLock")
private boolean mLoaded = false;

此方法中,有一个while循环,如果非mLoaded一直等待。那么mLoaded 字段在什么地方赋值true呢。

上面可知,是在loadFromDisk()方法里。也就是说是在获取SharedPreferences对象时读取完整个xml文件流内容后才会将字段mLoaded变成true然后getXXX()时才不会一直堵塞线程。那么如果xml文件文件过大,导致读取时间边长。这个时候恰巧使用getXXX()方法获取相关内容。这时候mLoaded字段还是false这样就会在while循环中一直等待。造成ANR

四.总结

结合以上基本使用和源码分析,SharedPreferences使用时,应该注意一下几点。

<1> 尽量保证一个xml文件不要过大。

<2> 提交SharedPreferences时,尽量使用apply()异步方法,不推荐使用commit()方法同步提交,尤其在主线程中。

<3> 尽量保证所有的xml文件名和getXXX()的Key 要统一维护。不要出现一个Key在一个地方是Int类型,在一个地方是String类型。

附:FrameWorks源码地址

http://androidxref.com/6.0.0_r1/xref/frameworks/

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SharedPreferencesAndroid平台提供的一种轻量级的数据存储方式,用于存储一些简单的键值对数据。SharedPreferences存储的数据可以被应用程序和其他应用程序(如果获得相应权限)共享,通常用于存储应用程序的配置信息、用户偏好设置等。 SharedPreferences的应用场景包括但不限于以下几个方面: 1. 存储应用程序的配置信息:应用程序的配置信息通常包括一些开关、标志位等,例如是否开启消息推送、是否开启震动等,这些信息可以使用SharedPreferences存储,方便快捷。 2. 存储用户偏好设置:应用程序的用户偏好设置通常包括一些用户习惯、偏好等,例如字体大小、颜色主题等,这些信息可以使用SharedPreferences存储,方便用户在下次打开应用程序时能够保持上一次的设置。 3. 存储应用程序的登录信息:应用程序的登录信息通常包括用户的账号、密码等,这些信息可以使用SharedPreferences存储,方便用户在下次打开应用程序时无需重新登录。 使用SharedPreferences存储数据的具体步骤如下: 1. 获取SharedPreferences对象:可以通过Context的getSharedPreferences()方法或Activity的getPreferences()方法获取SharedPreferences对象。 2. 存储数据:可以通过SharedPreferences.Editor对象的putBoolean()、putInt()、putString()等方法存储数据。 3. 提交数据:必须通过SharedPreferences.Editor对象的commit()或apply()方法提交数据,才能将数据真正地保存到SharedPreferences中。 例如,存储一个Boolean类型的数据可以按照以下代码进行: ``` SharedPreferences sharedPreferences = getSharedPreferences("config", MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putBoolean("is_push_enabled", true); editor.commit(); ``` 以上就是SharedPreferences的应用场景及使用方法的简单介绍。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值