SharedPreferences详解及其ANR解决方案

news2024/12/23 4:34:38

关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。
专注于分享各领域原创系列文章 ,擅长java后端、移动开发、商业变现、人工智能等,希望大家多多支持。

目录

  • 一、导读
  • 二、概览
  • 三、使用
  • 四、原理
  • 五、存在的问题
  • 六、优化
    • 6.1 DataStore
    • 6.2 MMKV
    • 6.3 sp优化
  • 七、 推荐阅读

在这里插入图片描述

一、导读

我们继续总结学习Java基础知识,温故知新。

二、概览

SharedPreferences 是 Android 平台上用于存储轻量级键值对数据的一种机制。它提供了一种简单的方式来保存和获取应用程序的数据。

SharedPreferences 存储的数据是基于键值对的,每个存储项都有一个唯一的键和对应的值。可以通过键来检索特定的值,也可以修改、添加或删除已存储的值。

SharedPreferences 存储的数据是持久化的,即使应用程序被关闭或设备重启,存储的数据仍然可用。

SharedPreferences 首次初始化时把整个xml文件加载到内存中,在使用过程中容易出现ANR,主要是因为加锁及线程等待。
ANR发生在QueuedWork.waitToFinish()方法。

三、使用

SharedPreferences 的使用非常方便,最终数据是以xml文件的形式存在在 /data/data/项目包名/shared_prefs/sp_name.xml文件

// 1、获取实例
SharedPreferences sp = mContext.getSharedPreferences("sp_name", Context.MODE_PRIVATE);

// 2、获取edit
SharedPreferences.Editor edit = sp.edit();

// 3、以key - value 的方式存储 
edit.putInt("KEY_SOPHIX", 0); 

// 4、异步写入数据
edit.apply();

// 4、同步写入数据
edit.commit();
// 1、获取实例
SharedPreferences sp = getContext().getSharedPreferences("sp_name", Context.MODE_PRIVATE);

// 2、通过key 获取value,后面一个是默认值
String var1 = sp.getString("key", "");

四、原理

SharedPreferences 基于键值对的存储原理。它是通过一个 XML 文件来保存数据,文件位于应用的私有存储空间中。
并且是基于内存缓存的, SharedPreferences在读取xml文件时,会把整个xml文件加载到内存中,并以DOM的形式进行解析。
当应用程序再次访问同一个 SharedPreferences 对象时,系统会直接从内存中读取数据,而不是每次都从 XML 文件中读取。
新增一个K-V时,写入磁盘是全量更新,即会把之前的文件再次更新一遍。

所以SharedPreferences适用于存储轻量级的数据。

我们在调用getxxx方法时,获取到的数据是内存中的数据,
在保存数据时,也是先保存在内存,再写入文件。


    String getString(String key, @Nullable String defValue);

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }

commit保存数据,先内存,再磁盘,这是一个同步的过程


    @Override
    public boolean commit() {
        long startTime = 0;

        // 数据保存到内存
        MemoryCommitResult mcr = commitToMemory();


        // 数据保存到磁盘文件
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        } finally {
        }
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }

apply 保存数据,先内存,再异步保存到磁盘



    @Override
    public void apply() {
        final long startTime = System.currentTimeMillis();
        
        // 数据保存到内存
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                @Override
                public void run() {
                    try {
                        看这里
                        //这里的操作使用CountDownLatch实现等待效果,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);

        notifyListeners(mcr);
    }

加锁
QueuedWork.java

    这个就是后面 ActivityonStop()ServiceonDestroy()执行时,QueuedWork.waitToFinish()里面要执行的线程
    
    public static void addFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishersField.add(finisher);
        }
    }

写文件后释放锁
SharedPreferencesImpl.java


    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    
        mcr.setDiskWriteResult(false, true);
        
    }

    看这里
    void setDiskWriteResult(boolean wasWritten, boolean result) {

        这里就是上面awaitCommit 线程
        writtenToDiskLatch.countDown();
    }

五、存在的问题

  1. 读取xml文件时,会把整个xml文件直接加载到内存中解析,如果文件过大,容易出内存问题。
  2. 文件数据的读取都加锁,如果SP 文件未被加载或解析到内存中,读写操作都需要等待,可能会对UI线程流畅度造成一定影响,甚至ANR.
  3. 在保存数据,apply 及 commit 都会出现ANR问题。

具体可以看上面的源码, apply 其实利用了CountDownLatch机制,阻塞了当前线程,后续 Activity 的onStop()
中会将这里的awaitCommit取出来执行,即UI线程会阻塞等待sp文件写入磁盘。
所以我们有的时候可以看到退出一个页面的时候,感觉也会卡,因为阻塞了主线程。

