赞
踩
简介: `SharedPreferences(以下简称SP)`是Android本地存储的一种方式,是以`key-value`的形式存储在`/data/data/项目包名/shared_prefs/sp_name.xml`里
SharedPreferences(以下简称SP)
是Android本地存储的一种方式,是以key-value
的形式存储在/data/data/项目包名/shared_prefs/sp_name.xml
里,SP
的使用示例及源码解析参见:Android本地存储之SharedPreferences源码解析。以下是SP
的一些结论:
SharedPreferences
读取xml
文件时,会以DOM
方式解析(把整个xml
文件直接加载到内存中解析),在调用getXXX()
方法时取到的是内存中的数据,方法执行时会有个锁来阻塞,目的是等待文件加载完毕,没加载完成之前会wait()
。SP
第一次初始化到读取到数据存在一定延迟,因为需要到文件中读取数据,因此可能会对UI
线程流畅度造成一定影响,严重情况下会产生ANR
。SharedPreferences
写文件时,如果调用的commit()
,会将数据同步写入内存中,内存数据更新,再同步写入磁盘中; 如果调用的apply()
,会将数据同步写入内存中,内存数据更新,然后异步写人磁盘,也就是说可能写磁盘操作还没有完成就直接返回了。在UI线程
中建议使用apply()
,因为同步写磁盘,当文件较大时,commit()
会等到写磁盘完成再返回,可能会有ANR
问题。apply()
方法,依然有可能会造成ANR
问题,这是为什么呢?先看下apply()
的流程。写文件流程( 8.0以上)
apply()
为什么还会出现ANR
呢?我们来看下apply()
的逻辑(这里源码是看的API30
的):
- //SharedPreferencesImpl.EditorImpl.java
- @Override
- public void apply() {
- final long startTime = System.currentTimeMillis();
- //写入内存
- final MemoryCommitResult mcr = commitToMemory();
- //这里的操作使用CountDownLatch实现等待效果
- final Runnable awaitCommit = new Runnable() {
- @Override
- public void run() {
- try {
- //writtenToDiskLatch类型是CountDownLatch(1)
- mcr.writtenToDiskLatch.await();
- } catch (InterruptedException ignored) {
- }
- }
- }
- };
- //将awaitCommit加入队列中,后续Activity的onStop()中即会执行这个Runnable等待
- QueuedWork.addFinisher(awaitCommit);
-
- Runnable postWriteRunnable = new Runnable() {
- @Override
- public void run() {
- awaitCommit.run();
- QueuedWork.removeFinisher(awaitCommit);
- }
- };
- //文件写入操作
- SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
QueuedWork.addFinisher(awaitCommit)
将awaitCommit
加入到队列中,awaitCommit
在执行时利用CountDownLatch
机制可以实现对当前线程的阻塞效果,后续Activity
的onStop()
中会将这里的awaitCommit
取出来执行,即UI线程
会阻塞等待sp文件
写入磁盘。
- //SharedPreferencesImpl.java
- 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();
- }
- }
- };
-
- //commit()会在当前线程进行写入操作
- if (isFromSyncCommit) {
- boolean wasEmpty = false;
- synchronized (mLock) {
- wasEmpty = mDiskWritesInFlight == 1;
- }
- if (wasEmpty) {
- writeToDiskRunnable.run();
- return;
- }
- }
-
- QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
- //QueuedWork.java
- 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并传入HandlerThread的Looper,即Handler会在子线程中处理消息
- 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 {
- static final int MSG_RUN = 1;
-
- QueuedWorkHandler(Looper looper) {
- super(looper);
- }
-
- public void handleMessage(Message msg) {
- if (msg.what == MSG_RUN) {
- processPendingWork();
- }
- }
- }
-
- private static void processPendingWork() {
- 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) {
- //取出Runnable并执行
- for (Runnable w : work) {
- w.run();
- }
- }
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
apply()
写入操作是通过SharedPreferencesImpl#enqueueDiskWrite()
在HandlerThread
构造的子线程中完成的,写入成功后会通过writtenToDiskLatch.countDown()
释放awaitCommit
中的锁,使得UI
线程恢复执行。
QueuedWork.waitToFinish ( 8.0以上)
Activity的onStop()
、Service的onDestroy()
执行时,都会调用到QueuedWork.waitToFinish()
方法:
- //ActivityThread.java
- private void handleStopService(IBinder token) {
- Service s = mServices.remove(token);
- if (s != null) {
- try {
- if (localLOGV) Slog.v(TAG, "Destroying service " + s);
- s.onDestroy();
- s.detachAndCleanUp();
- //看这里 看这里!!!
- QueuedWork.waitToFinish();
- //...其他代码...
- } catch (Exception e) {
- }
- }
- }
-
- @Override
- public void handleStopActivity(IBinder token, 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, true /* saveState */, finalStateRequest,
- reason);
- //大于API11的时候执行
- if (!r.isPreHoneycomb()) {
- //看这里 看这里!!!
- QueuedWork.waitToFinish();
- }
- //......
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
Activity的onStop()
、Service中的onDestroy()
都是间接在ActivityThread
中的handleStopService()、handleStopActivity()
执行的,这两个方法里都会执行到QueuedWork.waitToFinish()
:
- public static void waitToFinish() {
- long startTime = System.currentTimeMillis();
- boolean hadMessages = false;
-
- Handler handler = getHandler();
-
- synchronized (sLock) {
- if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
- // Delayed work will be processed at processPendingWork() below
- handler.removeMessages(QueuedWorkHandler.MSG_RUN);
- }
-
- // We should not delay any work as this might delay the finishers
- sCanDelay = false;
- }
-
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
- try {
- //把任务取出来,直接在当前线程处理 8.0之后才有的逻辑
- processPendingWork();
- } finally {
- StrictMode.setThreadPolicy(oldPolicy);
- }
-
- try {
- while (true) {
- Runnable finisher;
-
- synchronized (sLock) {
- //重点 看这里 看这里!!!
- finisher = sFinishers.poll();
- }
-
- if (finisher == null) {
- break;
- }
- finisher.run();
- }
- } finally {
- sCanDelay = true;
- }
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
这里的sFinishers
中取的Runnable
就是在写文件之前通过QueuedWork.addFinisher(awaitCommit)
添加的,当取出awaitCommit
执行时即会阻塞当前线程,如果apply()
中写入磁盘时间过长导致awaitCommit
的锁没有及时释放,UI线程
就会因为长时间被阻塞得不到执行而出现ANR
了。
用一张图来总结:
图片来自:今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
写文件流程( 8.0以下)
- public void apply() {
- final MemoryCommitResult mcr = commitToMemory();
- //这里的操作时为了CountDownLatch实现等待效果
- final Runnable awaitCommit = new Runnable() {
- public void run() {
- try {
- mcr.writtenToDiskLatch.await();
- } catch (InterruptedException ignored) {
- }
- }
- };
-
- QueuedWork.add(awaitCommit);
-
- Runnable postWriteRunnable = new Runnable() {
- public void run() {
- awaitCommit.run();
- QueuedWork.remove(awaitCommit);
- }
- };
-
- SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
QueuedWork.waitToFinish ( 8.0以下)
- public static void waitToFinish() {
- Runnable toFinish;
- while ((toFinish = sPendingWorkFinishers.poll()) != null) {
- toFinish.run();
- }
- }
8.0以下
的流程相对更简单一些,但核心流程是一样的,当在UI线程
中调用到QueuedWork.waitToFinish()
时,如果写入磁盘的操作还未完成且耗时比较长,就会引起UI线程
的ANR
。
综述,使用apply()
依然有可能会造成ANR
问题。
Jetpack DataStore替代
Jetpack DataStore
是一种改进的新数据存储解决方案,允许使用协议缓冲区存储键值对或类型化对象。DataStore
以异步、一致的事务方式存储数据,克服了 SharedPreferences(以下统称为SP)的一些缺点。DataStore
基于Kotlin
协程和Flow
实现,并且可以对SP
数据进行迁移,旨在取代SP
。
DataStore
提供了两种不同的实现:Preferences DataStore
与Proto DataStore
,其中Preferences DataStore
用于存储键值对;Proto DataStore
用于存储类型化对象,DataStore
更详细的介绍参见:Android Jetpack系列之DataStore
MMKV替代
MMKV
是基于 mmap
内存映射的 key-value
组件,底层序列化/反序列化使用 protobuf
实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX
平台,一并开源。
注:mmap
内存映射,可以提供一段可供随时写入的内存块,App
只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash
导致数据丢失。
MMKV
地址:https://github.com/tencent/mmkv
SP使用优化
SP
文件尽量按分类去加载存储,如果文件里存储的K-V
数据过多,会导致第一次加载时间过长;另外新增一个K-V
时,写入磁盘是全量更新,即会把之前的文件再次更新一遍,所以也要求SP
使用时尽量分类加载存储。UI线程
中执行QueuedWork.waitToFinish()
,当队列执行poll()
时,通过反射修改poll()
的返回值,将其设为null
,这样UI线程会继续往下执行而不会原地阻塞等待了。示例如下(注意8.0以上
与8.0以下
处理不一样):- object SPHook {
-
- fun optimizeSpTask() {
- if (Build.VERSION.SDK_INT < 26) {
- reflectSPendingWorkFinishers()
- } else {
- reflectSFinishers()
- }
- }
-
- /**
- * 8.0以上 Reflect finishers
- *
- */
- private fun reflectSFinishers() {
- try {
- val clz = Class.forName("android.app.QueuedWork")
- val field = clz.getDeclaredField("sFinishers")
- field.isAccessible = true
- val queue = field.get(clz) as? LinkedList<Runnable>
- if (queue != null) {
- val linkedListProxy = LinkedListProxy(queue)
- field.set(queue, linkedListProxy)
- log("hook success")
- }
- } catch (ex: Exception) {
- log("hook error:${ex}")
- }
- }
-
- /**
- * 8.0以下 Reflect pending work finishers
- */
- private fun reflectSPendingWorkFinishers() {
- try {
- val clz = Class.forName("android.app.QueuedWork")
- val field = clz.getDeclaredField("sPendingWorkFinishers")
- field.isAccessible = true
- val queue = field.get(clz) as? ConcurrentLinkedQueue<Runnable>
- if (queue != null) {
- val proxy = ConcurrentLinkedQueueProxy(queue)
- field.set(queue, proxy)
- log("hook success")
- }
- } catch (ex: Exception) {
- log("hook error:${ex}")
- }
- }
-
- /**
- * 在8.0以上apply()中QueuedWork.addFinisher(awaitCommit), 需要代理的是LinkedList,如下:
- * # private static final LinkedList<Runnable> sFinishers = new LinkedList<>()
- */
- private class LinkedListProxy(private val sFinishers: LinkedList<Runnable>) :
- LinkedList<Runnable>() {
-
- override fun add(element: Runnable): Boolean {
- return sFinishers.add(element)
- }
-
- override fun remove(element: Runnable): Boolean {
- return sFinishers.remove(element)
- }
-
- override fun isEmpty(): Boolean = true
-
- /**
- * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了
- */
- override fun poll(): Runnable? {
- return null
- }
- }
-
- /**
- * 在8.0以下代理
- * // The set of Runnables that will finish or wait on any async activities started by the application.
- * private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>();
- */
-
- private class ConcurrentLinkedQueueProxy(private val sPendingWorkFinishers: ConcurrentLinkedQueue<Runnable>) :
- ConcurrentLinkedQueue<Runnable>() {
-
- override fun add(element: Runnable?): Boolean {
- return sPendingWorkFinishers.add(element)
- }
-
- override fun remove(element: Runnable?): Boolean {
- return sPendingWorkFinishers.remove(element)
- }
-
- override fun isEmpty(): Boolean = true
-
- /**
- * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了
- */
- override fun poll(): Runnable? {
- return null
- }
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
注: Android P(9.0)
中引入了对隐藏API
的限制,因为这里是hook
的源码,所以如果后续版本如果也被标记为隐藏API
,可能会导致反射失败。找到一个绕过隐藏API
限制的库:https://github.com/LSPosed/AndroidHiddenApiBypass
,原理是通过Unsafe
去操作的,后续可以考虑通过这种方式去突破限制。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。