当前位置:   article > 正文

Android 性能优化篇之SharedPreferences使用优化_安卓开发sp的优化

安卓开发sp的优化

简介: `SharedPreferences(以下简称SP)`是Android本地存储的一种方式,是以`key-value`的形式存储在`/data/data/项目包名/shared_prefs/sp_name.xml`里

SP的使用及存在的问题

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()的流程。

apply()流程分析

写文件流程( 8.0以上)

apply()为什么还会出现ANR呢?我们来看下apply()的逻辑(这里源码是看的API30的):

  1. //SharedPreferencesImpl.EditorImpl.java
  2. @Override
  3. public void apply() {
  4. final long startTime = System.currentTimeMillis();
  5. //写入内存
  6. final MemoryCommitResult mcr = commitToMemory();
  7. //这里的操作使用CountDownLatch实现等待效果
  8. final Runnable awaitCommit = new Runnable() {
  9. @Override
  10. public void run() {
  11. try {
  12. //writtenToDiskLatch类型是CountDownLatch(1)
  13. mcr.writtenToDiskLatch.await();
  14. } catch (InterruptedException ignored) {
  15. }
  16. }
  17. }
  18. };
  19. //将awaitCommit加入队列中,后续Activity的onStop()中即会执行这个Runnable等待
  20. QueuedWork.addFinisher(awaitCommit);
  21. Runnable postWriteRunnable = new Runnable() {
  22. @Override
  23. public void run() {
  24. awaitCommit.run();
  25. QueuedWork.removeFinisher(awaitCommit);
  26. }
  27. };
  28. //文件写入操作
  29. SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  30. }

QueuedWork.addFinisher(awaitCommit)awaitCommit加入到队列中,awaitCommit在执行时利用CountDownLatch机制可以实现对当前线程的阻塞效果,后续ActivityonStop()中会将这里的awaitCommit取出来执行,即UI线程会阻塞等待sp文件写入磁盘。

  1. //SharedPreferencesImpl.java
  2. private void enqueueDiskWrite(final MemoryCommitResult mcr,
  3. final Runnable postWriteRunnable) {
  4. final boolean isFromSyncCommit = (postWriteRunnable == null);
  5. final Runnable writeToDiskRunnable = new Runnable() {
  6. @Override
  7. public void run() {
  8. synchronized (mWritingToDiskLock) {
  9. //写入磁盘操作
  10. writeToFile(mcr, isFromSyncCommit);
  11. }
  12. synchronized (mLock) {
  13. mDiskWritesInFlight--;
  14. }
  15. if (postWriteRunnable != null) {
  16. postWriteRunnable.run();
  17. }
  18. }
  19. };
  20. //commit()会在当前线程进行写入操作
  21. if (isFromSyncCommit) {
  22. boolean wasEmpty = false;
  23. synchronized (mLock) {
  24. wasEmpty = mDiskWritesInFlight == 1;
  25. }
  26. if (wasEmpty) {
  27. writeToDiskRunnable.run();
  28. return;
  29. }
  30. }
  31. QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
  32. }
  1. //QueuedWork.java
  2. public static void queue(Runnable work, boolean shouldDelay) {
  3. Handler handler = getHandler();
  4. synchronized (sLock) {
  5. sWork.add(work);
  6. if (shouldDelay && sCanDelay) {
  7. handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
  8. } else {
  9. handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
  10. }
  11. }
  12. }
  13. //构造一个Handler并传入HandlerThread的Looper,即Handler会在子线程中处理消息
  14. private static Handler getHandler() {
  15. synchronized (sLock) {
  16. if (sHandler == null) {
  17. HandlerThread handlerThread = new HandlerThread("queued-work-looper",
  18. Process.THREAD_PRIORITY_FOREGROUND);
  19. handlerThread.start();
  20. sHandler = new QueuedWorkHandler(handlerThread.getLooper());
  21. }
  22. return sHandler;
  23. }
  24. }
  25. private static class QueuedWorkHandler extends Handler {
  26. static final int MSG_RUN = 1;
  27. QueuedWorkHandler(Looper looper) {
  28. super(looper);
  29. }
  30. public void handleMessage(Message msg) {
  31. if (msg.what == MSG_RUN) {
  32. processPendingWork();
  33. }
  34. }
  35. }
  36. private static void processPendingWork() {
  37. synchronized (sProcessingWork) {
  38. LinkedList<Runnable> work;
  39. synchronized (sLock) {
  40. work = (LinkedList<Runnable>) sWork.clone();
  41. sWork.clear();
  42. // Remove all msg-s as all work will be processed now
  43. getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
  44. }
  45. if (work.size() > 0) {
  46. //取出Runnable并执行
  47. for (Runnable w : work) {
  48. w.run();
  49. }
  50. }
  51. }
  52. }

