SharedPreferences是Android平台上 轻量级的存储类,用来保存App的各种配置信息,其本质是一个以 键值对(key-value)的方式保存数据的xml文件,其保存在/data/data/PACKAGE_NAME /shared_prefs目录下。
这里先列举在使用时的注意事项:
1、SharedPreferences一经加载,它内部存储的数据就会以Map的形式一直保存在内存中,所以不宜存放大数据。
2、获取SharedPreferences时,如果是初次加载,那么需要从文件中去加载数据到内存中,这是一个耗时操作,所以加载数据的这个过程是在子线程中完成的,但是,注意重点,当我们对SharedPreferences进行操作,而SharedPreferences加载数据又还没有结束,这时会导致线程等待,直到数据加载完后在开始操作数据,所以,对于要用到SharedPreferences,应该提前去获取,以确保使用时已经将数据加载完毕,这里说的是首次,如果加载过一次,那么之后的获取就是直接从内存中获取了;所以说如果不合理使用就会出现卡顿甚至ANR问题:
3、保存数据时,apply()是先将数据保存到内存中,然后在子线程中将数据保存到磁盘,commit()也是先将数据保存到内存中,之后立即将数据同步到磁盘,这个操作是在当前线程中完成,所以会阻塞线程,所以现在android建议使用apply(),但是使用apply()时也有需要注意的地方,如果保存数据时这个过程还没有结束,但是这时退出了activity,这时activity会先确保数据是否已经全部同步到磁盘了,如果没有,这时会两种情况,一是保存的过程正在子线中执行,这时等待就好,如果这时还没分发给子线程,那么就直接切换到主线程执行了,所以这时提交数据时也有需要注意的地方:使用SharedPreferences.Editor提交数据时,edit() 方法每次都会新建一个 EditorImpl 对象。所以应该多次 putXXX(),尽量在所有数据都提交后再调用apply()方法。
4、无论是 commit() 还是 apply() ,针对任何修改都是全量写入。所以应该要分清哪些数据是常用的,哪些是不常用的,常用的保存在一起,不常用的保存在一起。
5、commit() 同步保存,有返回值。apply() 异步保存,无返回值。按需取用。
6、onPause() 、onReceive() 等时机会等待异步写操作执行完成,可能造成卡顿或者 ANR。
以上提出的使用注意事项和结论,我会从下面分析中一一解答。
一、读取数据
基本写法:
1 2 | SharedPreferences sharedPreferences = getContext().getSharedPreferences("sp_yy_fly", Context.MODE_PRIVATE); boolean value= sharedPreferences .getBoolean("key", false); |
注:
上文中 Context.MODE_PRIVATE是用来声明读写模式,将此位置设置未mode, mode指定为MODE_PRIVATE,则该配置文件只能被自己的应用程序访问。(也可写成0) mode指定为MODE_WORLD_READABLE,则该配置文件除了自己访问外还可以被其它应该程序读取。不安全,官方已弃用(也可写成1) mode指定为MODE_WORLD_WRITEABLE,则该配置文件除了自己访问外还可以被其它应该程序写入。不安全,官方已弃用(也可写成2) mode指定为MODE_APPEND,检查文件是否存在,存在就往文件追加内容,否则就创建新文件 |
读取数据主要执行两步:
- 获取SharedPreferences实例
- 调用getString(key,"") 方法获取数据
从上面可以看出getSharedPerferences(String name,int mode)方法来自Context,所以在Context的实现类ContextImpl中分析getSharedPreferences方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | /** * 文件名为 key,具体文件为 value。存储所有 sp 文件 * 由 ContextImpl.class 锁保护 */ @GuardedBy("ContextImpl.class") private ArrayMap<String, File> mSharedPrefsPaths;
@Override public SharedPreferences getSharedPreferences(String name, int mode) { // At least one application in the world actually passes in a null // name. This happened to work because when we generated the file name // we would stringify it to "null.xml". Nice. // 在api 19之前,如果name为null,那默认就将文件取名为null.xml,但是之后就会报NullPointerException; if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } }
File file; synchronized (ContextImpl.class) { //首先会尝试从mSharedPrefsPaths中获取name在本地的File, if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null) { //创建一个name +".xml"的文件 file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); } /** * 创建一个name +".xml"的文件 * @param name * @return */ @Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); }
/** *将此 xml文件,其保存在/data/data/PACKAGE_NAME /shared_prefs目录下。 * @return */ @UnsupportedAppUsage private File getPreferencesDir() { synchronized (mSync) { if (mPreferencesDir == null) { //getDataDir()获取包名信息 mPreferencesDir = new File(getDataDir(), "shared_prefs"); } return ensurePrivateDirExists(mPreferencesDir); } } |
mSharedPrefsPaths 是一个 ArrayMap ,缓存了文件名和 sp 文件的对应关系。首先会根据参数中的文件名 name 查找缓存中是否存在对应的 sp 文件。如果不存在的话,会新建名称为 [name].xml 的文件,并存入缓存 mSharedPrefsPaths 中。最后会调用另一个重载的 getSharedPreferences(file,mode) 方法,参数是 File 。
这里先看下ArrayMap,它是Android中提供的,它的作用类似HashMap,但是ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计,所以在内存方面ArrayMap比HashMap更有效率,所以在android中推荐使用ArrayMap,这里使用的就是ArrayMap。
接下来调用的就是getSharedPreferences(file, mode),如果你不想使用默认保存文件的地方,那就可以使用这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @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); //targetSdk为androidO及以上、访问内部存储空间、当前设备处于锁定状态时不可以访问内部存储空间 if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) { if (isCredentialProtectedStorage() && !getSystemService(UserManager.class) .isUserUnlockingOrUnlocked(UserHandle.myUserId())) { throw new IllegalStateException("SharedPreferences in credential encrypted " + "storage are not available until after user is unlocked"); } } sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } // mode 为 MODE_MULTI_PROCESS 时,文件可能被其他进程修改,则重新加载 // 显然这并不足以保证跨进程安全 if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { // If somebody else (some other process) changed the prefs // file behind our back, we reload it. This has been the // historical (if undocumented) behavior. sp.startReloadIfChangedUnexpectedly(); } return sp; } |
注:targetSdk为androidO及以上、访问内部存储空间、当前设备处于锁定状态时不可以访问内部存储空间
这个方法主要的逻辑就是先获取缓存,根据文件查看缓存中是否存在,如果存在就直接返回,如果不存在,那么就新建一个SharedPreferences,然后保存到缓存中,再返回,这里获取缓存使用的是getSharedPreferencesCacheLocked(),这里先去看下它的缓存是怎样设计的;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /** * 使用static使sSharedPrefsCache一直缓存在内存中,所以说在第一次加载后,在这之后就一直在内存中获取了 */ private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@GuardedBy("ContextImpl.class") 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; } |
这里用的也是ArrayMap,并且还是static的,这也就是说这个是一直保存在内存中的,这也就是为什么说在第一次加载后,在这之后就一直在内存中获取了,接下来就去看下初次加载是怎样的一个执行逻辑,对于初次加载,是创建sp = new SharedPreferencesImpl(file, mode)这样一个对象并返回,那我们就去看看它的构造方法到底做了些什么事情:
SharedPreferencesImpl(file, mode)的构造方法:
1 2 3 4 5 6 7 8 9 10 | @UnsupportedAppUsage SharedPreferencesImpl(File file, int mode) { mFile = file; // sp 文件 mBackupFile = makeBackupFile(file);// 创建备份文件 mMode = mode; mLoaded = false;// 标识 sp 文件是否已经加载到内存 mMap = null; // 存储 sp 文件中的键值对 mThrowable = null; startLoadFromDisk(); } |
咋一看没什么啊,都是初始化一些变量,但这里有一个重点需要注意,那就是startLoadFromDisk(),看名字就知道是去磁盘中加载数据,也就是说只有在创建实例的时候才会去加载数据,前面的有说到,一旦创建一个实例后就会保存到缓存中,这也就是说只在第一次获取的时候才会去磁盘中加载数据;所以第一次读取时稍微慢一点, 后面的都在内存操作所以比较快。
startLoadFromDisk()方法会启动一个线程, 然后异步调用loadFromDisk()把xml文件的内容加载到内存Map中。
startLoadFromDisk()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | @UnsupportedAppUsage private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); }
private void loadFromDisk() { synchronized (mLock) { if (mLoaded) { return; } if (mBackupFile.exists()) { mFile.delete(); // 如果存在备份文件,直接将备份文件重命名为 sp 文件 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; //具体的文件读取操作,然后解析XML,存储在Map中!这也是为何SharedPrefernces不是线程安全的,因为使用的是Map数据结构 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 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,之后的获取数据都是通过mMap 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(); } } } |
简单捋一下流程:
- 判断是否已经加载进内存
- 判断是否存在遗留的备份文件,如果存在,重命名为 sp 文件
- 读取 sp 文件,并存入内存
- 更新文件信息
- 释放锁,唤醒处于等待状态的线程
这里是具体的文件读取操作,然后解析XML,然后存储在Map中,这也是为何SharedPrefernces不是线程安全的,因为使用的是Map数据结构导致的线程不安全。这里有一个mLock锁,这个锁很重要,这里获取到了锁,当SharedPreferences去操作数据的时候就会先去判断这个锁是否释放了,如果没有,那么就会等待锁释放了在执行,最后一步mLock.notifyAll()指加载完成,通知正在等待的代码继续执行。读取数据完成后就将数据加载到了内存中,最终保存在mMap中,那么接下来看他是怎样获取数据的,这里就看下它的getString()方法
1 2 3 4 5 6 7 8 9 10 | @Override @Nullable public String getString(String key, @Nullable String defValue) { synchronized (mLock) { //等待文件加载到内存 awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } } |
getString根据key调用各种get方法,从mMap中获取值。其中在读取get方法时需要加锁。这里一进来就加了一个锁,这个锁是为了防止数据还没加载就执行到这里,那这时就需要将锁释放掉,但又不对数据进行操作,那这个awaitLoadedLocked()就起到了这个作用。
继续分析awaitLoadedLocked()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @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(); } //在读取文件完成后,mLoaded会置为true while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } if (mThrowable != null) { throw new IllegalStateException(mThrowable); } } |
awaitLoadedLocked()会判断当前文件是否下载完成, 如果没有完成就阻塞等待; 上面异步加载文件的loadFromDisk()方法, 在加载完成后会调用mLock.notifyAll()通知这里继续执行。这也就是说,数据没加载完是不能对数据进行操作的,这时就阻塞在这里,到这,一开始说到的第一点和第二点就可以解释了,那么接下来就来看看数据是如何修改的。
二、存入数据
SharedPreferences sharedPreferences = getContext().getSharedPreferences("sp_yy_fly", Context.MODE_PRIVATE); SharedPreferences.Editor edit =sharedPreferences.edit(); edit.putBoolean("key", false); edit.putInt("key",123); edit.putFloat("key",1234L); edit.putString("key","value"); edit.apply(); //edit.commit(); |
从上面可以看出,在获取SharedPreferences 实例后,调用edit()方法获取Editor的实例EditorImpl,这里也是要等数据加载完才能对数据进行操作,所以awaitLoadedLocked()会判断当前文件是否下载完成, 如果没有完成就阻塞等待。实际上对数据进行修改使用的是EditorImpl这个类。
每调用一次edit()方法就会新建一个EditorImpl实例,所以不要每修改一次就调用一次,可以尽量将所有需要修改的数据放一起进行操作,接下来看看这个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } @Override public Editor putStringSet(String key, @Nullable Set<String> values) { synchronized (mEditorLock) { mModified.put(key, (values == null) ? null : new HashSet<String>(values)); return this; } } @Override public Editor putInt(String key, int value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } @Override public Editor putLong(String key, long value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } @Override public Editor putFloat(String key, float value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } } @Override public Editor putBoolean(String key, boolean 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; } } |
拿到SharedPreferences.Editor后就可以往里面putString(), putInt 等等...put的数据都是保存在mModified这个临时的Map集合里面了。
这个时候数据还是在内存里面, 只有调用commit()或者apply()才会保存到本地文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | @Override public void apply() { final long startTime = System.currentTimeMillis(); // 将临时存储数据的集合mModified合并到要保存到磁盘里的集合中mapToWriteToDisk final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { // writtenToDiskLatch是一个CountDownLatch对象,除非数据全部同步到磁盘,否则这里就一直阻塞 mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { }
if (DEBUG && mcr.wasWritten) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms"); } } }; // 这里将awaitCommit添加到QueuedWork中的目的是为了退出activity时,确保所有数据已经全部同步到磁盘中了 QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); // 数据同步完这里就会将awaitCommit从QueuedWork移除 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); } |
注:QueuedWork这是个内部工具类,用于跟踪那些未完成的或尚未结束的全局任务,新任务通过方法这个类用于可延迟
注:CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); 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); return mcr.writeToDiskResult; } |
相同点:
- 都会调用commitToMemory(), 然后调用enqueueDiskWrite()把Map的数据写入本地文件中
不同点:
- commit() 有返回值, 可以知道成功还是失败
- apply() 没有返回值, 并且多了2个Runnable
commitToMemory()方法会先把mMap的数据放到mapToWriteToDisk这个map上,之后遍历之前put数据的mModified这个Map, 把我们修改的数据同步到mapToWriteToDisk中, 并且清空mModified自己。最后返回一个MemoryCommitResult对象,将数据保存到磁盘中用到的就是这个类;此处就可以解释问题4:无论是 commit() 还是 apply() ,针对任何修改都是全量写入。所以应该要分清哪些数据是常用的,哪些是不常用的,常用的保存在一起,不常用的保存在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | // Returns true if any changes were made private MemoryCommitResult commitToMemory() { long memoryStateGeneration; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) { // We optimistically don't make a deep copy until // a memory commit comes in when we're already // writing to disk. if (mDiskWritesInFlight > 0) { // We can't modify our mMap as a currently // in-flight write owns it. Clone it before // modifying it. // noinspection unchecked mMap = new HashMap<String, Object>(mMap); } // 将需要保存到磁盘中的数据保存到这个集合中 mapToWriteToDisk = mMap; mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); }
synchronized (mEditorLock) { boolean changesMade = false; // 如果调用了clear(),那么就会将集合中原先的数据清除掉 if (mClear) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } mClear = false; } // 遍历存储在临时集合中的数据,然后合并到mapToWriteToDisk中 for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); // "this" is the magic value for a removal mutation. In addition, // setting a value to "null" for a given key is specified to be // equivalent to calling remove on that 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 mModified.clear();
if (changesMade) { mCurrentMemoryStateGeneration++; }
memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk); } |
commit()方法调用enqueueDiskWrite的时候, 这个postWriteRunnable传为空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { // 注意这里的postWriterRunnable,如果是apply()方法,传进来的参数是不为null的,如果是commit(), // 这里传进来的为null,isFromSyncCommit的值决定了是立即同步还是稍后再同步 final boolean isFromSyncCommit = (postWriteRunnable == null); final Runnable writeToDiskRunnable = new Runnable() { @Override public void run() { synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); } synchronized (mLock) { mDiskWritesInFlight--; } if (postWriteRunnable != null) { postWriteRunnable.run(); } } }; // 如果是commit(),那么就会执行到这里,在当前线程中同步数据 if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } // 如果是apply(),那么就会执行到这里, QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); } |
这里会创建一个writeToDiskRunnable里面会执行当writeToFile()把数据写入文件。
调用commit的时候, isFromSyncCommit = true, 所以会在当前线程执行同步数据,你在主线程调用该方法,就会直接在主线程进行 IO 操作,如果数据过大,可能就会再次阻塞,这也就是为什么android现在不推荐使用这个方法了。
调用apply的时候会把写入文件的Runnable加入QueuedWork.queue()中执行, 其实是把Runnable发送到HandlerThread的子线程中执行,
接下来看看queue()里面是如何做的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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); } } } |
先获取到一个handler,然后将同步数据的任务添加到了sWork这个集合中,那后面处理肯定也是去这个集合里面拿了,根据上面的分析,这里的shouldDelay是为false的,所以执行的是下面这个方法,看来重点是这个handler了,那就去看看这个getHandler()了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @UnsupportedAppUsage private static Handler getHandler() { synchronized (sLock) { if (sHandler == null) { // 这里使用HandlerThread是为了获取子线程的looper对象,handler中处理消息就是通过这个looper 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 { static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) { super(looper); }
public void handleMessage(Message msg) { if (msg.what == MSG_RUN) { processPendingWork(); } } } |
这里就是构建一个handler,如果已经存在就直接返回了,不过这个handler处理消息是在子线程中执行的,要看明白这里,首先你得知道Handler和HadlerThread的原理,构建Handler在子线中处理消息时,一定要为他准备一个looper对象,否则是会有异常的,看这里的handleMessage(),将执行的逻辑交给了processPendingWork(),来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | private static void processPendingWork() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } synchronized (sProcessingWork) { LinkedList<Runnable> work;
synchronized (sLock) { // 将前面添加的任务全部clone出来,然后清除 work = (LinkedList<Runnable>) sWork.clone(); sWork.clear();
// 将任务取出来后,就可以取消后面执行任务的消息了,这里取消的只是上面clone出来的任务 getHandler().removeMessages(QueuedWorkHandler.MSG_RUN); } // 这里就是开始将数据同步到内存中 if (work.size() > 0) { for (Runnable w : work) { w.run(); }
if (DEBUG) { Log.d(LOG_TAG, "processing " + work.size() + " items took " + +(System.currentTimeMillis() - startTime) + " ms"); } } } } |
这里执行的逻辑就是将前面添加的数据clone出来作为局部变量,然后将clone的集合清空,再移除发送任务的消息(这个任务已经添加到集合中了),这样数据同步就在子线程中完成了。
到这,主要的逻辑都捋了一遍,但是还有一个点,就是一开始说的第三点,退出activity时,数据没同步完是会阻塞线程的,现在就看看为什么会阻塞线程?这里要先知道一点,Activity生命周期都是通过ActivityThread来控制的,在ActivityThread的handleStopActivity方法中,这里控制的就是Activity的stop,在这个方法中有调用到:QueuedWork.waitToFinish();这里就是查看QueuedWork中的任务是否都执行完了,如果没有执行完,那么就会处于等待状态,看下它里面是一个怎样的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | public static void waitToFinish() { long startTime = System.currentTimeMillis(); boolean hadMessages = false;
Handler handler = getHandler(); // 先看下Handler中是否还有等待的消息,如果有那么就移除 synchronized (sLock) { if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) { // Delayed work will be processed at processPendingWork() below handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) { hadMessages = true; Log.d(LOG_TAG, "waiting"); } }
// We should not delay any work as this might delay the finishers 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++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) { mWaitTimes.log(LOG_TAG, "waited: "); } } } } |
这里会判断添加的任务是否全部执行完了,如果没有执行完,那么就会将本该在子线程中执行的任务全部移到主线程中来执行。