基础说明
涉及到几个类:
ContextImpl、SharedPreferencesImpl、QueueWork、ActivityThread,代码版本基于Android Q
。
假设有这么一个文件:
//文件名
public static final String SPNAME_USER = "userInfo";
//保存着两个键值对
public static final String USER_ID = "userId";
public static final String USER_NAME = "userName";
public SharePreferenceTest(Context context,String spName){
sharedPreferences = context.getSharedPreferences(spName,Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
}
变量说明
- 1、
mFile
:源数据文件,对应SPNAME_USER.xml
文件- 2、
mBackupFile
:灾备文件,对应SPNAME_USER.xml.bak
文件- 3、
mMap
:mFile
文件里面的具体键值对,对应例如:USER_ID:123、 USER_NAME:法外狂徒张三
,间接保存在内存中- 4、
CountDownLatch
:
- (1)
new CountDownLatch(1)
:创建一个值为count
的计数器- (2)
await
:阻塞调用方法的线程,如果如果当前计数为零,则此方法不执行阻塞,立即返回(很重要)
- (3)
countDown
:对计数器进行递减1
操作- 5、
renameTo
:重命名操作,我们可以理解为将A
文件移动到B
文件所在的目录,并以B
的文件名存在(前提是B
已经被删除),移动过去之后会将A
删除- 6、
mModified
:put
方法所操作的临时哈希表,最终会在commitToMemory
将数据转移到MemoryCommitResult
的mapToWriteToDisk
,但由于是将mMap
的引用赋值给mapToWriteToDisk
,所以对mapToWriteToDisk
赋值也就是对mMap
赋值,因此将数据保存在SharedPreferencesImpl
中,又因为SharedPreferencesImpl
实际是被缓存在静态变量sSharedPrefsCache
中,所以最终是保存在内存
中
源码
ContextImpl
1、sSharedPrefsCache
是
ContextImpl
的静态变量,存储了File
和SharedPreferencesImpl
的关系,好处是可以不需要每次都新建SharedPreferencesImpl
,但也有需要注意的点:SharedPreferencesImpl
持有着mMap
对象,如果数据量太大的话是会占据一定的内存空间的
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
2、mSharedPrefsPaths
一个
文件名
对应一个xml
文件,是getSharedPreferences(String name, int mode)
用来获取File
从而调用getSharedPreferences(File file, int mode)
的方法。
private ArrayMap<String, File> mSharedPrefsPaths;
3、getSharedPreferences(根据FileName
获取)
通过定义的
fileName
去获取一个SharedPreferences
,这是我们平常使用比较多的方法。
mSharedPrefsPaths
是一个ArrayMap<String, File>
数据结构,保存着文件名
和对应的文件
,这里主要做了:
- 1、判断
mSharedPrefsPaths
是否初始化过,没有的话进行初始化- 2、判断
文件名对应的文件
是否存在,不存在则新建
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
synchronized (ContextImpl.class) {
//
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
2、getSharedPreferences(根据File
获取)
这里做了:
- 1、获取保存在静态变量中的
ArrayMap<File, SharedPreferencesImpl>
,如果不存在则新建,注意这里会调用到SharedPreferencesImpl
的构造函数,这里面开启了一个线程去获取本地文件,这个下面再展开讲- 2、当
mode
是MODE_MULTI_PROCESS
时,通过startReloadIfChangedUnexpectedly
重新调用startLoadFromDisk
去磁盘获取xml
文件数据,所以它根本不能够作为跨进程通信的方案
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//获取缓存
final ArrayMap<File, SharedPreferencesImpl> cache =
getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
SharedPreferencesImpl
1、SharedPreferencesImpl
这里有几个值得关注的点:
- 1、将
file
保存在mFile
中- 2、创建一个
mBackupFile
,这是一个灾备文件- 3、构造函数调用了
startLoadFromDisk
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
2、startLoadFromDisk
这里做了:
- 1、加锁更改
mLoaded
变量- 2、新建一个线程去调用
loadFromDisk
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
3、loadFromDisk
这里有几个重要的点:
- 1、如果灾备文件存在,则使用灾备文件作为
mFile
,这里主要是和writeToFile
相呼应:如果writeToFile
写入磁盘失败的话,是会保留灾备文件mBackUpFile
同时删除源文件mFile
,下次初始化SharedPreferencesImpl
进入这个方法时会使用灾备文件作为源数据文件- 2、将文件读取到一个
map
里面,这里解析xml
的方式和LayoutInflater
一样使用XmlPullParserException
- 3、最后调用了
mLock.notifyAll()
,根据这行代码大概就可以知道肯定在某处有调用wait
操作来等待这个方法的结束
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
//renameTo的作用就是:
//假设有A、B两个文件,调用A.renameTo(B),会将A移动
//到B所在的目录,并使用B的文件名(前提是B已经被删除)
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
//将文件里的内容读到一个map里面
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);
}
}
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
//只有上面读文件出错了,thrown才会为null
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();
}
}
}
4、getString
可以看到有几个值得关注的点:
- 1、
get
方法都是加了synchronized
锁的,所以多线程情况下的读操作
性能都不会很好- 2、通过
强转
来转换要得到的目标数据类型,所以这里会有类型安全
的隐患,当put
进去的和get
出来的类型不一致就有可能会出现闪退,这在多人协作开发时出现的概率更高- 3、
awaitLoadedLocked
等待唤醒,而这里等待的就是loadFromDisk
这个方法,所以:如果一个xml
不幸保存了一些很大的数据,那再第一次初始化SharedPreferencesImpl
的时候立刻调用get
方法,是有很大概率是会造成ui
阻塞从而导致卡顿的。
关于
3
:
如果在应用启动的时候初始化SharedPreferencesImpl
,表现出来就是会白屏,因为这个消息处理的时间太长,导致后续的ui
绘制消息一直得不到处理(就算是同步屏障消息也得等正在处理的消息处理完)。同时也有可能会造成ANR
,因为我实验过取出一个200M
的xml
,通过systrace
可以看到线程大概睡眠了2-3秒
,假设在没有设置android:largeheap = "true"
的情况下,200m
已经接近oom
了(不同厂商手机有不同的APP
最大堆空间,这是我用华为手机得出的数据),而ANR
是5s内没有响应触摸事件(也就是新入队的消息没有及时得到处理)
,也就是说在设置android:largeheap = "true"
的情况下,当xml
文件足够大,大到可以让主线程阻塞5s
以上,再点击屏幕时就会造成ANR
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
1、awaitLoadedLocked
可以看到在
loadFromDisk
还没有mLock.notifyAll()
之前,这里是会一直wait
阻塞的
@GuardedBy("mLock")
private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
...
}
1、contains
也是
加锁
并wait
@Override
public boolean contains(String key) {
synchronized (mLock) {
awaitLoadedLocked();
return mMap.containsKey(key);
}
}
1、edit
获取
Editor
的方法,同样是加锁
并wait
,要注意每次调用edit
都会新建一个EditorImpl
对象,所以最好不要多次调用edit
,而是创建一个保存在单例工具栏中
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
EditorImpl
1、mModified
SharedPreferencesImpl
内部类EditorImpl
的一个变量,代表着通过put添加或者修改的数据
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
1、putString
可以看到
put
方法同样也是加了synchronized
,这说明对于多线程的读写性能
都有一定的影响,但是并不会影响到单线程,因为synchronized
是有一个锁升级的过程,单线程多次获取同一个锁的话synchronized
是不会升级成重量级锁
的,而是保持在偏向锁
的状态。
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
1、remove
删除对应
key
的value
用的是this
,根据commitToMemory
的注释说这是一个magic value
,也就是这个类的编写者自己规定好的规则,我们可以将它理解为null
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
1、clear
可以看到
clear
方法只是将标志变量
设置为true
,内部并没有立刻调用commit
或者apply
,需要手动调用
@Override
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
1、commit(重要)
commit
提交有几个特点:
- 1、在调用线程执行
enqueueDiskWrite
,从而执行writeToFile
写入磁盘- 2、
commit
有返回值:mcr.writeToDiskResult
- 3、
commit
和apply
的提交一样都是分两步:内存和磁盘;这里可以看到commit
的commitToMemory
和enqueueDiskWrite(writeToFile)
都是执行在调用线程的,这也就是网上说的:commit的原子性是包括提交到内存和磁盘
注意这里SharedPreferencesImpl.this.enqueueDiskWrite和mcr.writtenToDiskLatch.await(),我一度以为CountDownLatch是以lock-unlock的调用形式进行同步的,所以一直想不通怎么会先通过countDown唤醒,再调用await锁定呢?其实这里的逻辑主要是enqueueDiskWrite调用到writeToFile执行setDiskWriteResult,从而调用countDown将计数器设置为0,然后await的时候发现计数器为0是不会阻塞的,直接跳过
@Override
public boolean commit() {
long startTime = 0;
//得到一个MemoryCommitResult对象
MemoryCommitResult mcr = commitToMemory();
//注意第二个参数传了null,而apply是传了具体的runnable
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null);
try {
//在主线程调用await阻塞
mcr.writtenToDiskLatch.await();
}
//回调监听
notifyListeners(mcr);
//返回结果值
return mcr.writeToDiskResult;
}
1、commitToMemory
这个方法主要做了几件事:
- 1、只有进入了
commitToMemory
方法,mDiskWritesInFlight
才会大于0
,所以这里个人理解
是为了避免单线程短时间内多次进入这个方法,导致下面调用HashMap的遍历时出现fail-fast(快速失败),所以这里直接拷贝一个map来操作
- 2、处理所有监听器
- 3、遍历出
mModified
的所有数据,除去被remove掉或者是不存在的那些
,剩下的保存在mapToWriteToDisk
这个Map
变量中,这就相当于将mModified
过滤后保存在mapToWriteToDisk
- 3、将监听器、过滤后的数据、当前修改版本号、被修改过的值所对应的
key
全部存入一个MemoryCommitResult
对象中
注:这个方法叫做“提交到内存”,但乍一看貌似没有对mMap做什么操作,但实际上是将mMap的引用赋值给了mapToWriteToDisk,所以修改了mapToWriteToDisk也就是修改mMap
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
//那些数据已经发生变化的所对应的key
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
//临时变量,用来保存mModified过滤后,需要被写入
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
//只有进入了这个方法,mDiskWritesInFlight才会大于0
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
//处理所有的监听器,最终都是要保存在MemoryCommitResult对象中
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
//判断数据是否有改变
//如果没有改变的话,提交任务的时候并不会执行,而是直接return
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
//取出mModified中所有的k-v
//除去被remove掉或者是不存在的那些
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//根据注释,这个this算是一个"magic value",用来替代null
//所以remove那里也是置的this
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,因为有用的数据已经存在mapToWriteToDisk中了
mModified.clear();
//如果改变过数据,则将修改版本号+1
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
MemoryCommitResult
commitToMemory
的返回值,里面保存了:
- 1、修改的版本号
memoryStateGeneration
- 1、已过滤的数据
mapToWriteToDisk
- 2、已修改的
key
列表- 3、
CountDownLatch
同步工具类- 4、监听器
OnSharedPreferenceChangeListener
setDiskWriteResult
方法是用来表明写入是否成功,成功或失败后都会调用writtenToDiskLatch.countDown
将计数器-1
从而达到唤醒阻塞线程的作用
private static class MemoryCommitResult {
final long memoryStateGeneration;
@Nullable final List<String> keysModified;
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;
final Map<String, Object> mapToWriteToDisk;
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
@GuardedBy("mWritingToDiskLock")
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
Map<String, Object> mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysModified = keysModified;
this.listeners = listeners;
this.mapToWriteToDisk = mapToWriteToDisk;
}
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
1、enqueueDiskWrite(重要)
commit
和apply
都会调用到这个方法对于
commit
:
- 1、
isFromSyncCommit
为true
- 2、
commit
在主线程
调用writeToDiskRunnable.run()
,从而触发writeToFile
对于
apply
:
- 1、
isFromSyncCommit
为false
- 2、
apply
将任务通过Handler
发送到QueuedWork
内部的HandlerThread
中,也就是在子线程
中处理
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//如果是apply,这个变量会是true
//如果是commit,这个变量是false
final boolean isFromSyncCommit = (postWriteRunnable == null);
//创建一个writeToDiskRunnable
//这个runnable主要执行了writeToFile和postWriteRunnable
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//这是commit才会执行的逻辑
//在当前线程执行writeToDiskRunnable.run()
if (isFromSyncCommit) {
boolean wasEmpty = false;
//只有在writeToDiskRunnable里面才会执行mDiskWritesInFlight--将
//值减为0,所以这里为true
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
//执行writeToDiskRunnable
//也就是执行了writeToFile和postWriteRunnable
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//这是apply才会执行的逻辑
//将任务加入队列,内部逻辑是将任务通过handler发送到
//创建的子线程的MessageQueue中
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
1、writeToFile
这里主要做了:
- 1、判断
mFile
是否存在,根据loadFromDisk
可以知道原本的mFile
已经被delete
了,现在存在的mFile
是原来的mBackUpFile
通过renamTo
顶替过去的,同时mBackUpFile已经不存在了
,所以fileExists
为true
- 2、根据版本号是否变化判断变量
needsWrite
,如果needsWrite
为false
即代表数据没有发生变化,不需要做无谓的IO操作
,直接return
- 3、判断灾备文件是否存在,如果存在,将
mFile
删除;如果不存在,将mFile
通过renameTo
移动到mBackUpFile
的位置并重命名,同时将mFile
删除(因为renameTo
),虽然mFile
一定会被删除,但是delete
后并不会使mFile == null
- 4、将
MemoryCommitResult
里面通过commitToMemory
方法得到的数据mapToWriteToDisk
写入xml
,再通过FileUtils.sync(str)
将数据强制写入磁盘文件(为了提升 I/O 性能,文件系统把数据写入到 Page Cache 中,然后等待合适的时机才会真正的写入磁盘),这里面分成两种情况:
- (1)写入成功:将
mBackUpFile
删除- (2)写入失败:保留
mBackUpFile
,删除mFile
,这里和loadFromDisk
相呼应。假设这次写入因为异常原因(例如过热关机)而失败,那下次初始化SharedPreferencesImpl
调用loadFromDisk
时会使用mBackUpFile
作为源文件
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
//判断文件是否存在
boolean fileExists = mFile.exists();
if (fileExists) {
boolean needsWrite = false;
//判断版本是否已经改变,如果数据没有变化的话是不需要更新的
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
//needsWrite为false
//也就是数据没有变化,调用setDiskWriteResult通过
//CountDownLatch唤醒被阻塞的线程
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
//对于初始化来说这里是false
boolean backupFileExists = mBackupFile.exists();
//如果灾备文件不存在,将mFile作为灾备文件,并删除mFile(因为renameTo)
//如果灾备文件存在,删除mFile
//但是要注意,delete后并不代表mFile就为null了
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
//到了这一步就已经将数据保存在灾备文件里面了(什么数据?)
//
try {
FileOutputStream str = createFileOutputStream(mFile);
...
//通过XmlUtils工具类写入文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
...
//为了提升 I/O 性能,文件系统把数据写入到 Page Cache 中
//,然后等待合适的时机才会真正的写入磁盘
//而通过sync方法可以不需要等待,强制让数据写入文件
FileUtils.sync(str);
...
str.close();
...
//已经写入
//删除灾备文件
mBackupFile.delete();
//更新 修改版本号
mDiskStateGeneration = mcr.memoryStateGeneration;
//
mcr.setDiskWriteResult(true, true);
...
//注意这个return,如果写入成功的话,是不会执行下面的代码的
return;
}catch(){
...
}
//执行到这里说明写入失败了
//将mFile删除
if (mFile.exists()) {
if (!mFile.delete()) {
...
}
}
mcr.setDiskWriteResult(false, false);
}
1、apply
这里有几个点:
- 1、获取一个
MemoryCommitResult
- 2、创建一个
awaitCommit
,awaitCommit
这个Runnable
的执行内容是CountDownLatch.await()
,它的add
时机在本方法内,remove
时机在postWriteRunnable
这个Runnable
的run
方法,而它执行run的时机在QueuedWork.waitToFinish里面,重点来了:waitToFinish在Activity和Service的多个生命周期里面会调用,所以如果迟迟得不到remove,是有可能会造成ANR的,那调用remove方法的postWriteRunnable什么时候执行呢?是在QueueWork.processPendingWork中,这个方法会等writeToFile执行完再会执行
- 3、将
postWriteRunnable
作为enqueueDiskWrite
方法的参数,这会导致enqueueDiskWrite
方法里面的isFromSyncCommit
变量为false
,从而区分commit
和apply
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
//过滤完mModified的值后,保存在MemoryCommitResult中
final MemoryCommitResult mcr = commitToMemory();
//创建一个awaitCommit
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
//CountDownLatch是一个同步工具类,允许一个或
//多个线程一直等待,
//直到其他线程运行完成后再执行。
//这行代码运行在主线程,因此是会让主线程阻塞
mcr.writtenToDiskLatch.await();
}
}
};
//给QueuedWork添加一个runnable
QueuedWork.addFinisher(awaitCommit);
//创建一个postWriteRunnable,这个runnable是用来触发上面那个awaitCommit的
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
//回调监听
//所以apply也并不是拿不到操作结果
notifyListeners(mcr);
}
notifyListeners
这里要注意的是:
- 1、
onSharedPreferenceChanged
执行在主线程- 2、注册监听的时候不要用匿名内部类,不然会注销不了,而且
listener
是一个WeakHashMap
,很容易因为被回收而导致mcr.listeners == null
- 3、只会回调
value有变化
的key
private void notifyListeners(final MemoryCommitResult mcr) {
if (mcr.listeners == null || mcr.keysModified == null ||
mcr.keysModified.size() == 0) {
return;
}
if (Looper.myLooper() == Looper.getMainLooper()) {
for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
final String key = mcr.keysModified.get(i);
for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
if (listener != null) {
listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
}
}
}
} else {
// Run this function on the main thread.
ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr));
}
}
QueuedWork
1、getHandler
获取一个绑定了
HandlerThread Looper
的Handler
@UnsupportedAppUsage
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;
}
}
1、waitToFinish(重要)
在
handleServiceArgs(Service.onStartCommand)、handleStopService(Service.onDestroy)、handlePauseActivity(Activity.onPause)、handleStopActivity(Activity.onStop)等生命周期中都有调用
但在Android 8.0之前和之后的实现有些不同
,8.0之后会在调用这个方法的时候尝试通过processPendingWork
去触发任务
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
//这里
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
}
}
}
1、queue
apply
入队是有100ms
的DELAY
的
@UnsupportedAppUsage
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
1、processPendingWork
这里有几个主要的点:
- 1、如果在
100ms
之内多次调用apply
,是会一次性将这100ms queue
的msg
全部取出来一并处理的,这样就可以不需要每个消息都等待100ms
(但是这里是遍历出所有的runnable
执行run
,并没有网络上说的只执行最后一次apply
,还是说我看漏了?)
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);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
ActivityThread
1、handleServiceArgs
可以看到这里会
等待任务完成
,而这些方法在AMS
层启动的时候都会发送一个延时的ANR
消息到MessageQueue
的,如果这里阻塞太久而导致无法通知AMS
去remove
掉这个消息,就会造成ANR
但Android 8.0
我们可以看到有所优化:主动触发任务进行
private void handleServiceArgs(ServiceArgsData data) {
res = s.onStartCommand(data.args, data.flags, data.startId);
...
QueuedWork.waitToFinish();
}
1、handleStopService
同上
private void handleStopService(IBinder token) {
s.onDestroy();
...
QueuedWork.waitToFinish();
}
1、handlePauseActivity
同上
@Override
public void handlePauseActivity(){
performPauseActivity(r, finished, reason, pendingActions);
...
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
}
1、handleStopActivity
同上
@Override
public void handleStopActivity(){
performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
reason);
...
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
}
1、handleSleeping
同上
private void handleSleeping() {
if (!r.stopped && !r.isPreHoneycomb()) {
callActivityOnStop(r, true /* saveState */, "sleeping");
}
...
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
}
总结
- 1、支持的存储类型:
String、StringSet、Int、Long、Float、Boolean
- 2、保存着
K-V
的mMap
会通过静态变量sSharedPrefsCache
被间接保存在内存中- 3、通过强转获取数据,有可能导致
类型不安全
而闪退- 4、方法都用了
synchronized
加锁,是线程安全
的- 5、
MODE_MULTI_PROCESS
只是重新从磁盘获取文件,并不能用于多进程通信
- 6、调用
getSharedPreferences
时会阻塞直到文件被读出来,所以不适合一个文件里面放很大的数据,可以考虑分成多个小文件(但这些小文件一样会占据着内存空间)- 7、提交分为
commit
和apply
commit
:
- (1)内存写入和磁盘写入都运行在
调用线程
- (2)有写入结果的返回值
apply
:
- (1)内存写入运行在
主线程
,磁盘写入运行在子线程
- (2)无返回值
- (3)在
100ms
内多次调用apply
,系统会将这期间内的所有写入磁盘任务
一起执行,从而避免每个任务都等待100ms
- 8、
onSharedPreferenceChanged
运行在主线程- 9、
commit
和apply
都无法避免ANR
:commit
的ANR
是因为提交过程在调用线程(假设是ui线程)
,如果文件太大阻塞了5s
以上,这时候系统再接收到一个触摸事件
,就会抛出ANR
错误;apply
的ANR
是Activity
和Service
在多个生命周期里面会通过QueueWork.waitToFinish
等待任务完成,而Activity
和Service
在执行这些生命周期过程中AMS
也是会参与的,在服务端AMS
执行的时候会发送一个ANR
延时消息,假设客户端在回调完生命周期之后没有及时告知AMS
去remove
掉ANR
消息的话,就会触发ANR
- 10、写入过程中意外退出是会丢失数据的,关于灾备文件的逻辑:在初始化
SharedPreferencesImpl
时不会创建灾备文件(只是创建了个File
文件对象),在磁盘写入方法writeToFile
时会判断灾备文件是否存在,如果不存在就将源数据文件mFile作为灾备文件
,假如写入过程中失败,则会将源数据文件丢弃,保留灾备文件,下次重新启动应用初始化SharedPreferencesImpl
检测到灾备文件存在,就会用灾备文件代替源数据文件
没懂的地方
1、writeToFile
mFile
其实在renameTo
后就不存在了,为什么还要删除多一次?
2、网上提到的apply非全量写入体现在哪
等有时间再扫多一遍
3、SharedPreferences接口在commit和apply方法的注释写着Note that when two editors are modifying preferences at the same time, the last one to call commit wins
这个是我看漏了吗,貌似多次调用一样是遍历调用
Runnable.run
,后续再重新看一下