赞
踩
在 Android 中,常用数据存储方式通常有以下几类:
文件存储:将数据存储在文件中。文件存储根据位置不同,可以存储在应用包下,成为内部存储;也可以存储在 storage文件夹(当然也有可能是mnt文件夹,不同的手机厂商可能不一样)上,称之为外部存储
- SharedPreferences 存储:SharedPreferences 是 Android 提供的用来存储一些简单配置信息的一种机制,核心原理是:保存基于 XML 文件存储的 key-value 键值对数据。通常使用该种方式用来存储一些简单信息,例如:应用版本信息,应用主题类型,开关配置,用户信息等等。其采用 Map 数据结构来存储数据,以键值对方式存储,使得读取与写入很方便,属于一种轻量级的存储机制。
- SQLite数据库存储:Android 系统中轻量级关系型数据,允许用户进行创建表结构,存储应用数据等操作
- 使用 ContentProvider 存储数据:在应用程序之间,共享或者传递相关信息时,往往可以使用 Content Provider 和 ContentResolver 实现
- 网络获取:存储在服务器上,通过接口数据从服务器后台获取,需要网络访问
SharedPreferences 本身是一个接口,无法直接创建 SharedPreferences 实例。可以通过 Context 提供的 getSharedPreferences(String name, int mode)方法来获取 SharedPreferences 实例,第一个参数表示要操作的xml文件名,第二个参数表示操作模式:MODE_PRIVATE、MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE,推荐使用 MODE_PRIVATE。
Editor:SharedPreferences 只能获取数据,不能存储和修改。存储修改是通过 SharedPreferences.edit() 获取的内部接口 Editor 对象实现。
SharedPreferences 简单使用:
val sp = getSharedPreferences("sp_test", Context.MODE_PRIVATE)
val edit = sp.edit()
edit.putString("key", "123")
edit.putInt("number", 10)
edit.apply()
println("sp_string " + sp.getString("key", ""))
println("sp_string " + sp.getInt("number", 0))
SharedPreferences 对应的 xml 文件位置:/data/data/package name/shared_prefs/
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="number" value="10" />
<string name="key">123</string>
</map>
在分析 SharedPreferences 源码之前,我们带着几个问题来看:
- 为什么 SharedPreferences 适用于少量数据存储?
- SharedPreferences 的 commit 和 apply 有什么区别,分别在什么场景下使用?
- 为什么 SharedPreferences 会导致卡顿、ANR 等问题?
- 如何正确使用 SharedPreferences,以及我们可以做哪些优化?
- 多进程下使用 SharedPreferences 是否安全?
SharedPreferences 实例获取是通过 Context 来获取,具体实现在 ContextImpl 中。在 ContextImpl 中有这几个变量:
// 记录所有的 SharedPreferences 文件,key 为文件名,value 为文件
private ArrayMap<String, File> mSharedPrefsPaths;
// 以包名为key, 二级key是以SP文件, 以 SharedPreferencesImpl 为value 的嵌套 map 结构. 注意:sSharedPrefsCache 是静态类成员变量, 每个进程只保存唯一一份, 且由 ContextImpl.class 锁保护.
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
// 记录 SharedPreferences 所在文件路径,/data/data/包名/shared_prefs/
private File mPreferencesDir;
下面来看看如果获取 SharedPreferences 实例:
首先调用 getSharedPreferences 方法,该方法先找到名字为 name 的 xml 文件,不存在的话则创建一个新文件。可以看到 mSharedPrefsPaths 在这个方法中创建,mSharedPrefsPaths 不是 static 变量,相当于每个 Context 都有一个 mSharedPrefsPaths,但相当于 static 变量,因为创建时是以 ContextImpl.class 加锁保护的,也就是说同一个文件名的 xml 文件,有且仅有一个。
@Override public SharedPreferences getSharedPreferences(String name, int mode) { if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } } 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); }
获取指定路径下的 Sharepreferences 文件
// 返回的是获取的 Sharepreferences 文件 File,文本全路径为 /data/data/包名/shared_prefs/name.xml @Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); } // mPreferencesDir 为 Sharepreferences 的文件路径,即 /data/data/包名/shared_prefs/ @UnsupportedAppUsage private File getPreferencesDir() { synchronized (mSync) { if (mPreferencesDir == null) { mPreferencesDir = new File(getDataDir(), "shared_prefs"); } return ensurePrivateDirExists(mPreferencesDir); } }
有了 name.xml 文件,开始创建 SharedPreferencesImpl 对象,注意只是首次创建,后面不再创建,因为内存中有缓存该对象。
@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); 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"); } } // 创建 SharedPreferencesImpl 对象并缓存起来 sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } // 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; } // 创建缓存集合 sSharedPrefsCache,key 是包名,value 是以 file 为key,value 为 SharedPreferencesImpl 对象的嵌套 map 集合,也就是获取该当前包名下 ArrayMap<File, SharedPreferencesImpl>,file 和 SharedPreferencesImpl 一一对应。 @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; }
android N 以后不再支持 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE。
private void checkMode(int mode) {
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");
}
}
}
下面来看下 MODE_MULTI_PROCESS 这种扩进程方式是不安全的,在 MODE_MULTI_PROCESS 模式下,如果文件被修改过或重新加载,使得数据是最新的。
@UnsupportedAppUsage void startReloadIfChangedUnexpectedly() { synchronized (mLock) { // TODO: wait for any pending writes to disk? if (!hasFileChangedUnexpectedly()) { return; } startLoadFromDisk(); } } // 获取文件 mFile 的 StructStat 信息,根据文件的时间戳和文件大小判定是不是修改过,注意虽然加了 mLock,包括重新加载方法 startLoadFromDisk 也是加锁的,但是在多进程下,是有多个虚拟机的,加载对于多进程是无效的,所以对于多进程下使用 MODE_MULTI_PROCESS 是不安全的。 private boolean hasFileChangedUnexpectedly() { synchronized (mLock) { if (mDiskWritesInFlight > 0) { // If we know we caused it, it's not unexpected. if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected."); return false; } } final StructStat stat; try { BlockGuard.getThreadPolicy().onReadFromDisk(); stat = Os.stat(mFile.getPath()); } catch (ErrnoException e) { return true; } // synchronized (mLock) { return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size; } }
SharedPreferences 与 Editor 是两个接口,SharedPreferencesImpl 和 EditorImpl 分别实现了对应接口. SharedPreferencesImpl 提供查询各个类型数据的方法,getXX(String key, float defValue),而如果是存储数据和删除等操作,需要通过 edit() 方法获取 EditorImpl 实例来操作。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
这里主要做两件事,一个是先备份 name.xml 文件,备份文件名字是 name.bak。另一件是从文件中加载数据到内存中。下面主要看下加载过程。
// 通过 mLock 锁来确保 mLoaded 状态同步,然后开启一个线程来加载文件 @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; } 删除原有文件,并将备份文件重命名为 mFile if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } // Debugging if (mFile.exists() && !mFile.canRead()) { Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission"); } // 这段主要操作是通过流操作来读取 XML 文件,将里面的 key-value 读取到 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); } } } catch (ErrnoException e) { // An errno exception means the stat failed. Treat as empty/non-existing by // ignoring. } catch (Throwable t) { thrown = t; } // 读取完成后的操作 // 1.记录文件的操作时间和文件大小 // 2.读取完成后则通知其他等待的位置唤醒,继续执行操作,因为等待的地方是在等待文件加载完成,也就是说任何操作都是需要在文件加载完成的基础上执行的 synchronized (mLock) { mLoaded = true; mThrowable = thrown; // It's important that we always signal waiters, even if we'll make // them fail with an exception. The try-finally is pretty wide, but // better safe than sorry. try { if (thrown == null) { if (map != null) { mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } } // In case of a thrown exception, we retain the old map. That allows // any open editors to commit and store updates. } catch (Throwable t) { mThrowable = t; } finally { mLock.notifyAll(); } } }
文件未加载前任何操作都需要等待,getXX 方法和 edit 方法
@GuardedBy("mLock") private void awaitLoadedLocked() { if (!mLoaded) { // Raise an explicit StrictMode onReadFromDisk for this // thread, since the real read will be in a different // thread and otherwise ignored by StrictMode. BlockGuard.getThreadPolicy().onReadFromDisk(); } while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } if (mThrowable != null) { throw new IllegalStateException(mThrowable); } }
查询方法比较简单,文件加载完成后则直接从内存中读取数据,即从 Map 中查询数据,以 getString 方法为例:
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
注意,读取操作是同步操作,因为 mMap 中数据是有可能改变的,这里猜想一下,为什么不用 ConcurrentHashMap 呢?是不就可以不用加锁处理了?个人认为是可以的,但是有一点 ConcurrentHashMap 是在 jdk 5.0 才出现的,意味着之前的版本还是得用加锁处理,索性干脆直接加锁处理。这里除了加锁处理还有一个 awaitLoadedLocked() 方法,即上面提到的需要在文件加载完成才可以查询,否则会阻塞在这里。
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
同样需要等待文件加载完成,否则也会等待
EditorImpl 实现 Editor 接口,完成 SharePreferences 的添加、删除、清除、commit 和 apply 操作。EditorImpl 有两个变量 mModified 和 mClear。putXX,remove 操作都是基于 mModified 进行的,mClear 用于 clear 方法。
下面看下 putString 和 remove 方法,方法操作都加了所操作,保证数据的同步性,remove 方法有点特殊,待删除的值用 EditorImpl 自己来占位
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
调用 EditorImpl 的 putXX 等方法仅仅将要操作的元素放在 mModified,还没有和 SharePreferences 中的元素同步以及写入文件中,调用 commit 或者 apply 方法才会进行提交到内存并写入文件中。
commit 方法:
@Override public boolean commit() { long startTime = 0; // 提交到内存中,将 mModified 中数据和 SharePreferences 中 Map 同步 MemoryCommitResult mcr = commitToMemory(); // 开启写入文件任务,可以看到注释中提示 commit 方法是同步操作,即在当前线程直接指定写文件操作,这一点需要注意 SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); // 这里虽然调用了 await,并不会阻塞,因为 commit 方法是同步操作执行完成时 writtenToDiskLatch 中当前的 count 是 0,,会直接向下执行。 try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } notifyListeners(mcr); return mcr.writeToDiskResult; }
commit 是同步操作方法,也是耗时方法,毕竟需要进行写文件操作。执行分为两步:
- 将修改的数据提交到内存中 commitToMemory
- enqueueDiskWrite,执行写文件操作
// Returns true if any changes were made private MemoryCommitResult commitToMemory() { long memoryStateGeneration; boolean keysCleared = false; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; // 最终需要写入文件的数据 Map<String, Object> mapToWriteToDisk; synchronized (SharedPreferencesImpl.this.mLock) { // 如果正在写文件,则拷贝一份 mMap 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 在这里时代表的旧的数据 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; // 调用 EditorImpl 的 clear 方法会走这里 if (mClear) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } keysCleared = true; mClear = false; } // 这里遍历 EditorImpl 中的 mModified,并和 mapToWriteToDisk 中的数据对比,此时 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. // 注意这里的 this,上面提到过 this 在删除时用于占位,被删除的 key,则会从 mapToWriteToDisk 中删除 if (v == this || v == null) { if (!mapToWriteToDisk.containsKey(k)) { continue; } mapToWriteToDisk.remove(k); } else { // mapToWriteToDisk 中已经有了 mModified 中的元素,则不进行操作 if (mapToWriteToDisk.containsKey(k)) { Object existingValue = mapToWriteToDisk.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } // 否则不存在或者元素修改过,则添加 mapToWriteToDisk 中或者覆盖旧的元素 mapToWriteToDisk.put(k, v); } changesMade = true; if (hasListeners) { keysModified.add(k); } } mModified.clear(); if (changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified, listeners, mapToWriteToDisk); }
commitToMemory() 方法中代码虽然有点多,但是逻辑还是比较清晰的,主要的操作就是先将 SharePreferences 中 mMap 赋给 mapToWriteToDisk,此时 mapToWriteToDisk 中是修改前的元素,再和 EditorImpl 中的 mModified 中元素对比,得到最终的元素,这样就将修改结果同步到内存中了。
enqueueDiskWrite 是执行写文件的入口,commit 方法调用时传入的 postWriteRunnable 为 null,所以 isFromSyncCommit 为 true,代表同步操作,不需要提交到 QueuedWork 的队列中。
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { 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(); } } }; // Typical #commit() path with fewer allocations, doing a write on // the current thread. // 在 commitToMemory 中 mDiskWritesInFlight++,所以 对于 commit 方法,会直接调用 writeToDiskRunnable.run(),直接进行写文件操作 if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
写文件操作
@GuardedBy("mWritingToDiskLock") private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { long startTime = 0; long existsTime = 0; long backupExistsTime = 0; long outputStreamCreateTime = 0; long writeTime = 0; long fsyncTime = 0; long setPermTime = 0; long fstatTime = 0; long deleteTime = 0; 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; // Only need to write if the disk state is older than this commit // 当写入计数小于内存计数时才进行写入 if (mDiskStateGeneration < mcr.memoryStateGeneration) { // 如果是同步写,即调用 commit 则需要写入 if (isFromSyncCommit) { needsWrite = true; } else { // 调用 apply 方法时,需要保持 mcr 中数据和 SharePreferences 中的数据保持一致才进行写入,比如对于同一个sp文件,连续调用 n 次 apply ,就会有 n 次写入磁盘任务执行,实际上只需要最后执行最后那次就可以了,最后那次提交对应内存的 map 是持有最新的数据,所以就可以省掉前面 n-1 次的执行,这个就是 android 8.0 中做的优化 synchronized (mLock) { // No need to persist intermediate states. Just wait for the latest state to // be persisted. if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { needsWrite = true; } } } } // 不需要写入则直接放回 if (!needsWrite) { mcr.setDiskWriteResult(false, true); return; } // 备份文件,防止异常情况,写入成功后再删除 boolean backupFileExists = mBackupFile.exists(); 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(); } } // Attempt to write the file, delete the backup and return true as atomically as // possible. If any exception occurs, delete the new file; next time we will restore // from the backup. try { FileOutputStream str = createFileOutputStream(mFile); if (str == null) { mcr.setDiskWriteResult(false, false); return; } XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); writeTime = System.currentTimeMillis(); FileUtils.sync(str); fsyncTime = System.currentTimeMillis(); 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) { // Do nothing } // 写入成功,删除备份文件 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); } // 写入失败请款下则删除 mFile,下次从备份文件恢复 if (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false, false); }
写入文件主要是将 commitToMemory() 方法中的 mapToWriteToDisk 集合数据写入文件中,可以看出每次写入都是全量写入文件,如果写入成功则删除备份文件,如果写入失败则删除mFile.
可见, 每次commit是把全部数据更新到文件, 所以每个文件的数据量必须保证足够精简。每次修改数据进行一次性调用 commit,而且尽量不要在主线程中调用,因为 commit 方法是同步操作,写文件耗时操作会在当前线程中执行,在主线程调用可能会导致卡顿、ANR 等问题。
再来看看 apply 方法
@Override public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } 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); }
apply 方法中多了一个 awaitCommit,这是一个等待操作,android 系统会在 Activity 的 onStop ,onPause 等生命周期中,调用 QueuedWork.waitToFinish,等待落盘的任务队列执行完成,但是如果任务队列中的任务很多,或者待写入的数据量很大时(sp 文件是全量读写的),在一些 io 性能差的中低端机型上就会很容易出现 anr。
在 Android 8.0 下进行了优化,8.0 之前只会等待 awaitCommit,在低端机上多次调用 apply 时很容易出现 ANR,8.0 之后 会主动触发 processPendingWork 取出写任务列表中依次执行,而不是只在在等待。同时 8.0 之后对多次调用 apply 方法也进行了优化,比如对于同一个sp文件,连续调用 n 次 apply ,就会有 n 次写入磁盘任务执行,实际上只需要最后执行最后那次就可以了,最后那次提交对应内存的 map 是持有最新的数据,所以就可以省掉前面 n-1 次的执行,这个就是 android 8.0 中做的优化。
- 在于 apply 的写入文件操作是在单线程的线程池来完成。apply方法开始的时候, 会把 awaitCommit 放入 QueuedWork;文件写入操作完成, 则会把相应的 awaitCommit 从 QueuedWork 中移除。QueuedWork 在这里存在的价值主要是用于在 Stop Service, finish BroadcastReceiver 过程用于判定是否处理完所有的异步 SP 操作.
- apply没有返回值, commit 有返回值能知道修改是否提交成功
- apply是将修改提交到内存,再异步提交到磁盘文件; commit 是同步的提交到磁盘文件;
- 多并发的提交 commit 时,需等待正在处理的 commit 数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而 apply 只是原子更新到内存,后调用 apply 函数会直接覆盖前面内存数据,从一定程度上提高很多效率。
- 在工作线程中写入 sp 时,直接调用 commit 就可以,不必调用 apply,这种情况下,commit 开销更小
- 在主线程中写入 sp 时,不要调用 commit,要调用 apply
- sp 对应的文件尽量不要太大,按照模块创建不同的 sp 文件,而不是一个整个应用都读写一个 sp 文件。适当地拆分文件, 可以减少同步锁竞争,并提高写入效率。
- sp 适合读写轻量的、小的配置信息,不适合保存大数据量的信息,比如大的 json 字符串,有助于减少卡顿/anr
- 使用 SharedPreferences 是最好不要一上来就执行 getSharedPreferences().edit(),将 getSharedPreferences() 和 edit 过程拆分,从源码分析中可以看到,edit 会等待文件加载完成,此时会造成阻塞,容易导致卡顿等问题出现
- 当有连续的调用 putXX 方法操作时(特别是循环中),当确认不需要立即读取时,最后一次调用 commit 或 apply 即可
- 不要使用MODE_MULTI_PROCESS
在 8.0 以下没优化之前,如果防止等待任务导致的卡顿和 ANR 问题呢?要想解决的话,可以尝试清理锁队列,风险就是可能导致数据存储失败。
具体操作:
Activity 的 onStop,以及 Service 的 onStop 和 onStartCommand 都是通过 ActivityThread 触发的,ActivityThread 中有一个 Handler 变量,我们通过 Hook 拿到此变量,给此 Handler 设置一个 callback,Handler 的 dispatchMessage 中会先处理 callback。
try { Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentAtyThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); Object activityThread = currentAtyThreadMethod.invoke(null); Field mHField = activityThreadClass.getDeclaredField("mH"); mHField.setAccessible(true); Handler handler = (Handler) mHField.get(activityThread); Field mCallbackField = Handler.class.getDeclaredField("mCallback"); mCallbackField.setAccessible(true); mCallbackField.set(handler,new SpCompatCallback()); Log.d(TAG,"hook success"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (Throwable e){ e.printStackTrace(); }
自定义callbak:SpCompatCallback,在这个方法中做清理等待锁列表的操作:
public class SpCompatCallback implements Handler.Callback { public SpCompatCallback(){ } //handleServiceArgs private static final int SERVICE_ARGS = 115; //handleStopService private static final int STOP_SERVICE = 116; //handleSleeping private static final int SLEEPING = 137; //handleStopActivity private static final int STOP_ACTIVITY_SHOW = 103; //handleStopActivity private static final int STOP_ACTIVITY_HIDE = 104; //handlePauseActivity private static final int PAUSE_ACTIVITY = 101; //handlePauseActivity private static final int PAUSE_ACTIVITY_FINISHING = 102; @Override public boolean handleMessage(Message msg) { switch (msg.what){ case SERVICE_ARGS: SpHelper.beforeSpBlock("SERVICE_ARGS"); break; case STOP_SERVICE: SpHelper.beforeSpBlock("STOP_SERVICE"); break; case SLEEPING: SpHelper.beforeSpBlock("SLEEPING"); break; case STOP_ACTIVITY_SHOW: SpHelper.beforeSpBlock("STOP_ACTIVITY_SHOW"); break; case STOP_ACTIVITY_HIDE: SpHelper.beforeSpBlock("STOP_ACTIVITY_HIDE"); break; case PAUSE_ACTIVITY: SpHelper.beforeSpBlock("PAUSE_ACTIVITY"); break; case PAUSE_ACTIVITY_FINISHING: SpHelper.beforeSpBlock("PAUSE_ACTIVITY_FINISHING"); break; default: break; } return false; } }
清理操作实际上是反射调用 sPendingWorkFinishers.clear();
public class SpHelper { private static final String TAG = "SpHelper"; private static boolean init = false; private static String CLASS_QUEUED_WORK = "android.app.QueuedWork"; private static String FIELD_PENDING_FINISHERS = "sPendingWorkFinishers"; private static ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null; public static void beforeSpBlock(String tag){ if(!init){ getPendingWorkFinishers(); init = true; } Log.d(TAG,"beforeSpBlock "+tag); if(sPendingWorkFinishers != null){ sPendingWorkFinishers.clear(); } } private static void getPendingWorkFinishers() { Log.d(TAG,"getPendingWorkFinishers"); try { Class clazz = Class.forName(CLASS_QUEUED_WORK); Field field = clazz.getDeclaredField(FIELD_PENDING_FINISHERS); field.setAccessible(true); sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null); Log.d(TAG,"getPendingWorkFinishers success"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (Throwable e){ e.printStackTrace(); } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。