SharedPreferences卡顿分析

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()的流程。
SharedPreferencesImpl#apply()流程分析(基于8.0以上版本)
SharedPreferencesImpl$EditorImpl
@Override
public void apply() {final long startTime = System.currentTimeMillis();// 写入内存(更新修改的字段)final MemoryCommitResult mcr = commitToMemory();// 使用CountDownLatch实现等待写入文件操作完成final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {// writtenToDiskLatch初始化为CountDownLatch(1)mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}}};// 将awaitCommit添加到等待队列中,后续Activity/Servicede的onStop()会执行该Runnable等待文件写入完成QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};// 将待写入文件的集合添加到工作任务队列中SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);notifyListeners(mcr);
}

QueuedWork.addFinisher(awaitCommit)awaitCommit加入到等待队列中,awaitCommit在执行时利用CountDownLatch机制可以实现对当前线程的阻塞效果,后续ActivityonStop()中会将这里的awaitCommit取出来执行,即UI线程会阻塞等待sp文件写入磁盘,写入操作是通过SharedPreferencesImpl#enqueueDiskWrite()完成的,写入成功后会通过writtenToDiskLatch.countDown()释放awaitCommit中的锁,如果写入操作比较耗时,就会造成ANR问题。

SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic 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);
}

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) {// (1) 消息队列的工作线程中执行processPendingWork();}}
}// 该方法存在两种执行路径: (1)在消息队列对应的工作线程中执行、(2)当前线程执行(执行前会将任务队列克隆并清空)
private static void processPendingWork() {synchronized (sProcessingWork) {LinkedList<Runnable> work;synchronized (sLock) {// a. 拷贝工作队列中的任务集合,然后将原任务集合清理,当(2)场景主线程执行到这里时因为集合没有任务直接跳过,进入等待写入磁盘任务完成work = (LinkedList<Runnable>) sWork.clone();sWork.clear();// b. 移除队列中的所有消息,下面立即处理getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);}if (work.size() > 0) {// 取出Runnable并执行for (Runnable w : work) {w.run();}}}
}

QueuedWork.waitToFinish

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();}//......
}

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() belowhandler.removeMessages(QueuedWorkHandler.MSG_RUN);}// We should not delay any work as this might delay the finisherssCanDelay = false;}StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();try {// (2) 把任务取出来,直接在当前线程处理文件操作 8.0之后的逻辑(文件操作容易导致anr),因为之前清理任务集合,这里可能会立即执行完成进入下面执行等待状态processPendingWork();} finally {StrictMode.setThreadPolicy(oldPolicy);}try {while (true) {Runnable finisher;synchronized (sLock) {// 重点finisher = sFinishers.poll();}if (finisher == null) {break;}finisher.run();}} finally {sCanDelay = true;}}
}

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

总结如下图:

图片来自:今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待,所以结论是:使用apply()依然有可能会造成ANR问题。

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);
}

QueuedWork.waitToFinish()

public static void waitToFinish() {Runnable toFinish;while ((toFinish = sPendingWorkFinishers.poll()) != null) {toFinish.run();}
}

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

apply()使用优化

主要是优化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 = trueval 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 = trueval 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}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/654582.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JAVA语言-第16话】集合框架(三)——Set、HashSet、LinkedHashSet、TreeSet集合的详细解析

目录 Set集合 1.1 概述 1.2 特点 1.3 HashSet集合 1.3.1 概述 1.3.2 哈希表 1.3.3 哈希值 1.3.4 练习 1.3.5 HashSet存储自定义类型元素 1.4 LinkedHashSet集合 1.4.1 概述 1.4.2 特点 1.4.3 练习 1.5 TreeSet集合 1.5.1 概述 1.5.2 练习 1.6 HashSet、Lin…

Pandas展开数据

