1.SharedPreferences

在Android中,通常需要存储一些数据,一些大型的数据如图片、JSON数据等,可以通过读写File的方式实现;一些大量级的关系型数据,可以通过数据库SQLite实现;而一些简单的、无安全风险的键值对数据,可以通过Android提供的SharedPreferences实现。

SharedPreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。其背后是用xml文件存放数据,文件存放在/data/data//shared_prefs目录下。SharedPreferences所保存的数据会一直存在,除非被覆盖、移除、清空或文件被删除。(SharedPreferences保存的数据会随着应用的卸载而被删除)

SharedPreferences可以保存的数据类型有:int、boolean、float、long、String、StringSet。

SharedPreferences优点:

相对于文件存储来说比较方便,支持多种数据类型的存储。

SharedPreferences缺点:

①不安全,一般只用来存储配置信息

②对数据的操作单一

③存储相同的key值时,存入的数据会被覆盖

2.SharedPreferences使用

①获取到应用中的SharedPreferences

有三种方式。

1)getSharedPreferences(String,mode)

如果需要多个通过名称参数来区分的sharedpreference文件, 名称可以通过第一个参数来指定。可在app中通过任何一个Context 执行该方法。

SharedPreferences sp = context.getSharedPreferences("setting", Context.MODE_PRIVATE);

第一次访问名为"setting"的SharedPreferences文件时,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的xml文件。也就是说该文件不存在时,直接创建;如果已经存在,则直接使用。

mode指定为MODE_PRIVATE,则该配置文件只能被自己的应用程序访问。

2)getPreferences(mode)

这个方法默认使用当前类不带包名的类名作为文件的名称,配置文件仅可以被调用的Activity使用。当Activity只需要创建一个SharedPreferences对象的时候,可以使用该方法。

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

3)PreferenceManager.getDefaultSharedPreferences(Context)

每个应用都有一个默认的配置文件preferences.xml,可以使用getDefaultSharedPreferences获取。

每个应用默认的配置文件的名字是:包名+_preferences,其文件读取类型为Context.MODE_PRIVATE。

看一下它的源码:

public static SharedPreferences getDefaultSharedPreferences(Context context) {

return context.getSharedPreferences( getDefaultSharedPreferencesName(context),getDefaultSharedPreferencesMode());

}

private static String getDefaultSharedPreferencesName(Context context) {

return context.getPackageName() + "_preferences";

}

private static int getDefaultSharedPreferencesMode() {

return Context.MODE_PRIVATE;

}

以上三种获取SharedPreferences的方法都提到了mode,来看一下关于mode的指定:

1)私有模式​ Context.MODE_PRIVATE

​ 只能被创建这个文件的当前应用访问。若文件不存在会创建文件;若创建的文件已存在则会覆盖掉原来的文件。

2)追加模式​ Context.MODE_APPEND

​ 只能被创建这个文件的当前应用访问。若文件不存在会创建文件;若文件存在则在文件的末尾进行追加内容。

3)可读模式​ Context.MODE_WORLD_READABLE

​ 创建出来的文件可以被其他应用所读取

4)可写模式​ Context.MODE_WORLD_WRITEABLE

​ 允许其他应用对其进行写入。

②读写SharedPreferences

1)写SharedPreferences

为了写sharedPreferences文件,需要通过执行edit()创建一个 SharedPreferences.Editor。通过putXXX()方法传递keys与values,最后通过commit() 或apply()提交改变。

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

SharedPreferences.Editor editor = sharedPref.edit();

editor.putString("number",number);

editor.putString("password",pwd);

editor.apply(); 或 editor.commit();

commit表示同步提交到SharedPreferences文件,获取是否同步成功的结果:boolean success = editor.commit();

apply表示异步提交到SharedPreferences文件:editor.apply();

2)读SharedPreferences

通过getXXX()方法从sharedPreferences中读取数据。在这些方法里面传入想要获取的value对应的key,并提供一个默认的value作为查找的key不存在时函数的返回值。如下:

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

String number=sp.getString("number","");

String pasword=sp.getString("password","");

③移除数据

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

abstract SharedPreferences.Editor remove(String key)

参数key:指定数据的key

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

abstract SharedPreferences.Editor clear()

④系统默认的SharedPreferences

每个应用都有一个默认编好的preferences.xml文件,使用getDefaultSharedPreferences获取,其余操作是一样的。

SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

SharedPreferences.Editor editor = preferences.edit();

editor.putBoolean("if_set_location", false);

editor.commit();

⑤访问其他应用的SharedPreferences

如果应用B要读写访问A应用中的Preference前提条件是,A应用中该preference创建时指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE权限,代表其他的应用能访问读取或者写入。

具体步骤:

在B中创建一个指向A应用的Context:

Context otherAppsContext = createPackageContext("A应用的包名", Context.CONTEXT_IGNORE_SECURITY);

然后通过context获取到SharedPreferences实体:

SharedPreferences sharedPreferences = otherAppsContext.getSharedPreferences("SharedPreferences的文件名", Context.MODE_WORLD_READABLE);

String name = sharedPreferences.getString("key", "");

注:如果不通过创建Context访问其他应用的preference,也可以以读取xml文件方式直接访问其他应用preference对应的xml文件,如:

File xmlFile = new File(“/data/data/<package name>/shared_prefs/itcast.xml”);

//<package name>应替换成应用的包名。

3.SharedPreferences变化监听