apply()写入操作是通过SharedPreferencesImpl#enqueueDiskWrite()HandlerThread构造的子线程中完成的,写入成功后会通过writtenToDiskLatch.countDown()释放awaitCommit中的锁,使得UI线程恢复执行。

QueuedWork.waitToFinish ( 8.0以上)

Activity的onStop()Service的onDestroy()执行时,都会调用到QueuedWork.waitToFinish()方法:

  1. //ActivityThread.java
  2. private void handleStopService(IBinder token) {
  3. Service s = mServices.remove(token);
  4. if (s != null) {
  5. try {
  6. if (localLOGV) Slog.v(TAG, "Destroying service " + s);
  7. s.onDestroy();
  8. s.detachAndCleanUp();
  9. //看这里 看这里!!!
  10. QueuedWork.waitToFinish();
  11. //...其他代码...
  12. } catch (Exception e) {
  13. }
  14. }
  15. }
  16. @Override
  17. public void handleStopActivity(IBinder token, int configChanges,
  18. PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
  19. final ActivityClientRecord r = mActivities.get(token);
  20. r.activity.mConfigChangeFlags |= configChanges;
  21. final StopInfo stopInfo = new StopInfo();
  22. performStopActivityInner(r, stopInfo, true /* saveState */, finalStateRequest,
  23. reason);
  24. //大于API11的时候执行
  25. if (!r.isPreHoneycomb()) {
  26. //看这里 看这里!!!
  27. QueuedWork.waitToFinish();
  28. }
  29. //......
  30. }

Activity的onStop()Service中的onDestroy()都是间接在ActivityThread中的handleStopService()、handleStopActivity()执行的,这两个方法里都会执行到QueuedWork.waitToFinish()

  1. public static void waitToFinish() {
  2. long startTime = System.currentTimeMillis();
  3. boolean hadMessages = false;
  4. Handler handler = getHandler();
  5. synchronized (sLock) {
  6. if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
  7. // Delayed work will be processed at processPendingWork() below
  8. handler.removeMessages(QueuedWorkHandler.MSG_RUN);
  9. }
  10. // We should not delay any work as this might delay the finishers
  11. sCanDelay = false;
  12. }
  13. StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
  14. try {
  15. //把任务取出来,直接在当前线程处理 8.0之后才有的逻辑
  16. processPendingWork();
  17. } finally {
  18. StrictMode.setThreadPolicy(oldPolicy);
  19. }
  20. try {
  21. while (true) {
  22. Runnable finisher;
  23. synchronized (sLock) {
  24. //重点 看这里 看这里!!!
  25. finisher = sFinishers.poll();
  26. }
  27. if (finisher == null) {
  28. break;
  29. }
  30. finisher.run();
  31. }
  32. } finally {
  33. sCanDelay = true;
  34. }
  35. }
  36. }

这里的sFinishers中取的Runnable就是在写文件之前通过QueuedWork.addFinisher(awaitCommit)添加的,当取出awaitCommit执行时即会阻塞当前线程,如果apply()中写入磁盘时间过长导致awaitCommit的锁没有及时释放,UI线程就会因为长时间被阻塞得不到执行而出现ANR了。

用一张图来总结:

图片来自:今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待

写文件流程( 8.0以下)

  1. public void apply() {
  2. final MemoryCommitResult mcr = commitToMemory();
  3. //这里的操作时为了CountDownLatch实现等待效果
  4. final Runnable awaitCommit = new Runnable() {
  5. public void run() {
  6. try {
  7. mcr.writtenToDiskLatch.await();
  8. } catch (InterruptedException ignored) {
  9. }
  10. }
  11. };
  12. QueuedWork.add(awaitCommit);
  13. Runnable postWriteRunnable = new Runnable() {
  14. public void run() {
  15. awaitCommit.run();
  16. QueuedWork.remove(awaitCommit);
  17. }
  18. };
  19. SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  20. }

QueuedWork.waitToFinish ( 8.0以下)

  1. public static void waitToFinish() {
  2. Runnable toFinish;
  3. while ((toFinish = sPendingWorkFinishers.poll()) != null) {
  4. toFinish.run();
  5. }
  6. }

