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 的值是 0;
只能被创建这个文件的当前应用访问。若文件不存在会创建文件;若创建的文件已存在则会覆盖掉原来的文件。
2)追加模式
Context.MODE_APPEND 的值是 32768;
只能被创建这个文件的当前应用访问。若文件不存在会创建文件;若文件存在则在文件的末尾进行追加内容。
3)可读模式
Context.MODE_WORLD_READABLE的值是1;
创建出来的文件可以被其他应用所读取
4)可写模式
Context.MODE_WORLD_WRITEABLE的值是2;
允许其他应用对其进行写入。
②读写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,找一下它的定义:
//文件名为 key,具体文件为 value。存储所有 sp 文件,由 ContextImpl.class 锁保护
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;
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。