这是当SharedPreferences改变时的回调,是SharedPreferences的一个接口。

通过registerOnSharedpreferenceListener方法设置监听:

SharedPreferences sp = getSharedPreferences( "testSP", Context.MODE_PRIVATE);

sp. registerOnSharedPreferenceChangeListener( new SharedPreferences. OnSharedPreferenceChangeListener() {

@Override

public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String s) {

Log.i("spTest","sp changed, key is "+ s);

}

});

关于这个监听,官方文档是这样描述的:

Called when a shared preference is changed, added, or removed.

This may be called even if a preference is set to its existing value.

This callback will be run on your main thread.

使用这个监听时,如果你用匿名对象也就是下面这样,可能会被当作垃圾回收,导致会回调一次你的callback,达不到监听的效果。

//匿名回调

protected void onCreate(Bundle savedInstanceState)  {

SharedPreferences sp = getSharedPreferences( "testSP", Context.MODE_PRIVATE);

sp.registerOnSharedPreferenceChangeListe ner(new OnSharedPreferenceChangeListener() {

@Override

public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) {

Log.i(LOGTAG, "testOnSharedPreference ChangedWrong key =" + key);

}

});

}

这种写法看上去没有什么问题,而且很多时候开始几次onSharedPreferenceChanged方法也可以被调用。但是过一段时间(简单demo 不容易出现,但是使用DDMS中的gc会立刻导致接下来的问题),你会发现前面的方法突然不再被调用,进而影响到程序的处理。

原因剖析:

真正的原因就是注册的监听器被移除掉了。

首先先了解一下registerOnSharedPreferenceChangeListener注册的实现。

private final WeakHashMap<OnSharedPreferenc eChangeListener, Object> mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {

synchronized(this) {

mListeners.put(listener, mContent);

}

}

从上面的代码可以得知,一个OnSharedPreferenceChangeListener对象实际上是放到了一个WeakHashMap的容器中,执行完示例中的onCreate方法,这个监听器对象很快就会成为垃圾回收的目标,由于放在WeakHashMap中作为key不会阻止垃圾回收, 所以当监听器对象被回收之后,这个监听器也会从mListeners中移除。所以就造成了onSharedPreferenceChanged不会被调用。

解决办法:改为对象成员变量(推荐)

将监听器作为Activity的一个成员变量,在Activity的onResume进行注册,在onPause时进行注销。推荐在这两个Activity生命周期中进行处理,尤其是当SharedPreference值发生变化后,对Activity展示的UI进行处理操作的情况。这种方法是最推荐的解决方案。

private OnSharedPreferenceChangeListener mListener = new OnSharedPreferenceChangeL istener() {

@Override

public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) {

Log.i(LOGTAG, "instance variable key=" + key);

}

};

@Override

protected void onResume() {

sp.registerOnSharedPreferenceChangeListener(mListener);

super.onResume();

}

@Override

protected void onPause() {

sp.unregisterOnSharedPreferenceChangeList ener(mListener);

super.onPause();

}

总结一下:

①仅当添加或更改值时,监听器才会触发,设置相同的值将不会调用它;

②监听器需要保存在成员变量中,而不是匿名类,因为registerOnSharedPreferenceChangeListener使用弱引用进行存储,因此将被垃圾回收;

③除了使用成员变量,也可以由类直接实现,然后调用 registerOnSharedPreferenceChangeListener(this);

④当不再需要使用时,请记住注销该侦听器unregisterOnSharedPreferenceChangeListener。

4.实现原理

①创建SharedPreferences

通过Context的getSharedPreferences()方法获取一个SharedPreferences对象,而Context中的getSharedPreferences方法是abstract抽象方法,它的实际逻辑载体在ContextImpl类里,所以来看ContextImpl的该方法。

ContextImpl.java:

public SharedPreferences getSharedPreferences(String name, int mode) {

File file;

synchronized (ContextImpl.class) {

if (mSharedPrefsPaths == null) {

mSharedPrefsPaths = new ArrayMap<>();

}

//先从缓存mSharedPrefsPaths中查找sp文件是否存在

file = mSharedPrefsPaths.get(name);

//如果缓存中不存在,则新建sp文件,文件名为name.xml

if (file == null) {

file = getSharedPreferencesPath(name);

mSharedPrefsPaths.put(name, file);

}

}

//获取File对象对应的SharedPreferences对象

return getSharedPreferences(file, mode);

}

这里出现了一个变量mSharedPrefsPaths,找一下它的定义:

@GuardedBy("ContextImpl.class")

private ArrayMap<String, File> mSharedPrefsPaths; //文件名为key,具体文件为value。mSharedPrefsPaths存储所有sp文件,由ContextImpl.class锁保护

可见,mSharedPrefsPaths是一个ArrayMap ,缓存了文件名和sp文件的对应关系。首先会根据参数中的文件名name查找缓存中是否存在对应的sp文件。如果不存在,会新建名称为 [name].xml 的文件,并存入缓存 mSharedPrefsPaths 中。最后会调用另一个重载的getSharedPreferences()方法,参数是File 。

public File getSharedPreferencesPath(String name) {

return makeFilename(getPreferencesDir(), name + ".xml");

}

private File getPreferencesDir() {

synchronized (mSync) {

if (mPreferencesDir == null) {

///data/data/packageName/目录

mPreferencesDir = new File(getDataDir(), "shared_prefs");

}

return ensurePrivateDirExists( mPreferencesDir);

}

}