Activity的onStop()、Service的onDestroy()执行时,都会调用到QueuedWork.waitToFinish()方法。
具体代码在ActivithThread.java

public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        

        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {
                    finisher = sFinishersField.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }

    }

借用一张图总结下:
图片来自:今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
1

六、优化

6.1 DataStore

DataStore

6.2 MMKV

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强,且支持多进程访问。
使用起来也非常简单,先引入依赖

dependencies {
    implementation 'com.tencent:mmkv:1.3.0'
    // replace "1.3.0" with any available version
}
public void onCreate() {
    super.onCreate();

    String rootDir = MMKV.initialize(this);
    System.out.println("mmkv root: " + rootDir);
    //……
}


MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");

kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");

kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");

MMKV地址:https://github.com/tencent/mmkv

6.3 sp优化

  • SP文件按分类去加载存储,K-V数据不要太多。
  • 利用反射,使waitToFinish 失效

参考头条的文章,如果需要主线程在 waitToFinish 的时候直接跳过去,让 sPendingWorkFinishers.poll()返回为 null,
则这里的等待行为直接就跳过去了,sPendingWorkFinishers 是个 ConcurrentLinkedQueue 集合,
可以直接动态代理这个集合,复写 poll 方法,让其永远返回 null,这个时候 UI 永远不会等待子线程写入文件完毕。

ActivithThread.java

public static void waitToFinish() {

        try {
            看这里,这里也可能发生ANR 哦,
            processPendingWork();
        } finally { 
            StrictMode.setThreadPolicy(oldPolicy);
        }
        try {
            while (true) {
                synchronized (sLock) {
                    finisher = sFinishersField.poll();
                }

                看这里,不让它执行即可,
                if (finisher == null) {
                    break;
                }
                
                finisher.run();
            }
        } finally {
        }

    }

为什么可以这么做呢,我们回头看看源码,这里面加入进来的线程,其实就一个线程阻塞的方法,
我们不让它运行,就不会阻塞主线程了。
mcr.writtenToDiskLatch.await();


Class<?> aClass = Class.forName("android.app.QueuedWork");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
    
    获取指定name名称的(包含private修饰的)字段,不包括继承的字段
    Field sFinishersField = aClass.getDeclaredField("sFinishersField");
    
    将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性
    sFinishersField.setAccessible(true);
    
    返回指定对象上此 Field 表示的字段的值,这里返回的是 LinkedList<Runnable> sFinishersField = new LinkedList<>()
    LinkedList<?> sPendingWorkFinishers = (LinkedList<?>) sFinishersField.get(null);
    
    HookLinkedList<?> proxyFinishers = new HookLinkedList<>(sPendingWorkFinishers);
    将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
    sFinishersField.set(null, proxyFinishers);
}


private static class HookLinkedList<T> extends LinkedList<T> {

    private final LinkedList<T> linkedList;

    public HookLinkedList(LinkedList<T> linkedList) {
        this.linkedList = linkedList;
    }

    /**
     * always return null
     */
    @Nullable
    @Override
    public T poll() {
        return null;
    }

    @Override
    public boolean add(T t) {
        return linkedList.add(t);
    }

    @Override
    public boolean isEmpty() {
        return true;
    }

    @Override
    public boolean remove(@Nullable Object o) {
        return linkedList.remove(o);
    }
}

由于源码不一样,8.0以下用以下方法,方式跟上面一样

Field sPendingWorkFinishers = aClass.getDeclaredField("sPendingWorkFinishers");
sPendingWorkFinishers.setAccessible(true);
ConcurrentLinkedQueue<?> sPendingWorkFinishers = (ConcurrentLinkedQueue<?>) sPendingWorkFinishers.get(null);
SpConcurrentLinkedQueue<?> proxyFinishers = new SpConcurrentLinkedQueue<>(sPendingWorkFinishers);
sPendingWorkFinishers.set(null, proxyFinishers);


private static class SpConcurrentLinkedQueue<T> extends ConcurrentLinkedQueue<T> {

    private final ConcurrentLinkedQueue<T> concurrentLinkedQueue;

    public MyConcurrentLinkedQueue(ConcurrentLinkedQueue<T> concurrentLinkedQueue) {
        this.concurrentLinkedQueue = concurrentLinkedQueue;
    }

    /**
     * always return null
     */
    @Nullable
    @Override
    public T poll() {
        return null;
    }

    @Override
    public boolean remove(@Nullable Object o) {
        return concurrentLinkedQueue.remove(o);
    }