8.0以下的流程相对更简单一些,但核心流程是一样的,当在UI线程中调用到QueuedWork.waitToFinish()时,如果写入磁盘的操作还未完成且耗时比较长,就会引起UI线程ANR

综述,使用apply()依然有可能会造成ANR问题。

如何优化

Jetpack DataStore替代

Jetpack DataStore 是一种改进的新数据存储解决方案,允许使用协议缓冲区存储键值对或类型化对象。DataStore 以异步、一致的事务方式存储数据,克服了 SharedPreferences(以下统称为SP)的一些缺点DataStore基于Kotlin协程和Flow实现,并且可以对SP数据进行迁移,旨在取代SP

DataStore提供了两种不同的实现:Preferences DataStoreProto 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以下处理不一样):
  1. object SPHook {
  2. fun optimizeSpTask() {
  3. if (Build.VERSION.SDK_INT < 26) {
  4. reflectSPendingWorkFinishers()
  5. } else {
  6. reflectSFinishers()
  7. }
  8. }
  9. /**
  10. * 8.0以上 Reflect finishers
  11. *
  12. */
  13. private fun reflectSFinishers() {
  14. try {
  15. val clz = Class.forName("android.app.QueuedWork")
  16. val field = clz.getDeclaredField("sFinishers")
  17. field.isAccessible = true
  18. val queue = field.get(clz) as? LinkedList<Runnable>
  19. if (queue != null) {
  20. val linkedListProxy = LinkedListProxy(queue)
  21. field.set(queue, linkedListProxy)
  22. log("hook success")
  23. }
  24. } catch (ex: Exception) {
  25. log("hook error:${ex}")
  26. }
  27. }
  28. /**
  29. * 8.0以下 Reflect pending work finishers
  30. */
  31. private fun reflectSPendingWorkFinishers() {
  32. try {
  33. val clz = Class.forName("android.app.QueuedWork")
  34. val field = clz.getDeclaredField("sPendingWorkFinishers")
  35. field.isAccessible = true
  36. val queue = field.get(clz) as? ConcurrentLinkedQueue<Runnable>
  37. if (queue != null) {
  38. val proxy = ConcurrentLinkedQueueProxy(queue)
  39. field.set(queue, proxy)
  40. log("hook success")
  41. }
  42. } catch (ex: Exception) {
  43. log("hook error:${ex}")
  44. }
  45. }
  46. /**
  47. * 在8.0以上apply()中QueuedWork.addFinisher(awaitCommit), 需要代理的是LinkedList,如下:
  48. * # private static final LinkedList<Runnable> sFinishers = new LinkedList<>()
  49. */
  50. private class LinkedListProxy(private val sFinishers: LinkedList<Runnable>) :
  51. LinkedList<Runnable>() {
  52. override fun add(element: Runnable): Boolean {
  53. return sFinishers.add(element)
  54. }
  55. override fun remove(element: Runnable): Boolean {
  56. return sFinishers.remove(element)
  57. }
  58. override fun isEmpty(): Boolean = true
  59. /**
  60. * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了
  61. */
  62. override fun poll(): Runnable? {
  63. return null
  64. }
  65. }
  66. /**
  67. * 在8.0以下代理
  68. * // The set of Runnables that will finish or wait on any async activities started by the application.
  69. * private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>();
  70. */
  71. private class ConcurrentLinkedQueueProxy(private val sPendingWorkFinishers: ConcurrentLinkedQueue<Runnable>) :
  72. ConcurrentLinkedQueue<Runnable>() {
  73. override fun add(element: Runnable?): Boolean {
  74. return sPendingWorkFinishers.add(element)
  75. }
  76. override fun remove(element: Runnable?): Boolean {
  77. return sPendingWorkFinishers.remove(element)
  78. }
  79. override fun isEmpty(): Boolean = true
  80. /**
  81. * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了
  82. */
  83. override fun poll(): Runnable? {
  84. return null
  85. }
  86. }
  87. }

注: Android P(9.0) 中引入了对隐藏API的限制,因为这里是hook的源码,所以如果后续版本如果也被标记为隐藏API,可能会导致反射失败。找到一个绕过隐藏API限制的库:https://github.com/LSPosed/AndroidHiddenApiBypass,原理是通过Unsafe去操作的,后续可以考虑通过这种方式去突破限制。

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AI数据灵魂/article/detail/60775
推荐阅读
相关标签
  

闽ICP备14008679号