private static File ensurePrivateDirExists(File file){

return ensurePrivateDirExists(file,0771,-1,null);

}

private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {

if (!file.exists()) {

final String path = file.getAbsolutePath();

try {

Os.mkdir(path, mode);

...

}

}

return file;

}

如果应用目录下还没有shared_prefs文件夹,则创建一个该文件夹。

public File getSharedPreferencesPath(String name) {

return makeFilename(getPreferencesDir(), name + ".xml");

}

private File makeFilename(File base, String name) {

if (name.indexOf(File.separatorChar) < 0) {

final File result = new File(base, name);

return res;

}

}

在shared_prefs文件夹下,创建一个指定名字的xml文件,用来存储键值对数据。

至此,生成了一个相应SharedPreferences的File文件,并进行缓存。

生成File对象后,接下来通过File对象获取SharedPreferences对象。

ContextImpl.java:

public SharedPreferences getSharedPreferences(File file, int mode) {

SharedPreferencesImpl sp;

synchronized (ContextImpl.class) {

final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();

sp = cache.get(file); //先从缓存中尝试获取sp

if(sp == null) { //如果缓存中获取失败

checkMode(mode);//检查mode

sp = new SharedPreferencesImpl(file, mode);

cache.put(file, sp);

return sp;

}

}

//mode为MODE_MULTI_PROCESS时,由于文件可能被其他进程修改,则重新加载。该模式下每次获取SharedPreferences实例的时候都会尝试从磁盘中加载修改过的数据,并且读取是在异步线程中,因此一个线程的修改最终会反映到另一个线程,但不能立即反映到另一个进程,所以通过SharedPreferences无法实现多进程同步。如果仅仅让多进程可访问同一个SharedPref文件,不需要设置MODE_MULTI_PROCESS,,如果需要实现多进程同步,必须设置这个参数,但也只能实现最终一致,无法即时同步。显然这并不足以保证跨进程安全。

if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {

sp.startReloadIfChangedUnexpectedly();

}

return sp;

}

SharedPreferences只是接口而已,我们要获取的实际上是它的实现类SharedPreferencesImpl 。通过getSharedPreferencesCacheLocked()方法可以获取已经缓存的SharedPreferencesImpl对象和其sp文件。返回的SharedPreferences实际对象是一个SharedPreferencesImpl对象实例,并且也通过一个ArrayMap做了一个缓存,也就是说,一个name会对应一个SharedPreferences的File实例,而一个File会对应一个SharedPreferencesImpl实例。

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {

if (sSharedPrefsCache == null) {

sSharedPrefsCache = new ArrayMap<>();

}

final String packageName = getPackageName();

ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get( packageName);

if (packagePrefs == null) {

packagePrefs = new ArrayMap<>();

sSharedPrefsCache.put(packageName, packagePrefs);

}

return packagePrefs;

}

sSharedPrefsCache是一个嵌套的ArrayMap,其定义如下:

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

以包名为key,以一个存储了sp文件及其SharedPreferencesImp对象的ArrayMap为 value。如果存在直接返回,反之创建一个新的ArrayMap作为值并存入缓存。

private void checkMode(int mode) {

// 从N开始,如果使用 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,直接抛出异常

if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {

if((mode & MODE_WORLD_READABLE) != 0){

throw new SecurityException( "MODE_WORLD_READABLE no longer supported");

}

if((mode & MODE_WORLD_WRITEABLE) != 0){

throw new SecurityException( "MODE_WORLD_WRITEABLE no longer supported");

}

}

}

从Android N开始,明确不再支持 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,再加上 MODE_MULTI_PROCESS 并不能保证线程安全,一般就使用 MODE_PRIVATE 就可以了。

从源码中可以看到,如果缓存中没有对应的SharedPreferencesImpl对象,就得自己创建了。看一下它的构造函数:

SharedPreferencesImpl(File file, int mode) {
    mFile = file; // sp 文件
    mBackupFile = makeBackupFile(file); // 创建备份文件
    mMode = mode; 
    mLoaded = false; // 标识sp文件是否已经加载到内存
    mMap = null; // 存储sp文件中的键值对
    mThrowable = null;
    startLoadFromDisk(); // 加载数据
}
注意这里的mMap,它是一个 Map<String, Object>,存储了sp文件中的所有键值对。所以 SharedPreferences文件的所有数据都是存在于内存中的,既然存在于内存中,就注定它不适合存储大量数据。

private void startLoadFromDisk() {

synchronized (mLock) {

mLoaded = false;

}

//工作线程中进行

new Thread("SharedPreferencesImpl-load") {

public void run() {

loadFromDisk(); // 异步加载

}

}.start();

}

private void loadFromDisk() {

synchronized (mLock) { // 获取 mLock 锁

if (mLoaded) { // 已经加载进内存,直接返回,不再读取文件

return;

}

if (mBackupFile.exists()) { // 如果存在备份文件,直接将备份文件重命名为 sp 文件

mFile.delete();

mBackupFile.renameTo(mFile);

}

}

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 { // 读取 sp 文件

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);//解析XML生成Map

} catch (Exception e) {

Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);

} finally {

IoUtils.closeQuietly(str);

}

}

} catch (ErrnoException e) {

} catch (Throwable t) {

thrown = t;

}