    @Override
    public boolean isEmpty() {
        return true;
    }

    @Override
    public boolean add(T t) {
        return concurrentLinkedQueue.add(t);
    }
}

processPendingWork的ANR 也要处理下,这里有两个block点

  • 异步线程正在执行 processPendingWork函数,异步工作线程持有 sProcessingWork锁,因此主线程执行 processPendingWork时 ,因为获取不到 sProcessingWork锁 ,出现锁等待
  • 当主线程成功获取到 sProcessingWork锁,调用clone函数时,sWork队列中 确实存在未执行的任务,这部分任务将在主线程直接执行,如果此时IO操作较慢,则主线程因为慢IO出现阻塞甚至ANR

思路原理:
无论实在哪个线程执行,代理的clone函数都返回空队列,这样保证了processPendingWork的调用不会出现互相阻塞,相当于processPendingWork实际上没有执行任何操作, 并且通过反射获取QueuedWork的mHandler的Looper对象,创建一个新的Hander,并将sWork中的任务提交到这个Handler去执行,从而实现了无阻塞运行

推荐一个大佬的开源,里面有完整的处理代码,
再借一张大佬图:
d
github: https://github.com/Knight-ZXW/SpWaitKiller

七、 推荐阅读

Java 专栏

SQL 专栏

数据结构与算法

Android学习专栏

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

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

相关文章

Android动态添加和删除控件/布局

一、引言 最近在研究RecyclerView二级列表的使用方法&#xff0c;需要实现的效果如下。 然后查了一些博客&#xff0c;觉得实现方式太过复杂&#xff0c;而且这种方式也不是特别受推荐&#xff0c;所以请教了别人&#xff0c;得到了一种感觉还不错的实现方式。实现的思路为&…

【LeetCode-经典面试150题-day9]

目录 36.有效的数独 54.螺旋矩阵 48.旋转图像 73.矩阵置零 36.有效的数独 题意&#xff1a; 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1…

huggingface datasets离线加载文件的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

Sim/circuit10

通过观察可知&#xff0c;在a、b同时为0或1时&#xff0c;state的值改变 state的值可以改变q的输出&#xff0c;1为ab的同或&#xff0c;0为异或 利用assign q进行输出 module top_module (input clk,input a,input b,output q,output state );always(posedge clk)if(a&…

【TypeScript】元组

元组&#xff08;Tuple&#xff09;是 TypeScript 中的一种特殊数据类型&#xff0c;它允许你定义一个固定数量和类型的元素组合。元组可以包含不同类型的数据&#xff0c;每个数据的类型在元组中都是固定的。以下是 TypeScript 中元组的基本用法和特点&#xff1a; // 声明一…

Win系统下安装Linux双系统教程

软件下载 软件&#xff1a;Linux版本&#xff1a;18.0.4语言&#xff1a;简体中文大小&#xff1a;1.82G安装环境&#xff1a;Win11/Win10/Win8/Win7硬件要求&#xff1a;CPU2.0GHz 内存4G(或更高&#xff09;下载通道①丨百度网盘&#xff1a;1.ubuntu18.0.4下载链接&#xf…

量子计算对信息安全的影响:探讨量子计算技术对现有加密方法和信息安全基础设施可能带来的颠覆性影响,以及应对策略

第一章&#xff1a;引言 随着科技的迅猛发展&#xff0c;量子计算作为一项颠覆性的技术正逐渐走入我们的视野。量子计算以其强大的计算能力引发了全球科技界的广泛关注。然而&#xff0c;正如硬币的两面&#xff0c;量子计算技术所带来的不仅仅是计算能力的巨大飞跃&#xff0…

公文校对的艺术:如何确保你的正式文件零错误?

公文是政府和企业中最重要的正式文件之一。一个小小的错误&#xff0c;不仅会影响公文的专业性&#xff0c;甚至可能带来法律和经济后果。因此&#xff0c;如何进行精准的公文校对成为了一门必不可少的技能。接下来&#xff0c;我们将分享一些专业的公文校对技巧&#xff0c;并…

测试框架pytest教程(4)运行测试

运行测试文件 $ pytest -q test_example.py 会运行该文件内test_开头的测试方法 该-q/--quiet标志使输出保持简短 测试类 pytest的测试用例可以不写在类中&#xff0c;但如果写在类中&#xff0c;类名需要是Test开头&#xff0c;非Test开头的类下的test_方法不会被搜集为用…

通过Matlab编程分析微分方程、SS模型、TF模型、ZPK模型的关系

