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() {
        @Override
        public void run() {
            try {
                // writtenToDiskLatch初始化为CountDownLatch(1)
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };
    // 将awaitCommit添加到等待队列中,后续Activity/Servicede的onStop()会执行该Runnable等待文件写入完成
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
        @Override
        public 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() {
        @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);
}

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() 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 {
        // (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 = 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
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ASP .NET Core Api 使用过滤器

过滤器说明 过滤器与中间件很相似&#xff0c;过滤器&#xff08;Filters&#xff09;可在管道&#xff08;pipeline&#xff09;特定阶段&#xff08;particular stage&#xff09;前后执行操作。可以将过滤器视为拦截器&#xff08;interceptors&#xff09;。 过滤器级别范围…

第二证券:外围突传大利好!看涨期权交易骤增,中国资产大反攻继续?

外资正在做多我国财物。 据海外买卖网站marketchameleon的最新数据显现&#xff0c;海外挂钩追寻我国股票的iShares我国大型股ETF(FXI)的看涨期权买卖量在近一周内出现骤增&#xff0c;到达一年多来的最高点。别的&#xff0c;专心于科技范畴的KraneShares CSI我国互联网ETF&a…

开源之力与GPT的碰撞:探索未来技术的无限可能

摘要&#xff1a; 在本文中&#xff0c;我们将探讨开源软件与GPT&#xff08;大型预训练语言模型&#xff09;的完美结合如何推动技术的飞速发展。我们将简要介绍开源文化的价值观及其对技术创新的影响&#xff0c;分析GPT系列模型在开源社区中的发展与应用&#xff0c;并通过代…

STM32控制DS18B20温度传感器获取温度

时间记录&#xff1a;2024/1/28 一、DS18B20温度传感器介绍 &#xff08;1&#xff09;测温范围-55℃~125℃&#xff0c;在-10℃到85℃范围内误差为0.4 &#xff08;2&#xff09;返回的温度数据为16位二进制数据 &#xff08;3&#xff09;STM32和DS18B20通信使用单总线协议…

Linux下安装Nginx及配置SSL证书

安装 Nginx nginx 的一些模块需要依赖一些 lib 库&#xff0c;在安装 nginx 之前&#xff0c;须先安装这些 lib 库&#xff0c;比如常见依赖库主要有g、gcc、openssl-devel、pcre-devel和zlib-devel 所以执行如下命令安装&#xff1a; $ yum install gcc-c pcre pcre-devel z…

node后端服务框架

前言&#xff1a; 随着互联网的快速发展&#xff0c;Web 开发已成为当今计算机领域中非常重要的一部分。 Node.js 的出现为前端开发者提供了一个全新的方向&#xff0c;使得他们可以同时涉足前端和后端开发。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时&#xff…

批量导出域控用户及其所在OU和组

在Windows域环境中&#xff0c;批量导出域控用户及其所在OU&#xff08;组织单位&#xff09;和组成员身份信息&#xff0c;可以使用PowerShell脚本实现。以下是一个基本的示例脚本&#xff1a; Import-Module ActiveDirectory# 遍历所有用户 Get-ADUser -Filter * -Propertie…

数组奇缘:林浩然与杨凌芸的Java编程冒险记

数组奇缘&#xff1a;林浩然与杨凌芸的Java编程冒险记 Array Odyssey: The Java Programming Adventure of Lin Haoran and Yang Lingyun 在Java编程的广阔天地中&#xff0c;林浩然和杨凌芸的故事如同一段奇妙而生动的冒险传奇。林浩然&#xff0c;一个对代码充满热情、逻辑严…

python16-Python的字符串之转义字符

.前面已经提到&#xff0c;在字符串中可以使用反斜线进行转义;如果字符串本身包含反斜线&#xff0c;则需要使用“”表示&#xff0c;“W”就是转义字符。Python当然不会只支持这么几个转义字符&#xff0c;Python支持的转义字符如下表 掌握了上面的转义字符之后&#xff0c;下…

互斥锁/读写锁的概念及使用、死锁的避免

互斥锁的概念和使用 线程通信-互斥 临界资源&#xff1a;一次只允许一个任务(进程、线程)访问的共享资源 临界区&#xff1a;访问临界资源的代码 互斥机制&#xff1a;mutex互斥锁&#xff0c;任务访问临界资源前申请锁&#xff0c;访问完后释放锁互斥锁初始化 两种方法创建互…

中科星图——MODIS/006/MYD13A1的MYD13A1.006类数据集

数据名称&#xff1a; MYD13A1.006 Modis 16天 Aqua 500m 数据来源&#xff1a; NASA 时空范围&#xff1a; 2000-2022年 空间范围&#xff1a; 全国 数据简介&#xff1a; MOD13A1 V6数据集是由Aqua星搭载的中分辨率成像光谱仪获取的L3级植被指数产品&#xff0c…

三角函数转换(积分必备)

目录 一、诱导公式 二、二角和差公式 三、积化和差公式 四、万能、辅助角公式 五、倍角公式 六、反三角函数 七、余弦定理 一、诱导公式 1.公式一&#xff1a;设α为任意角&#xff0c;πα的三角函数值与α的三角函数值之间的关系 sin(π A) &#xff1d;&#xff0d;s…

《向量数据库指南》——Milvus Cloud向量过滤搜索及其优化

向量过滤搜索是一种基于条件的向量搜索方法&#xff0c;常用于推荐系统和信息检索等领域&#xff0c;能够帮助用户快速找到在给定条件下与其查询相关的内容。 在 Milvus Cloud社区中&#xff0c;这也是呼声比较高的功能。为满足广大用户的需求&#xff0c;Milvus Cloud在 Knowh…

王殿华主任:中医对睡眠的认知与睡眠障碍分类

睡眠不足不仅会导致黑眼圈&#xff0c;还会带来许多健康风险。怎样才能睡得更好&#xff1f; 一、睡眠障碍的分类 失眠症呼吸系统相关睡眠障碍&#xff1b;中枢多发性睡眠&#xff1b;睡眠节律紊乱&#xff1b;睡眠异常&#xff1b;睡眠运动障碍&#xff1b;其他睡眠障碍。 失…

CSS3的学习笔记

CSS3的学习笔记 什么是css: CSS是层叠样式表&#xff08;Cascading Style Sheets&#xff09;的缩写&#xff0c;是一种用来描述网页样式和布局的标记语言。它可以控制网页中的文字大小、颜色、间距、背景、边框、布局等方面&#xff0c;使网页更加美观和易于阅读。通过CSS&a…

Jmeter学习系列之一:Jmeter的详细介绍

目录 一、Jmeter的介绍 二、Jemeter的特点 三、Jemter相关概念 3.1采样器&#xff08;Samplers&#xff09; 3.2逻辑控制器&#xff08;Logic Controllers&#xff09; 3.3监听器&#xff08;Listeners&#xff09; 3.4配置元件&#xff08;Configuration Elements&#…

python 匿名函数lambda的简洁用法

当前版本&#xff1a; Python 3.8.4 文章目录如下 1. lambda 的特点 2. lambda 的用法 2.1. 基本语法 2.2. 函数传参 2.3. 结合条件语句 3. lambda 的应用场景 3.1. 处理列表 3.2. 处理字典 1. lambda 的特点 lambda 是一种匿名函数的定义方式&#xff0c;也称为 lam…

Windows 7 x64 SP1 安装 Google Chrome 109.0.5414.120 (正式版本) (64 位)

1 使用 IE 浏览器 输入网址 Google Chrome 网络浏览器得益于 Google 智能工具&#xff0c;Chrome 现在更易用、更安全、更快速。https://www.google.cn/chrome/&#xff0c;点击下载 Chrome。 2 点击 接受并安装。 3 提示。 4 保存。 5 双击 运行 ChromeSetup.exe。 6 等待安…

MySQL十部曲之六:数据操作语句(DML)

文章目录 前言语法约定DELETEINSERTSELECT查询列表SELECT 选项子句FROMWHEREORDER BYGROUP BYHAVINGWINDOWLIMITFOR SELECT ... INTO连接查询CROSS JOIN和INNER JOINON和USINGOUTER JOINNATURE JOIN 子查询标量子查询使用子查询进行比较带有ANY、IN或SOME的子查询带有ALL的子查…

网站将http升级到https大概要多少费用

随着网络安全意识的不断提升&#xff0c;越来越多的网站正从传统的HTTP协议转向更安全的HTTPS协议。这一转变的核心在于部署SSL&#xff08;Secure Sockets Layer&#xff09;或TLS&#xff08;Transport Layer Security&#xff09;证书&#xff0c;以实现数据加密传输&#x…
最新文章