synchronized (mLock) {

mLoaded = true;

mThrowable = thrown;

try {

if (thrown == null) {

if (map != null) {

mMap = map;

mStatTimestamp = stat.st_mtim; // 更新修改时间

mStatSize = stat.st_size; // 更新文件大小

} else {

mMap = new HashMap<>();

}

}

} catch (Throwable t) {

mThrowable = t;

} finally {

mLock.notifyAll(); // 唤醒处于等待状态的线程

}

}

}

可以看到,SharedPreferencesImpl创建的时候,简单粗暴的开启了一个工作线程,进行File的读取和解析,并生成了Map对象,赋值给mMap变量。

简单看一下流程:

(1)判断是否已经加载进内存

(2)判断是否存在遗留的备份文件,如果存在,重命名为 sp 文件

(3)读取 sp 文件,并存入内存

(4)更新文件信息

(5)释放锁,唤醒处于等待状态的线程

loadFromDisk() 是异步执行的,而且是线程安全的,读取过程中持有锁 mLock,看起来设计的都很合理,但是在不合理的使用情况下就会出现问题。

别忘了此时还停留在getSharedPreferences()方法,也就是获取SharedPreferences的过程中。如果在使用过程中,调用getSharedPreferences()之后,直接调用 getXXX() 方法来获取数据,恰好sp文件数据量又比较大,读取过程比较耗时,那么getXXX() 方法就会被阻塞。后面看到getXXX()方法的源码时,就会看到它需要等待sp文件加载完成,否则就会阻塞。所以在使用过程中,可以提前异步初始化SharedPreferences对象,加载sp文件进内存,避免发生潜在可能的卡顿。这是 SharedPreferences 的一个槽点,也是使用过程中需要注意的。

2.读取sp数据

获取sp文件中的数据使用的是SharedPreferencesImpl中的七个getXXX方法。它们都是一样的逻辑,以getInt()为例看一下源码:

public int getInt(String key, int defValue) {

synchronized (mLock) {

awaitLoadedLocked(); //sp文件尚未加载完成时,会阻塞在这里

Integer v = (Integer)mMap.get(key); // 加载完成后直接从内存中读取

return v != null ? v : defValue;

}

}

一旦sp文件加载完成,所有获取数据的操作都是从内存中读取的。这样的确提升了效率,但是很显然将大量的数据直接放在内存是不合适的,所以注定SharedPreferences不适合存储大量数据。

当调用getXXX()等操作Map的方法时,会通过awaitLoadedLocked()方法判断是否map已经生成,如果没有则等待。

@GuardedBy("mLock")

private void awaitLoadedLocked() {

if (!mLoaded) {

BlockGuard.getThreadPolicy(). onReadFromDisk();

}

while (!mLoaded) { //sp文件未加载完成时, 等待

try {

mLock.wait();

} catch (InterruptedException unused) {

}

}

if (mThrowable != null) {

throw new IllegalStateException( mThrowable);

}

}

mLoaded初始值为false,在loadFromDisk()方法中读取 sp 文件之后会被置为 true,并调用mLock.notifyAll()通知等待的线程。

③存储sp数据

SharedPreferences存储数据时,需要使用edit()方法,该方法会返回一个Editor()对象Editor和SharedPreferences一样都只是接口,它们的实现类分别是EditorImpl和 SharedPreferencesImpl。

SharedPreferencesImpl.java:

public Editor edit() {

synchronized (mLock) {

awaitLoadedLocked();//等待sp文件加载完成

}

return new EditorImpl();

}

可见,edit()方法同样需要等待sp文件加载完成,然后再进行EditImpl()的初始化。每次调用edit()方法都会实例化一个新的EditorImpl对象。所以在使用的时候要注意不要每次 put() 都去调用edit()方法,在封装SharedPreferences工具类的时候可能会犯这个错误。

接下来看看EditorImpl实现类:

SharedPreferencesImpl.java:

public final class EditorImpl implements Editor {

private final Object mEditorLock = new Object();

@GuardedBy("mEditorLock")

private final Map<String, Object> mModified = new HashMap<>(); // 存储要修改的数据

@GuardedBy("mEditorLock")

private boolean mClear = false; // 清除标记

@override

public Editor putString(String key, String value){

synchronized (mEditorLock) {

mModified.put(key, value);

return this;

}

}

@override

public Editor remove(String key) {

synchronized (mEditorLock) {

mModified.put(key, this);

return this;

}

}

@Override

public Editor clear() {

synchronized (mEditorLock) {

mClear = true;

return this;

}

}

@Override

public boolean commit() { }

@Override

public boolean apply() { }

}

EditorImpl有两个成员变量 : mModified, mClear。mModified 是一个HashMap,存储了所有通过putXXX()方法添加的需要添加或者修改的键值对。mClear是清除标记,在clear()方法中会被置为 true。

所有的putXXX()方法都只是改变了mModified集合,当调用commit()或者apply()时才会去修改sp文件。下面分别看一下这两个方法。

④commit()

SharedPreferencesImpl.java:

@override

public boolean commit() {

long startTime = 0;

if (DEBUG) {

startTime = System.currentTimeMillis();

}

// 先将 mModified 同步到内存

MemoryCommitResult mcr = commitToMemory();

// 再将内存数据同步到文件

SharedPreferencesImpl.this. enqueueDiskWrite(mcr, null );

try {

mcr.writtenToDiskLatch.await(); // 等待写入操作完成

} catch (InterruptedException e) {

return false;

} finally {

if (DEBUG) {

Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms");

}

}

notifyListeners(mcr); // 通知监听者,回调OnSharedPreferenceChangeListener

return mcr.writeToDiskResult; // 返回写入操作结果

}

