Android 性能优化篇之SharedPreferences使用优化

news2024/9/20 19:27:04

简介: `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的):

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

QueuedWork.addFinisher(awaitCommit)awaitCommit加入到队列中,awaitCommit在执行时利用CountDownLatch机制可以实现对当前线程的阻塞效果,后续ActivityonStop()中会将这里的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);
}
//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();
            }
        }
    }
}

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

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

这里的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);
        }

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 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以下处理不一样):
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
        }
    }
}

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

 

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

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

相关文章

机器学习吴恩达笔记第一篇——基于梯度下降的线性回归(零基础)

机器学习吴恩达笔记第一篇——基于梯度下降的线性回归&#xff08;零基础&#xff09; 一、线性回归——理论&#xff08;单变量&#xff09; 1、 假设函数h(x)为&#xff1a; ​ h ( x ) θ 0 θ 1 X h(x)\theta_0\theta_1 X h(x)θ0​θ1​X 2、要拟合数据成一条直线&…

如何让 300 万程序员爱上 CODING?

**《DNSPod十问》**是由腾讯云企业中心推出的一档深度谈话栏目&#xff0c;通过每期向嘉宾提出十个问题&#xff0c;带着广大读者站在产业互联网、科技领域精英的肩膀上&#xff0c;俯瞰各大行业发展趋势和前沿技术革新。 刘毅&#xff0c;腾讯云 CODING CEO、腾讯云开发者产品…

第十六章_Redis案例落地实战bitmap/hyperloglog/GEO

统计的类型有哪些 亿级系统中常见的四种统计 聚合统计 统计多个集合元素的聚合结果&#xff0c;就是前面讲解过的交差并等集合统计 复习命令 交并差集和聚合函数的应用 排序统计 抖音短视频最新评论留言的场景&#xff0c;请你设计一个展现列表。考察你的数据结构和设计思…

Nsudo,建议有编程基础的人使用,获取管理员和超级管理员权限

资源地址&#xff1a; https://download.csdn.net/download/yaosichengalpha/87801699 Nsudo,建议有编程基础的人使用&#xff0c;获取管理员和超级管理员权限 NSudo是一款非常不错的系统管理工具&#xff0c;他是基于raymai97的超级命令提示符&#xff0c;可以帮助我们获取T…

MybatisPlus--基础入门!真滴方便

目录 一、简介 2.特性 二、入门 1.创建springboot 项目 注意&#xff1a;引入 MyBatis-Plus 之后请不要再次引入 MyBatis 以及 MyBatis-Spring&#xff0c;以避免因版本差异导致的问题 2.数据准备 3.配置application.yml 4.代码 BaseMapper<>很重要&#xff01;…

vue 本地/PC端访问微信云数据库

1. 解决跨域访问问题 新建文件 vue.config.js // 后端服务器地址 let url "http://localhost:8888"; module.exports {publicPath: "./", // 【必要】静态文件使用相对路径outputDir: "./dist", //打包后的文件夹名字及路径devServer: {// 开…

组合数学第二讲

可以把取出来的数从小到大排序&#xff0c;第一个数不变&#xff0c;第二个数1&#xff0c;以此类推... 总共的情况为&#xff0c;数字取完后可再依次减回去&#xff0c;保证数在100以内 k-element multisets 引出下面的二项式系数 binomial coefficients&#xff08;二项式系…

线段树C++实现

一、本题线段树数组数据和结构 data[]{1,2,-3,5,6,-2,7,1,12,30,-10}&#xff0c;11个元素。 二、各个函数和结构 &#xff08;一&#xff09;线段树结构 创建线段树的结构&#xff0c; l、r为左边界和右边界&#xff0c;maxV和minV为最大值和最小值&#xff0c;sum为和&#…

English Learning - L3 作业打卡 Lesson2 Day12 2023.5.16 周二

English Learning - L3 作业打卡 Lesson2 Day12 2023.5.16 周二 引言&#x1f349;句1: Dollars are called greenbacks because that is the color of the back side of the paper money.成分划分弱读连读爆破语调 &#x1f349;句2: The color black is used often in expres…

抽象 + 接口 + 内部类

抽象类和抽象方法 抽象类不能实例化抽象类不一定有抽象方法&#xff0c;有抽象方法的类一定是抽象方法可以有构造方法抽象类的子类 要么重写抽象类中的所有抽象方法要么是抽象类 案例 Animal类Dog类 Sheep类Test类 接口 接口抽象类针对事物&#xff0c;接口针对行为案…

使用Google浏览器开启New bing

简介 搭建 通过谷歌商店下载两个浏览器插件&#xff0c;一个用于修改请求头agent的插件和一个用于伪造来源的插件x-forwarded-for插件&#xff0c;当然类似的插件很多很多&#xff0c;我这里使用的两个插件是 User-Agent Switcher Header Editor 使用 User-Agent Switcher 插件…

云HIS住院业务模块常见问题及解决方案

一&#xff1a;住院业务 1.患者办理住院时分配了错误的病区怎么办&#xff1f; 操作员误操作将患者分配了错误的病区分为以下两种情况&#xff1a; &#xff08;1&#xff09;、患者刚刚入院&#xff0c;未分配床位、主治医师与管床护士&#xff1a;这种情况比较好处理&#xf…

文件转pdf

背景 项目中很多预览工具&#xff0c;文件转pdf预览&#xff0c;采用libreoffice6.1插件实现 环境说明 系统CentOS&#xff1a;CentOS7 libreoffice&#xff1a;6.1 下载 中文官网 https://zh-cn.libreoffice.org/download/libreoffice/ 下载其他老版本 Index of /lib…

不敢妄谈K12教育

做为大学生的父亲&#xff1a;不敢妄谈孩子教育 大约10年前&#xff0c;写了一本教育书稿 找到一个出版社的编辑&#xff0c;被训了一通 打消了出书以及K12教育的念想 趣讲大白话&#xff1a;娘生九子&#xff0c;各有不同 【趣讲信息科技171期】 ****************************…

Vs+Qt+C++电梯调度控制系统

程序示例精选 VsQtC电梯调度控制系统 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<VsQtC电梯调度控制系统>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。…

PT100温度采集

1、信号采集的基本原理 PT100是将温度信号转换为电阻输出&#xff0c;其电阻值变化范围为0~200Ω。AD转换器只能对电压进行转换&#xff0c;无法采集直接采集温度&#xff0c;因此&#xff0c;需要一个1mA恒电流源给PT100供电&#xff0c;将电阻变化转换为电压变化。使用恒流源…

linux 安装 maven 3.8 版本

文章目录 1&#xff1a;maven 仓库官网 2、下载安装包 3、使用&#xff1a;Xftp 上传到你想放的目录 4、解压文件 ​编辑 5、配置环境变量 ​编辑 6、刷新 /etc/profile 文件 7、查看maven 版本 1&#xff1a;maven 仓库官网 Maven – Download Apache Mavenhttps://mave…

【C++】模板的一点简单介绍

模板 前言泛型编程函数模板概念格式函数模板的原理函数模板的实例化 类模板类模板的定义格式类模板的实例化 前言 这篇博客讲的是模板的一些基本知识&#xff0c;并没有那么深入&#xff0c;但是如果你是为了过期末考试而搜的这篇博客&#xff0c;我觉得下面讲的是够了的。 之…

简单分享线程池的设计

温故而知新&#xff0c;可以为师矣。 线程池是什么 线程池&#xff08;Thread Pool&#xff09;是一种基于池化思想管理线程的工具&#xff0c;经常出现在多线程服务器中&#xff0c;如MySQL。 池化思想&#xff0c;就是为了提高对资源的利用率&#xff0c;减少对资源的管理&a…

MySQL---空间索引、验证索引、索引特点、索引原理

1. 空间索引 MySQL在5.7之后的版本支持了空间索引&#xff0c;而且支持OpenGIS几何数据模型 空间索引是对空间数据类型的字段建立的索引&#xff0c;MYSQL中的空间数据类型有4种&#xff0c;分别是&#xff1a; 类型 含义 说明 Geometry 空间数据 任何一种空间类型 Poi…