def testExplode():df = pd.DataFrame({key: [A, B],data: [[1

【论文复现】

code&#xff1a; paper&#xff1a; 论文 介绍 方法 实验 结论 复现 Image generation 问题1&#xff1a;No models "dcface/dcface/pretrained_models/adaface_ir101_webface4m.ckpt Traceback (most recent call last):File "/data/dcface/dcface/src/r…

线性回归需要满足的几个假设

线性回归模型是基于一些假设构建的&#xff0c;这些假设有助于确保模型的有效性和可解释性。以下是线性回归需要满足的几个主要假设&#xff1a; 线性关系假设&#xff08;Linearity&#xff09;: 线性回归假设因变量&#xff08;目标变量&#xff09;与自变量&#xff08;特征…

如何通俗解释Docker是什么?

要想弄懂Docker&#xff0c;咱们得先从“容器化”讲起。 一、容器化技术及Docker的出现 容器化&#xff0c;它是一种轻量级、可移植的软件打包方式&#xff0c;你就想象成一个快递箱子&#xff0c;里面装着你的应用和所有需要运行的环境&#xff0c;这个箱子能在任何支持容器…

Redisson分布式锁介绍及实战应用(防止缓存击穿)

本地锁 浏览器把100w请求由网关随机往下传&#xff0c;在集群情况下&#xff0c;每台服务都放行10w请求过来&#xff0c;这时候每台服务都用的是本地锁是跨JVM的&#xff0c; 列如这些服务都没有49企业&#xff0c;此时有几个服务进行回原了打击在DB上面&#xff0c;那后期把这…

Blender教程(基础)-物体的移动、旋转与缩放-04

一、新建一个立方体 ShiftA新建一个立方体用来演示。 二、物体的移动 xyz轴移动 点击下图图左侧的移动选项后&#xff0c;选中要移动的物体&#xff0c;会出现三个箭头的方向&#xff0c;这分别代表沿着x、y、z轴移动。xyz平面移动 这个小正方体代表沿着某一个面移动&#…

AWS 专题学习 P14 (Security Encryption)

文章目录 专题总览为什么需要加密&#xff1f;AWS KMS&#xff08;密钥管理服务&#xff09;KMS 密钥类型AWS KMS&#xff08;密钥管理服务&#xff09;Copying Snapshots across regionsKMS Key Policies在不同账户之间复制快照KMS Multi-Region Keys (多区域密钥)DynamoDB 全…

ElasticSearch 学习笔记

基本概念 术语 文档&#xff08;document&#xff09;&#xff1a;每条记录就是一个文档&#xff0c;会以 JSON 格式进行存储 映射&#xff08;mapping&#xff09;&#xff1a;索引中文档字段的约束信息&#xff0c;类似 RDBMS 中的表结构约束&#xff08;schema&#xff09…

在linux上进行编译调试

1.相关疑问 1. 为什么在代码里使用了一个未定义过的函数&#xff08;如add()&#xff09;&#xff0c;在编译阶段不会报错&#xff0c;在链接阶段会报错呢&#xff1f; 答&#xff1a;先说几个代码编译的结论&#xff1a; 单个\.c源文件文件被编译成机器码文件时&#xff0c…

LVS 工作模式

1、LVS DR模式 DR 模式是通过改写请求报文的目标 MAC 地址&#xff0c;将请求发给真实服务器的&#xff0c;而真实服务器响应后的处理结果直接返回给客户端用户。DR 模式可以极大的提高集群系统的伸缩性。但是要求调度器 LB 与真实服务器 RS 都有一块网卡连接到同一物理网段上…

Codeforces Round 785 C. Palindrome Basis

C. Palindrome Basis 题意 定义一个正整数 a a a 是回文的&#xff08;没有前导 0 0 0&#xff09;当且仅当&#xff1a; a a a 的十进制表示形式回文 给定一个正整数 n n n &#xff0c;求出将 n n n 拆分成若干个回文数之和的方案数 思路 这是一个经典模型&#xff0…

媒体邀约:怎么吸引总体目标受众?

新闻媒体影响力日益扩大。不论是公司、机构还是其他&#xff0c;都希望能够通过新闻媒体的曝光来吸引更多总体目标受众。要想真正吸引住总体目标受众并非易事&#xff0c;需要一定的方案和方法。下面我们就深入探究媒体邀约推广的真相&#xff0c;共享怎么吸引总体目标受众的方…

秋招面试—计算机网络安全

2021 计算机网络安全 1.Get 和 Post 的区别 get 用于获取数据&#xff0c;post用于提交数据&#xff1b; get 的缓存保存在浏览器和web服务器日志中&#xff1b; get 使用明文传输&#xff0c;post请求保存在请求体中&#xff1b; get 长度限制在2048以内 2.常见的HTTP请…

Android P 屏保和休眠相关知识

Android P添加屏保功能&#xff0c;如果休眠时间设定大于屏保时间&#xff0c;则先进入屏保&#xff0c;达到休眠时间后再进入休眠 需求&#xff1a; 添加屏幕互保开关&#xff0c;默认关闭。只保留时钟&#xff0c;可设定指针和数字、夜间模式。启用时间改多长时间无操作进入…

小程序和vue/react的区别/优势

小程序优势&#xff1a; 1.即开即走&#xff0c;不需要下载app&#xff0c;可以通过微信直接打开更加方便快捷 2.天然流量优势&#xff0c;依赖于微信这个大量流量平台 3.相比于app的开发 小程序的开发是前端人员更快上手 4.代码包要求为2M以内 性能更加突出 5.微信宿主提…

IBeginDragHandler,IEndDragHandler,IDragHandler拖拽接口

IBeginDragHandler, IEndDragHandler, 和 IDragHandler 是Unity的接口&#xff0c;用于处理拖拽相关的功能。需要引用 UnityEngine.EventSystems。 IBeginDragHandler 这个接口定义了一个方法&#xff0c;该方法在玩家开始拖拽一个对象时被调用。它通常用于初始化拖拽相关的变量…

Excel-Apache POI

Apache POI是用Java编写的免费开源的跨平台的Java API&#xff0c;Apache POI提供API给Java程 序对Microsoft Office格式档案读和写的功能&#xff0c;其中使用最多的就是使用POI操作Excel文 件。 Apache POI常用的类 HSSF &#xff0d; 提供读写Microsoft Excel XLS格式档案…

###C语言程序设计-----C语言学习(6)#

前言&#xff1a;感谢老铁的浏览&#xff0c;希望老铁可以一键三连加个关注&#xff0c;您的支持和鼓励是我前进的动力&#xff0c;后续会分享更多学习编程的内容。 一. 主干知识的学习 1. while语句 除了for语句以外&#xff0c;while语句也用于实现循环&#xff0c;而且它…

C++发起Https请求

Wininet库忽略Https证书 相信很多朋友使用C WINAPI开发的时候网络模块的时候遇到Https忽悠证书无效的情况下&#xff0c; 仍然希望获取结果下列代码便是忽略异常的Https CA证书&#xff0c;下面对原理进行简单的讲解首先, 需要设置Https忽略需要用到如下结果函数与参数Interne…