commit()的大致流程是:

(1)首先调用commitToMemory()方法同步mModified到内存中

(2)然后调用enqueueDiskWrite()方法同步内存数据到sp文件中

(3)等待写入操作完成,并通知监听者

内存同步是commitToMemory()方法,写入文件是enqueueDiskWrite()方法。来详细看一下这两个方法。

private MemoryCommitResult commitToMemory() {

long memoryStateGeneration;

List<String> keysModified = null;

Set<OnSharedPreferenceChangeListener> listeners = null;

Map<String, Object> mapToWriteToDisk;

synchronized (SharedPreferencesImpl.this. mLock) {

// 在commit()写入本地文件过程中,会将mDiskWritesInFlight置为 1。写入过程尚未完成时,又调用了commitToMemory(),直接修改 mMap可能会影响写入结果,所以这里要对 mMap 进行一次深拷贝

if (mDiskWritesInFlight > 0) {

mMap = new HashMap<String, Object>(mMap);

}

mapToWriteToDisk = mMap;

mDiskWritesInFlight++;

boolean hasListeners = mListeners.size() > 0;

if (hasListeners) {

keysModified = new ArrayList<String>();

listeners = new HashSet<OnSharedPre ferenceChangeListener>(mListeners.keySet());

}

synchronized (mEditorLock) {

boolean changesMade = false;

if (mClear) {

if (!mapToWriteToDisk.isEmpty()) {

changesMade = true;

mapToWriteToDisk.clear();

}

mClear = false;

}

for (Map.Entry<String, Object> e : mModified.entrySet()) {

String k = e.getKey();

Object v = e.getValue();

// v == this 和 v == null 都表示删除此 key

if(v == this || v == null) {

if(!mapToWriteToDisk.containsKey(k)){

continue;

}

mapToWriteToDisk.remove(k);

} else {

if(mapToWriteToDisk.containsKey(k)){

Object existingValue = mapToWriteToDisk.get(k);

if (existingValue != null && existingValue.equals(v)) {

continue;

}

}

mapToWriteToDisk.put(k, v);

}

changesMade = true;

if (hasListeners) {

keysModified.add(k);

}

}

mModified.clear();

if (changesMade) {

mCurrentMemoryStateGeneration++;

}

memoryStateGeneration = mCurrentMemoryStateGeneration;

}

}

return new MemoryCommitResult( memoryStateGeneration, keysModified, listeners, mapToWriteToDisk);

}

简单说,commitToMemory()方法会将所有需要改动的数据mModified和原sp文件数据mMap进行合并生成一个新的数据集合mapToWriteToDisk,从名字也可以看出来,这就是之后要写入文件的数据集。没错,SharedPreferences的写入都是全量写入,即使你只改动了其中一个配置项,也会重新写入所有数据。针对这一点,可以做的优化是,将需要频繁改动的配置项使用单独的sp文件进行存储,避免每次都要全量写入。

接下来是enqueueDiskWrite方法:

SharedPreferencesImpl.java:

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {

//commit()方法时为true

final boolean isFromSyncCommit = (postWriteRunnable == null);

final Runnable writeToDiskRunnable = new Runnable() {

@Override

public void run() {

synchronized (mWritingToDiskLock) {

writeToFile(mcr, isFromSyncCommit);

}

synchronized (mLock) {

//当前更新操作的线程数-1

mDiskWritesInFlight--;

}

if (postWriteRunnable != null) {

postWriteRunnable.run();

}

}

};

// commit()直接在当前线程进行写入操作

if (isFromSyncCommit) {

boolean wasEmpty = false;

synchronized (mLock) {

//只有当前线程调用时,因为同步进行,所以一直为1;有多个线程同时调用时,会大于1

wasEmpty = mDiskWritesInFlight == 1;

}

if (wasEmpty) {

writeToDiskRunnable.run();//当前线程直接调用

return;

}

}

//  //异步调用,apply()方法执行此处,由 QueuedWork.QueuedWorkHandler处理

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

}

当只有一个线程操作SharedPreferences的话,mDiskWritesInFlight计数器始终为1,因为是同步写入File,写入后计数器会-1。

回头先看一下commit()方法中是如何调用 enqueueDiskWrite() 方法的:

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);

第二个参数postWriteRunnable是null,所以 isFromSyncCommit为true,会执行上面的if代码块,而不执行QueuedWork.queue()。由此可见,commit()方法最后的写文件操作是直接在当前调用线程执行的,如果在主线程调用该方法,就会直接在主线程进行 IO 操作。显然,这是不建议的,可能造成卡顿或者 ANR。在实际使用中应该尽量使用apply()方法来提交数据。当然,apply()也并不是十全十美的,后面会提到。

