Android SharedPreference 详解

前提:基于 Android API 30

1. 认识 SharedPreference

SharedPreference 是 Android 提供的轻量级的,线程安全的数据存储机制,使用 key-value 键值对的方式将数据存储在 xml 文件中,存储路径为

/data/data/yourPackageName/shared_prefs

存储的数据是持久化(persistent)的,应用重启后仍然可以获取到存储的数据。由于SharedPreference 是轻量级的,所以不适合存储过多和过大的数据的场景,这种情况下应该考虑数据库

2. 获取 SharedPreference

SharedPreference 是一个接口,实现类是 android.app.SharedPreferencesImpl,获取 SharedPreference 的方法由 Context 提供

val sp = context.getSharedPreferences("FreemanSp", MODE_PRIVATE)

第一个参数是 name,无论哪个 context 对象,只要传入的 name 一样,获取到的就是同一个SharedPreference 对象

第二个参数是 mode,Android 提供 4 中模式可选,其中三种已明确标识为过期

  • MODE_PRIVATE,私有模式,只能由当前应用读写
  • MODE_WORLD_READABLE,其他应用可读,已过期
  • MODE_WORLD_WRITEABLE,其他应用可写,已过期
  • MODE_MULTI_PROCESS,同一应用存在多进程情况下共享,不可靠,已过期