微分方程、SS模型、TF模型、ZPK模型的关系 一、Matlab编程 微分方程、SS模型、TF模型、ZPK模型的关系二、对系统输出进行微分计算三、对系统输出进行积分计算四、总结五、系统的零点与极点的物理意义参考 &#xff1a;[https://www.zhihu.com/question/22031360/answer/3073452…

HCIP---VLAN实验(接入、中继、混杂)

实验要求 PC1/3的接口均为access模式&#xff0c;且属于van2&#xff0c;在同一网段 PC2/4/5/6的IP地址在同一网段&#xff0c;与PC1/3不在同一网段 PC2可以访问4/5/6&#xff0c;PC4不能访问5/6&#xff0c;PC5不能访问PC6 所有PC通过DHCP获取ip地址&#xff0c;PC1/3可以访问…

XXX程序 详细说明

用于记录理解PC程序的程序逻辑 1、程序的作用 根据原作者的说明&#xff08;文件说明.txt&#xff09;&#xff0c;该程序 (PC.py) 的主要作用是提取某一个文件夹中的某个设备 (通过config中的信息看出来是Ag_T_8) 产生的日志文件&#xff0c;然后提取其中某些需要的数据&…

Python爬虫(十四)_BeautifulSoup4 解析器

CSS选择器&#xff1a;BeautifulSoup4 和lxml一样&#xff0c;Beautiful Soup也是一个HTML/XML的解析器&#xff0c;主要的功能也是如何解析和提取HTML/XML数据。 lxml只会局部遍历&#xff0c;而Beautiful Soup是基于HTML DOM的&#xff0c;会载入整个文档&#xff0c;解析整…

智能硬件知识

第二章 第五章 第六章 第七章 第八章 第九章 第十章 考点 条件编译 volatile、static、 union、 struct、 const指针 堆与栈的不同点 3.功能模块应用题 (1) GPIO 的应用:流水灯的电路及软件编码、驱动数码管的电路及编码。 (2)外部中断的应用:电路及回调函数编码。 (3) …

关于数据中心存储智能运维的思考

随着互联网和大数据的快速发展&#xff0c;数据中心存储的重要性也日益凸显。在本文中&#xff0c;将深入探讨数据中心存储智能运维的历史变迁、当前的发展状态和未来的运维趋势。 数据中心存储运维的历史变迁可以分为以下几个阶段&#xff1a; 人工运维阶段 最初&#xff0c…

深度学习基本理论上篇:(MLP/激活函数/softmax/损失函数/梯度/梯度下降/学习率/反向传播)、深度学习面试

1、MLP、FCN、DNN三者的关系&#xff1f; 多层感知器MLP&#xff0c;全连接网络&#xff0c;DNN三者的关系&#xff1f;三者是不是同一个概念&#xff1f; FCN&#xff1a;Fully Connected Neural Network&#xff0c;全连接神经网络&#xff0c;也称为密集连接神经网络&#…

前端开发怎么解决前端安全性的问题? - 易智编译EaseEditing

前端安全性是保护前端应用程序免受恶意攻击和数据泄露的重要方面。以下是一些解决前端安全性问题的关键方法&#xff1a; 输入验证与过滤&#xff1a; 对所有用户输入进行验证和过滤&#xff0c;防止恶意用户通过注入攻击等手段破坏应用程序或获取敏感信息。 跨站点脚本&#…

Android笔记:在原生App中嵌入Flutter

首先有一个可以运行的原生项目 第一步&#xff1a;新建Flutter module Terminal进入到项目根目录&#xff0c;执行flutter create -t module ‘module名字’例如&#xff1a;flutter create -t module flutter-native 执行完毕&#xff0c;就会发现项目目录下生成了一个modu…

【核磁共振成像】单射成像和高速脉冲序列

目录 一、提高成像速度的手段二、平面回波成像(EPI)序列三、常用或基本EPI序列四、EPI变型序列五、渐开平面螺旋(spiral)扫描序列六、RARE序列七、GRASE序列八、STEAM序列 一、提高成像速度的手段 MRI扫描时间可表示为   其中Nex为激发次数&#xff0c;NpE1和NpE2是两个相位…

kubernetes--技术文档-真--集群搭建-三台服务器一主二从(非高可用)附属文档-使用不同运行商服务器-搭建公网集群

&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;版本&#xff01;&#xff01;&#xff01;&#xff01; 使用公网初始化 Kubernetes 需要 Kubernetes 版本 1.19 或更高版本。在早期的版本中&#xff0c;Kubernetes 还不支持公网初始化。因此&#xff0c;请确保…