commit()方法的最后一步了,将mapToWriteToDisk写入sp文件。而写入文件就是很简单的IO操作,只不过需要把Map转换为xml的格式。

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {

boolean fileExists = mFile.exists();

// Rename the current file so it may be used as a backup during the next read

if (fileExists) {

boolean needsWrite = false;

// 仅当磁盘状态比当前提交旧时才需要写入文件

if (mDiskStateGeneration < mcr.memoryStateGeneration) {

if (isFromSyncCommit) {

needsWrite = true;

} else {

synchronized (mLock) {

if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {

needsWrite = true;

}

}

}

}

if (!needsWrite) { // 无需写入,直接返回

mcr.setDiskWriteResult(false, true);

return;

}

boolean backupFileExists = mBackupFile.exists(); // 备份文件是否存在

// 如果备份文件不存在,将 mFile 重命名为备份文件,供以后遇到异常时使用

if (!backupFileExists) {

if (!mFile.renameTo(mBackupFile)) {

Log.e(TAG, "Couldn't rename file " + mFile  + " to backup file " + mBackupFile);

mcr.setDiskWriteResult(false, false);

return;

}

} else {

mFile.delete();

}

}

try {

FileOutputStream str = createFileOutputStream(mFile);

if (str == null) {

mcr.setDiskWriteResult(false, false);

return;

}

XmlUtils.writeMapXml( mcr.mapToWriteToDisk, str); // 全量写入,将map数据写成xml格式到file中

writeTime = System.currentTimeMillis();

FileUtils.sync(str);

str.close();

ContextImpl.setFilePermissionsFromMode( mFile.getPath(), mMode, 0);

try {

final StructStat stat = Os.stat(mFile.getPath());

synchronized (mLock) {

mStatTimestamp = stat.st_mtim; // 更新文件时间

mStatSize = stat.st_size; // 更新文件大小

}

} catch (ErrnoException e) {

}

// 写入成功,删除备份文件

mBackupFile.delete();

mDiskStateGeneration = mcr.memoryStateGeneration;

// 返回写入成功,唤醒等待线程

mcr.setDiskWriteResult(true, true);

long fsyncDuration = fsyncTime - writeTime;

mSyncTimes.add((int) fsyncDuration);

mNumSync++;

return;

} catch (XmlPullParserException e) {

Log.w(TAG, "writeToFile: Got exception:", e);

} catch (IOException e) {

Log.w(TAG, "writeToFile: Got exception:", e);

}

// 清除未成功写入的文件

if (mFile.exists()) {

if (!mFile.delete()) {

Log.e(TAG, "Couldn't clean up partially-written file " + mFile);

}

}

mcr.setDiskWriteResult(false, false); // 返回写入失败

}

⑤apply()

SharedPreferencesImpl.java:

@Override