声明 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 这两中模式,其他应用可以完全读写自己应用的数据,这是及其危险的,谷歌推荐使用 ContentProvier 方式去做数据共享,共享的数据可以完全由 provider 定义的 uri 做出限制。
甚至在 android N 后指定这两种方式,应用直接报错

    private void checkMode(int mode) {if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {if ((mode & MODE_WORLD_READABLE) != 0) {throw new SecurityException("MODE_WORLD_READABLE no longer supported");}if ((mode & MODE_WORLD_WRITEABLE) != 0) {throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");}}}

基于 mode 的限制,本文一切都已 MODE_PRIVATE 展开

创建 SharedPreferencesImpl 对象时,会起一个子线程,将 xml 文件解析的数据全量保存在内存中

    private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}

如果存储着大量数据时会有占用较大内存

3. 写入数据

SharedPreference通过调用edit()方法获取到一个Editor对象,其实现类是android.app.SharedPreferencesImpl.EditorImpl。写入数据提供了同步 commit 和异步 apply 两个提交方法

3.1 commit()

调用方式

 sp.edit().putLong("currentTime", System.currentTimeMillis()).commit()

该方法是个同步方法,IDE 会出现警告提示

Commit your preferences changes back from this Editor to the SharedPreferences object it is editing. This atomically performs the requested modifications, replacing whatever is currently in the SharedPreferences.
Note that when two editors are modifying preferences at the same time, the last one to call commit wins.
If you don’t care about the return value and you’re using this from your application’s main thread, consider using apply instead

结论是如果不考虑返回结果或者在主线程,应该考虑用 apply 方法替代,原因来看下原码

        @Overridepublic boolean commit() {long startTime = 0;if (DEBUG) {startTime = System.currentTimeMillis();}// 先提交到内存MemoryCommitResult mcr = commitToMemory();// 写入磁盘SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {// 当前线程 await,确保 mcr 已经提交到了磁盘mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " committed after " + (System.currentTimeMillis() - startTime)+ " ms");}}notifyListeners(mcr);return mcr.writeToDiskResult;}

再详细看下 commitToMemory(),这个方法的主要作用有两个:

  1. 写入内存
  2. 构造写入磁盘的map,需要注意的是这个map是全量写入的
        private MemoryCommitResult commitToMemory() {long memoryStateGeneration;boolean keysCleared = false;List<String> keysModified = null; // 需要提交存储的键值对Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk; // 需要写入磁盘的键值对,这里会是整个 xml 文件的全量数据synchronized (SharedPreferencesImpl.this.mLock) {// We optimistically don't make a deep copy until// a memory commit comes in when we're already// writing to disk.if (mDiskWritesInFlight > 0) { // 多线程提交时,对 mMap 做深拷贝,这个 mMap 就是整个 xml 文件的全量键值对// We can't modify our mMap as a currently// in-flight write owns it.  Clone it before// modifying it.// noinspection uncheckedmMap = new HashMap<String, Object>(mMap);}mapToWriteToDisk = mMap;// 同时要写入磁盘的提交计数标记为 +1,如果同时有异步多个提交,后面的 commit 有可能会提交到子线程,并且由于版本控制,最后一个 commit 的才会写入到磁盘mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0;if (hasListeners) {keysModified = new ArrayList<String>();listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}synchronized (mEditorLock) {boolean changesMade = false;if (mClear) { // 只有在调用了 Editor#clear() 方法才会进入这段代码,因为 mEditorLock 锁if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();}keysCleared = true;mClear = false;}// 将用户要修改的数据合并到 mapToWriteToDiskfor (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();// "this" is the magic value for a removal mutation. In addition,// setting a value to "null" for a given key is specified to be// equivalent to calling remove on that key.if (v == this || v == null) {if (!mapToWriteToDisk.containsKey(k)) {continue;}mapToWriteToDisk.remove(k);} else {if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}mapToWriteToDisk.put(k, v);}changesMade = true;if (hasListeners) {keysModified.add(k);}}mModified.clear();if (changesMade) {mCurrentMemoryStateGeneration++;}// 写入的版本管理,优化写入磁盘的写入次数,避免资源浪费和数据覆盖memoryStateGeneration = mCurrentMemoryStateGeneration;}}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,listeners, mapToWriteToDisk);}

写入内存后接着就要执行写入磁盘

  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);

enqueueDiskWrite 方法的主要作用是

  1. 同步写入磁盘
  2. 如果此时有新的 commit 提交,旧提交将会被抛弃,新提交则有可能提交到子线程
    private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null); // 同步commit,isFromSyncCommit = truefinal Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {/** 真正执行写入到磁盘的操作,但是 writeToFile 会有 memoryStateGeneration 控制,如果当前的 mcr 版本和 memoryStateGeneration 不一致,则当前的写入会被对其* 这也是 commit 方法注释上写的:* Note that when two editors are modifying preferences at the same time, the last one to call commit wins*/ writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) { // apply 方法不会进入到这个代码块boolean wasEmpty = false;synchronized (mLock) {// 如果此时有新的 commit 执行到 commitToMemory ,则 mDiskWritesInFlight 则有可能大于 1,此时 wasEmpty = falsewasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) { // 如果 wasEmpty 为 true,则直接在当前线程执行写入磁盘writeToDiskRunnable.run();return;}}// 将写入磁盘的操作加入到子线程QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}

总结

  • 如果主线程执行commit会在主线程执行 IO 操作,数据量太大可能会造成 ANR
  • edit() 方法每次都会返回一个新的Editor对象,应该缓存一个Editor对象重复使用,SP 是线程安全的
  • 如果实在需要使用commit,可以考虑将频繁写入的key单独放到一个 SP,这样避免资源浪费
  • 尽量避免多次调用commit,可以多次put数据,在合适实际执行一次commit
  • 避免使用commit
3.2 apply()

调用方式

 sp.edit().putLong("currentTime", System.currentTimeMillis()).apply()

该方法是个异步方法,Android 中推荐使用这个方法存储数据,直接看源码

public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory(); // 写入内存,构造写入磁盘的map,同commit一样,这个map是全量写入的final Runnable awaitCommit = new Runnable() { // 这个 Runnable 的作用就是为了统计写入磁盘的耗时@Overridepublic void run() {try {mcr.writtenToDiskLatch.await(); // block 待 mcr 写入磁盘完成后 writtenToDiskLatch 降为0,await()立刻返回,awaitCommit 继续执行} catch (InterruptedException ignored) {}if (DEBUG && mcr.wasWritten) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};QueuedWork.addFinisher(awaitCommit); // 将 awaitCommit 加入到 QueuedWork 子线程的消息队列// postWriteRunnable 会在写入磁盘完成后触发执行Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run(); // 触发 awaitCommit ,统计写入耗时QueuedWork.removeFinisher(awaitCommit);}};// postWriteRunnable 会在真正执行写入磁盘的 runnable 调用SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);// Okay to notify the listeners before it's hit disk// because the listeners should always get the same// SharedPreferences instance back, which has the// changes reflected in memory.notifyListeners(mcr);
}

commit一样,apply也是先写入内存后并构建全量写入磁盘的map。不同的是在调用 enqueueDiskWrite时传入了一个postWriteRunnableenqueueDiskWrite 通过判断这个参数来区别是否是异步执行,该方法在commit流程中已分析过一部分,这里再来看apply的流程

   private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) { // apply 传入的 postWriteRunnable 不为 nullfinal boolean isFromSyncCommit = (postWriteRunnable == null); // isFromSyncCommit = falsefinal Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {// 如果 memoryStateGeneration 一致,将完成磁盘的写入writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run(); // 通知写入完成}}};// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}// writeToDiskRunnable 直接加入子线程消息队列QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}

总结

  • edit() 方法每次都会返回一个新的Editor对象,应该缓存一个Editor对象重复使用,SP 是线程安全的
  • 多次 apply 也不会造成多次写入磁盘,因为有版本控制
  • 推荐使用apply

4. SharedPreference 造成ANR的原因

  1. 在主线程 commit 写入大量的数据
  2. 在 ActivityThread 共用四个地方会调用QueuedWork.waitToFinish();,分别是:
  • handleServiceArgs
  • handleStopService
  • handlePauseActivity
  • handleStopActivity

这些都是Android组件的生命周期管理,如果QueuedWork有遗留的runnable没有执行完,则会将剩余的runnable在主线程执行,导致产生了ANR的风险

解决办法

  1. 避免使用commit,使用 apply
  2. 将频繁写入的key放到独立的 SharedPreference
  3. 使用 MMKV 迁移或者代替SharedPreference

5. MMKV 简单分析

  1. mmkv 提供了 importFromSharedPreference 方法用来做数据迁移
  2. mmkv 不支持 getAll 方法,因为通过 protoBuf 二进制存取,在其他存取操作时都会指定数据类型,比如getInt, getBoolean。但是如果使用getAll,mmkv不知道每个key对应的数据类型,所以无法decode。为了能够代理 getAll,并避免第三方sdk有使用该方法,可以将类型转换上移,在使用mmkv存取时统一用 String 类型,再拿出值时再由业务自身去判断(因为业务知道自己在写入时,每个key对应的数据类型),SharedPreference 自身也只是返回了一个 Map<String, Object> 对象
  3. 支持多进程读写
  4. 每次都是增量写入,本身不提供缓存。当写入时,发现内存不够会动态申请内存空间

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

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

相关文章

自动化测试脚本语言选择

测试人员在选择自动化测试脚本语言时面临多种选项。Python、Java、C#、JavaScript 和 Ruby 都是常见选择&#xff0c;但哪种语言最适合&#xff1f;本文将详细分析这些语言的特点、适用场景和优劣势&#xff0c;结合行业趋势和社会现象&#xff0c;为测试人员提供全面指导。 选…

【Java项目】基于JSP的KTV点歌系统

【Java项目】基于JSP的KTV点歌系统 技术简介&#xff1a;采用JSP技术、B/S结构、MYSQL数据库等实现。 系统简介&#xff1a;KTV点歌系统的主要使用者分为管理员和用户&#xff0c;实现功能包括管理员&#xff1a;个人中心、用户管理、歌曲库管理、歌曲类型管理、点歌信息管理&a…

element-plus文档解析之Layout布局(el-row,el-col)

前言 这是element-plus提供的响应式布局组件。可以非常方便的实现响应式布局以及快速按比例分块。 例如实现下面的效果&#xff1a; 第一行&#xff1a;宽度占100% 第二行&#xff1a;宽度1&#xff1a;1 第三行&#xff1a;1&#xff1a;1&#xff1a;1 第四行&#xff1a;1…

【Java】——数据类型和变量

个人主页&#xff1a;User_芊芊君子 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 文章目录&#xff1a; 1.Java中的注释1.1.基本规则1.2.注释规范 2.标识符3.关键字4.字面常量5.数据类型6.变量6.1变量的概念6.2语法6.3整型变量6.3.1整型变量6.3.2长整…

串口数据记录仪DIY,体积小,全开源

作用 产品到客户现场出现异常情况&#xff0c;这个时候就需要一个日志记录仪、黑匣子&#xff0c;可以记录产品的工作情况&#xff0c;当出现异常时&#xff0c;可以搜集到上下文的数据&#xff0c;从而判断问题原因。 之前从网上买过&#xff0c;但是出现过丢数据的情况耽误…

JVM中是如何定位一个对象的

在 Java 中&#xff0c;对象定位指的是如何通过引用&#xff08;Reference&#xff09;在堆内存中找到对象实例及其元数据&#xff08;如类型信息&#xff09;。JVM 主要通过 直接指针访问 和 句柄访问 两种方式实现&#xff0c;各有其优缺点和应用场景&#xff1a; 一、直接指…

Mac 如何在idea集成SVN

在windows系统上面有我们最为熟悉的小乌龟TortoiseSVN&#xff0c;在mac系统上面则没有什么好用的svn的工具&#xff0c;而且大部分都付费&#xff0c;需要各种渠道找PJ版&#xff0c;费事费力&#xff0c;作为程序员&#xff0c;大部分人应该都会安装开发工具&#xff0c;本文…

批量测试IP和域名联通性

最近需要测试IP和域名的联通性&#xff0c;因数量很多&#xff0c;单个ping占用时间较长。考虑使用Python和Bat解决。考虑到依托的环境&#xff0c;Bat可以在Windows直接运行。所以直接Bat处理。 方法1 echo off for /f %%i in (E:\封禁IP\ipall.txt) do (ping %%i -n 1 &…

LabVIEW变频器谐波分析系统

随着工业自动化的发展&#xff0c;变频器在电力、机械等领域的应用日益广泛&#xff0c;但谐波问题直接影响系统效率与稳定性。传统谐波检测设备&#xff08;如Norma5000&#xff09;精度虽高&#xff0c;但价格昂贵且操作复杂&#xff0c;难以适应现场快速检测需求。本项目基于…

Unity Shader学习总结

1.帧缓冲区和颜色缓冲区区别 用于存储每帧每个像素颜色信息的缓冲区 帧缓冲区包括&#xff1a;颜色缓冲区 深度缓冲区 模板缓冲区 自定义缓冲区 2.ImageEffectShader是什么 后处理用的shader模版 3.computerShader 独立于渲染管线之外&#xff0c;在显卡上运行&#xff0c;大量…

OpenPLC WebServer启动

简述 OpenPLC OpenPLC 可运行在嵌入式系统和普通计算机上&#xff0c;其基本原理是在硬件上安装类似 Linux 的操作系统&#xff0c;并在该环境下运行 OpenPLC 应用程序&#xff0c;从而让用户开发、调试和运行工业自动化控制逻辑。它目前只支持部分 ARM 架构的嵌入式系统&…

【基础知识】回头看Maven基础

版本日期修订人描述V1.02025/3/7nick huang创建文档 背景 项目过程中&#xff0c;对于Maven的pom.xml文件&#xff0c;很多时候&#xff0c;我通过各种参考、仿写&#xff0c;最终做出想要的效果。 但实际心里有些迷糊&#xff0c;不清楚具体哪个基础的配置所实现的效果。 今…

ROS实践(四)机器人SLAM建图(gmapping)

目录 一、SLAM技术 二、常用工具和传感器 三、相关功能包 1. gmapping建图功能包 2. map_server 四、SLAM 建图实验 1. 配置gmapping(launch文件) 2. 启动机器人仿真(含机器人以及传感器) 3. 运行gmapping节点 4. 启动rviz可视化工具 5. 保存地图文件 一、SLAM技…

二进制安装指定版本的MariaDBv10.11.6

一、官网下载mariadb安装包 Download MariaDB Server - MariaDB.org 找到对应的版本 下载安装包后上传到服务器这里不再赘述。 二、安装二进制包 1、解压安装包 2、查看安装包内的安装提示文档根据提示文档进行安装 # 解压安装包 tar xf mariadb-10.11.6-linux-systemd-x8…

【抽奖项目】|第二篇

前言&#xff1a; 高并发的活动预热肯定不可以在数据库操作&#xff0c;需要redis&#xff0c;特别是这种秒杀活动更是需要注意&#xff0c;所以可以在高并发的前夕先进行活动预热。 思路&#xff1a; 1、 通过定时任务调度每分钟查询数据库也没有需要预热的活动 2、采用分布式…

异或和之和 第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 A 组

异或和之和 题目来源 第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 A 组 原题链接 蓝桥杯 异或和之和 https://www.lanqiao.cn/problems/3507/learning/ 问题描述 问题分析 要点1:异或运算 概念 异或(Exclusive OR,简称 XOR)是一种数学运算符,常用于逻辑运算与计算机…

从零到一:如何系统化封装并发布 React 组件库到 npm

1. 项目初始化 1.1 创建项目 首先&#xff0c;创建一个新的项目目录并初始化 package.json 文件。 mkdir my-component-library cd my-component-library npm init -y1.2 安装依赖 安装开发所需的依赖项&#xff0c;如构建工具、测试框架等。 npm install --save-dev webp…

现代互联网网络安全与操作系统安全防御概要

现阶段国与国之间不用对方路由器&#xff0c;其实是有道理的&#xff0c;路由器破了&#xff0c;内网非常好攻击&#xff0c;内网共享开放端口也非常多&#xff0c;更容易攻击。还有些内存系统与pe系统自带浏览器都没有javascript脚本功能&#xff0c;也是有道理的&#xff0c;…

2025-03-12 学习记录--C/C++-PTA 习题8-4 报数

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 习题8-4 报数 报数游戏是这样的&#xff1a;有n个人围成一圈&#xff0c;按顺序从1到n编好号。从第一个人开…

【js逆向】某预约票网站 (webpack技术)

1、查看数据包 calendar是需要的数据包&#xff0c;看下它的请求参数。 accTimes参数加密&#xff0c;_times是时间戳。 2、全局搜索 accTimes 关键字 3、n的定义 4、把整个js代码复制下来&#xff0c;应用到了webpack技术&#xff0c;图中的Q是n