public void apply() {

final long startTime = System.currentTimeMillis();

// 先将mModified同步到内存

final MemoryCommitResult mcr = commitToMemory();

final Runnable awaitCommit =new Runnable() {

@Override

public void run() {

try {

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 postWriteRunnable=new Runnable(){

@Override

public void run() {

awaitCommit.run();

QueuedWork.removeFinisher( awaitCommit);

}

};

SharedPreferencesImpl.this. enqueueDiskWrite(mcr, postWriteRunnable);

notifyListeners(mcr);

}

同样也是先调用commitToMemory()同步到内存,再调用enqueueDiskWrite()同步到文件。和commit()不同的是,enqueueDiskWrite()方法的 Runnable 参数不再是null了,传进来一个postWriteRunnable,不为null时,走的方法就是异步方法。所以其内部的执行逻辑和 commit() 方法是完全不同的。commit() 方法会直接在当前线程执行 writeToDiskRunnable(),而 apply() 会由QueuedWork来处理:

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

queue()方法的源码为:

Queue work.java:

public static void queue(Runnable work, boolean shouldDelay) {

Handler handler = getHandler();

synchronized (sLock) {

//将任务加入到队列中等待执行

sWork.add(work);

//通过handler发送消息

if (shouldDelay && sCanDelay) {

handler.sendEmptyMessageDelayed( QueuedWorkHandler.MSG_RUN, DELAY);

} else {

handler.sendEmptyMessage( QueuedWorkHandler.MSG_RUN);

}

}

}

这里的handler是一个全局的HandlerThread对象,handler所在的线程就是执行Runnable的线程了,也就是一个工作线程,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作,任务还是一样的writeFile()方法。看一下 getHandler 源码:

QueuedWork.java:

private static Handler getHandler() {

synchronized (sLock) {

if (sHandler == null) {

HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND);

handlerThread.start();

sHandler = new QueuedWorkHandler( handlerThread.getLooper());

}

return sHandler;

}

}

private static class QueuedWorkHandler extends Handler {

...

public void handleMessage(Message msg) {

if (msg.what == MSG_RUN) {

processPendingWork();

}

}

}

private static void processPendingWork() {

...

for (Runnable w : work) {

//执行任务

w.run();

}

}

可以看到,此时写sp文件的操作会异步执行在一个单独的线程上。

QueuedWork 除了执行异步操作之外,还有一个作用。它可以确保当 Activity onPause()/onStop() 之后,或者 BroadCast onReceive() 之后,异步任务可以执行完成。以 ActivityThread.java 中 handlePauseActivity() 方法为例:

@Override

public void handleStopActivity(IBinder token, boolean show, int configChanges, PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {

final ActivityClientRecord r = mActivities.get(token);

r.activity.mConfigChangeFlags |= configChanges;

final StopInfo stopInfo = new StopInfo();

performStopActivityInner(r, stopInfo, show, true, finalStateRequest, reason);

updateVisibility(r, show);

// 可能因等待写入造成卡顿甚至 ANR

if (!r.isPreHoneycomb()) {

QueuedWork.waitToFinish();

}

stopInfo.setActivity(r);

stopInfo.setState(r.state);

stopInfo.setPersistentState(r.persistentState);

pendingActions.setStopInfo(stopInfo);

mSomeActivitiesChanged = true;

}

初衷可能是好的,但是我们都知道在 Activity() 的 onPause()/onStop() 中不应该进行耗时任务。如果 sp 数据量很大的话,这里无疑会出现性能问题,可能造成卡顿甚至 ANR。

5.总结

①SharedPreferences的File创建和内容解析,在内存中是有缓存的

②SharedPreferences的提交,commit()方法是在当前线程完成,而apply()方法在全局唯一的一个工作线程中完成

③所有的文件和内存读写操作,都通过锁对象进行加锁,保证了多线程同步

④ShredPreferences是单例对象,第一次打开后,之后获取都无需创建,速度很快。

当第一次获取数据后,数据会被加载到一个缓存的Map中,之后的读取都会非常快。

当由于是XML<->Map的存储方式,所以,数据越大,操作越慢,get、commit、apply、remove、clear都会受影响,所以尽量把数据按功能拆分成若干份。

⑤同时执行这两句代码的时候,第一行代码所写的内容会被第二行代码取代。
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);

⑥执行以下代码会出现异常。
(指定key所保存的类型和读取时的类型不同)
editor.putInt("age", 32);//保存为int类型
String age = userInfo.getString("age", "null");//读取时为String类型,出现异常

⑦在这些动作之后,记得commit
editor.putInt("age", 20);//写入操作
editor.remove("age");   //移除操作
editor.clear();     //清空操作
editor.commit();//记得commit

同时,SharedPreferences的槽点也不少:

①不支持跨进程,MODE_MULTI_PROCESS 也没用。跨进程频繁读写可能导致数据损坏或丢失。

②初始化的时候会读取sp文件,可能导致后续 getXXX() 方法阻塞。建议提前异步初始化 SharedPreferences。

③sp文件的数据会全部保存在内存中,所以不宜存放大数据。

④edit()方法每次都会新建一个EditorImpl对象。建议一次 edit(),多次 putXXX() 。

⑤无论是commit()还是apply() ,针对任何修改都是全量写入。建议针对高频修改的配置项存在单独的sp文件中。

⑥commit()同步保存,有返回值。apply()异步保存,无返回值。按需取用。

⑦onPause() 、onReceive() 等时机会等待异步写操作执行完成,可能造成卡顿或者 ANR。

如果不需要跨进程,仅仅存储少量的配置项,SharedPreferences 仍然是一个很好的选择。

6.SharedPreferences缺点

google对SP的定义为轻量级存储,如果存储的数据少,使用起来没有任何问题,当需要存储数据比较多时,SP可能会导致以下问题:

①SP第一次加载数据时需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿

②SP读写文件不是类型安全的,且没有发出错误信号的机制,缺少事务性API

③commit() / apply()操作可能会造成ANR问题:

commit()是同步提交,会在UI主线程中直接执行IO操作,当写入操作耗时比较长时就会导致UI线程被阻塞,进而产生ANR;apply()虽然是异步提交,但异步写入磁盘时,如果执行了Activity / Service中的onStop()方法,那么一样会同步等待SP写入完毕,等待时间过长时也会引起ANR问题。针对apply()展开来看一下:

SharedPreferencesImpl#EditorImpl.java中最终执行了apply()函数:

public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

public void apply() {

final MemoryCommitResult mcr = commitToMemory();

final Runnable awaitCommit = new Runnable(){

public void run() {

try {

mcr.writtenToDiskLatch.await();

} catch (InterruptedException ignored) {

}

}

};

//8.0之前

QueuedWork.add(awaitCommit);

//8.0之后

QueuedWork.addFinisher(awaitCommit);

//异步执行磁盘写入操作  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

//......

}

构造一个名为awaitCommit的Runnable任务并将其加入到QueuedWork中,该任务内部直接调用了CountDownLatch.await()方法,即直接在UI线程执行等待操作,那么需要看QueuedWork中何时执行这个任务。

QueuedWork类在Android8.0以上和8.0以下的版本实现方式有区别:

8.0之前QueuedWork.java:

public class QueuedWork {

private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>();

public static void add(Runnable finisher) {

sPendingWorkFinishers.add(finisher);

}

public static void waitToFinish() {

Runnable toFinish;

// 从队列中取出任务:如果任务为空,则跳出循环,UI线程可以继续往下执行;反之任务不为空,取出任务并执行,实际执行的CountDownLatch.await(),即UI线程会阻塞等待

while ((toFinish = sPendingWorkFinishers.poll()) != null) {

toFinish.run();

}

}

//......

}

8.0之后QueuedWork.java:

public class QueuedWork {

private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

public static void waitToFinish() {

Handler handler = getHandler();

StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();

try {

//8.0之后优化,会主动尝试执行写磁盘任务

processPendingWork();

} finally {

StrictMode.setThreadPolicy(oldPolicy);

}

try {

while (true) {

Runnable finisher;

synchronized (sLock) {

//从队列中取出任务

finisher = sFinishers.poll();

}

//如果任务为空,则跳出循环,UI线程可以继续往下执行

if (finisher == null) {

break;

}

//任务不为空,执行CountDownLatch.await(),即UI线程会阻塞等待

finisher.run();

}

} finally {

sCanDelay = true;

}

}

}

可以看到不管8.0之前还是之后,waitToFinish()都会尝试从Runnable任务队列中取任务,如果有的话直接取出并执行,直接看哪里调用了waitToFinish():

ActivityThread.java

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {

//......

QueuedWork.waitToFinish();

}

private void handleStopService(IBinder token) {

//......

QueuedWork.waitToFinish();

}

省略了一些代码细节,可以看到在ActivityThread中handleStopActivity、handleStopService方法中都会调用waitToFinish()方法,即在Activity的onStop()中、Service的onStop()中都会先同步等待写入任务完成才会继续执行。

所以apply()虽然是异步写入磁盘,但是如果此时执行到Activity/Service的onStop(),依然可能会阻塞UI线程导致ANR。

Android SharedPreferences★★★★相关推荐

  1. android SharedPreferences数据存储

    android  SharedPreferences数据存储 很多时候我们开发的软件需要向用户提供软件参数设置功能,例如我们常用的QQ,用户可以设置是否允许陌生人添加自己为好友.对于软件配置参数的保存 ...

  2. android sharedpre,Android SharedPreferences四种操作模式使用详解_Android_脚本之家

    Android  SharedPreferences详解 获取SharedPreferences的两种方式: 1 调用Context对象的getSharedPreferences()方法 2 调用Ac ...

  3. android sharedpreferences工具类

    今天,简单讲讲如何写一个sharedpreferences的工具类. 很简单,把一些重复的操作封装在工具类里,其他地方调用就可以.在网上搜索了比较多的资料,找到一个比较好的工具类. 参考文章:http ...

  4. android SharedPreferences的使用优化

    今天,简单讲讲android如何优化使用SharedPreferences保存数据. 之前,我写代码是都是每次缓存一个键值对就commit一次,后来在完成看了看,发现应该是缓存完所有键值对后,再一次性 ...

  5. android SharedPreferences保存list数据

    今天,简单讲讲如何使用  SharedPreferences保存list数据. 网上找了很多资料,还是觉得这种比较简单.直接上代码: 保存: public static boolean saveA ...

  6. android sharedpreferences 工具类,android sharedpreferences工具类

    释放双眼,带上耳机,听听看~! 今天,简单讲讲如何写一个sharedpreferences的工具类. 很简单,把一些重复的操作封装在工具类里,其他地方调用就可以.在网上搜索了比较多的资料,找到一个比较 ...

  7. android 同步list数据,android SharedPreferences保存list数据

    释放双眼,带上耳机,听听看~! 今天,简单讲讲如何使用SharedPreferences保存list数据. 网上找了很多资料,还是觉得这种比较简单.直接上代码: 保存: public static b ...

  8. android SharedPreferences 存储对象

    原文地址为: android SharedPreferences 存储对象 我们知道SharedPreferences只能保存简单类型的数据,例如,String.int等. 如果想用SharedPre ...

  9. android: SharedPreferences存储

    不同于文件的存储方式,SharedPreferences 是使用键值对的方式来存储数据的.也就是 说当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可 以通过这个键把相应的 ...

  10. 大佬教你极简方法来处理Android SharedPreferences设计与实现

    起源 就在前几日,有幸拜读到 HiDhl 的文章,继腾讯开源类似功能的MMKV之后,Google官方维护的 Jetpack DataStore 组件横空出世--这是否意味着无论是腾讯三方还是Googl ...

最新文章

  1. 一文理解Ranking Loss/Margin Loss/Triplet Loss
  2. 符号说明表怎么做_教会你的孩子正确使用标点符号
  3. 计算机组成原理数据冒险的解决nop,计算机组成原理实验讲义(103页)-原创力文档...
  4. 论文翻译——FingerSound:Recognizing unistroke thumb gestures using a ring
  5. 负载均衡实现的几种方式
  6. Android 系统性能优化(30)---Android性能全面分析与优化方案研究
  7. openeuler安装mysql_基于鲲鹏底座openEuler系统部署web站点(java+mysql+tomcat)实践
  8. 国外图片分享网站有哪些?20个国外免费、高清图片素材网站、图库全合集
  9. ROI和widthStep
  10. 基础命令和脚本练习初识
  11. ubuntu18.04换源(阿里无脑版)
  12. 腾讯云服务器Ubuntu系统如何使用 root 用户远程登录
  13. 数据库原理-SQL Server版(期末复习)
  14. RPA财务软件对社会的影响
  15. 用微信小程序实现视频通话
  16. 陕师大计算机好就业吗,陕师大是好学校吗?陕师大出来好不好就业?
  17. 企业变革与创新 | 如何打造创新”永动机“?
  18. 1.5小时,一键部署Oracle 11GR2 RAC 集群
  19. 经典设计原则:单一职责原则(SRP)
  20. 生肖猪鼠年运程面面观

热门文章

  1. varnish 加速
  2. 从零打造视频播放网站(2)-后端接口设计篇
  3. Android Studio打包SDK后,为什么没有bundles文件夹?
  4. 条款32:确定你的public继承塑模出is-a关系
  5. Vue图片浏览组件v-viewer简单应用
  6. python爬豆瓣小组,爬虫豆瓣群数量,小组
  7. RE《歌舞伎町案内人》
  8. thinkphp6 + phpexcel 导入导出数据,设置特殊表格
  9. Linux: kernel: eBPF BCC
  10. 线程的销毁java,Java如何销毁线程组?