Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

SharedPreferences 原理 源码分析 进程间通信 MD


目录

SharedPreferences

简介

在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件 存放在/data/data/ <package name> /shared_prefs目录下.

之所以说SharedPreference是一种轻量级的存储方式,是因为它在创建的时候会一次性把整个文件全部加载进内存,如果SharedPreference文件比较大,会带来以下问题:

  • 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
  • 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
  • 这些key和value会永远存在于内存之中,占用大量内存。

优化建议

  • 不要存放大的key和value,会引起界面卡、频繁GC、占用内存等等。
  • 毫不相关的配置项就不要放在在一起,文件越大读取越慢。
  • 读取频繁的key和不易变动的key尽量不要放在一起,影响速度。
  • 不要乱edit和apply,尽量批量修改一次提交,多次apply会阻塞主线程。
  • 尽量不要存放JSON和HTML,这种场景请直接使用JSON。
  • SharePreferences的commit与apply一个是同步一个是异步
  • SharedPreference无法进行跨进程通信

跨进程通信时的问题

问题

先启动主线程并获取SharedPreferences对象,然后对值进行修改,然后再启动其它进程并获取SharedPreferences对象,能够获取修改后的值,但此时如果对此值进行修改,不能对其他进程产生作用,必须等到进程重启或者app重启才能与其他进程进行数据同步。

原因

只有在创建SharedPreferences对象的时候才会从磁盘中进行读取,读取完以后值保存在内存(HashMap)当中,下次获取SharedPreferences对象优先从缓存当中获取,所以在当前进程修改了SharedPreferences的值,其他进程的SharedPreferences对象的值并不会改变。只有把当前另外的进程关闭(如:关闭手机、或杀死该app重新进入),再次创建进程时才会重新从磁盘中再次读取文件。

源码分析

通常我们获取SharedPreferences都是通过Context中的getSharedPreference方法来获取SharedPreferences对象,在Context中,getSharedPreference方法是一个抽象方法,没有具体实现:

public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

检索并保存首选项文件“name”的内容,返回SharedPreferences,您可以通过它来检索和修改其值。只有一个SharedPreferences对象实例返回给任何相同名称的调用者,这意味着他们一旦完成就会看到彼此的编辑。

Retrieve and hold the contents of the preferences file 'name', returning a SharedPreferences through which you can retrieve and modify its values. Only one instance of the SharedPreferences object is returned to any callers for the same name, meaning they will see each other's edits as soon as they are made.

我们知道Context的实现类是ContextImpl,所以直接找到ContextImpl的getSharedPreference方法。

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>(); //class ArrayMap implements Map
}
file = mSharedPrefsPaths.get(name); //根据 name 获取对应的文件
if (file == null) { //首次访问可能连文件都不存在,那么还需要创建 xml 文件
file = getSharedPreferencesPath(name); //创建文件,文件名为【name + ".xml"】
mSharedPrefsPaths.put(name, file); //保存起来
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file); //获取缓存对象
if (sp == null) { //如果内存中不存在,会创建SharedPreferencesImpl
checkMode(mode);
sp = new SharedPreferencesImpl(file, mode); //创建SharedPreferences的实例对象
cache.put(file, sp); //缓存起来
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { //11
//If somebody else (some other process) changed the prefs file behind our back, we reload it.
//This has been the historical (if ndocumented) behavior.
//如果其他人(其他进程)在我们后面更改了prefs文件,我们会重新加载它。这是历史(如果没有文档记录)的行为。
sp.startReloadIfChangedUnexpectedly();
}
return sp; //如果内存中已经存在,那么直接返回
}

可以看到,这里将SharedPreferences的实例对象SharedPreferencesImpl的先通过Map缓存起来,以后每次获取如果内存已经存在,那么直接返回,如果不存在才会重新创建。

持久化数据的更新

通常更新SharedPreferences的时候是首先获取一个SharedPreferences.Editor,利用它缓存一批操作,之后当做事务提交,有点类似于数据库的批量更新。

Editor是一个接口,这里的实现是一个EditorImpl对象,它首先批量预处理更新操作,之后再提交更新。在提交事务的时候有两种方式,一种是apply,另一种commit,两者的区别在于:前者是异步的,后者是同步的。Google推荐使用前一种,因为,就单进程而言,只要保证内存缓存正确就能保证运行时数据的正确性,而持久化,不必太及时,这种手段在Android中使用还是很常见的,比如权限的更新也是这样,况且,Google并不希望SharePreferences用于多进程,因为不安全。

无论调用哪一个方法都会调用 commitToMemory 和 enqueueDiskWrite 方法。commitToMemory 方法就是将值提交到内存当中,enqueueDiskWrite 将修改后的内容写入到磁盘当中,所以下一次取出的值是正确的。

@Override
public void apply() {
final MemoryCommitResult mcr = commitToMemory(); //提交到内存当中
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
}; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { //提交一个事务到已给线程池,之后直接返回
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
}; 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);
}

MODE_MULTI_PROCESS 标记

结论

MODE_MULTI_PROCESS只是保证了在 API 11 以前的系统上,如果sp已经被读取进内存,再次获取这个SharedPreference的时候,如果有这个flag,会重新读一遍文件,仅此而已,并不能保证跨进程通信。

解释

SharePreferences在新建时有个mode参数,可以指定它的加载模式,MODE_MULTI_PROCESS是Google提供的一个多进程模式,但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的。

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//...
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { //11
//If somebody else (some other process) changed the prefs file behind our back, we reload it.
//This has been the historical (if ndocumented) behavior.
//如果其他人(其他进程)在我们后面更改了prefs文件,我们会重新加载它。这是历史(如果没有文档记录)的行为。
sp.startReloadIfChangedUnexpectedly();
}
return sp; //如果内存中已经存在,那么直接返回
}

也就是说MODE_MULTI_PROCESS只是个鸡肋Flag,对于多进程的支持几乎为0,下面是Google文档,简而言之,就是:不要用。

文档描述

@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;

SharedPreference loading标志:设置后,即使已在此过程中加载了共享首选项实例,也会检查磁盘上的文件是否已修改。 在应用程序具有多个进程的情况下,有时需要此行为,所有进程都写入相同的SharedPreferences文件。 但是,通常在进程之间存在更好的通信形式。

SharedPreference loading flag: when set, the file on disk will be checked for modification even if the shared preferences instance is already loaded in this process. This behavior is sometimes desired in cases where the application has multiple processes, all writing to the same SharedPreferences file. Generally there are better forms of communication between processes, though.

这是Gingerbread(Android 2.3)之前的遗留(但未记录)行为,并且在针对此类版本时隐含了此标志。 对于针对SDK版本大于 Android 2.3的应用程序,如果需要,必须明确设置此标志。

This was the legacy (but undocumented) behavior in and before Gingerbread (Android 2.3) and this flag is implied when targetting such releases. For applications targetting SDK versions greater than Android 2.3, this flag must be explicitly set if desired.

MODE_MULTI_PROCESS在某些Android版本中无法可靠地工作,而且没有提供任何协调跨进程的并发修改的机制。 应用程序不应尝试使用它。 相反,他们应该使用明确的跨进程数据管理方法,例如ContentProvider。

MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

2019-2-20

SharedPreferences 原理 源码 进程间通信 MD的更多相关文章

  1. ThreadLocal 简介 案例 源码分析 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  2. Laya Timer原理 & 源码解析

    Laya Timer原理 & 源码解析 @author ixenos 2019-03-18 16:26:38 一.原理 1.将所有Handler注册到池中 1.普通Handler在handle ...

  3. 深入理解Faiss 原理&源码 (一) 编译

    目录 深入理解Faiss 原理&源码 (一) 编译 mac下安装 安装mac xcode工具包 安装 openblas 安装swig 安装libomp 编译faiss 附录 深入理解Faiss ...

  4. Mybatis Interceptor 拦截器原理 源码分析

    Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最 ...

  5. 带着萌新看springboot源码11(springboot启动原理 源码上)

    通过前面这么多讲解,springboot原理应该也大概有个轮廓了,一些基本的配置,从客户端url到controller(配置一些要用的组件,servlet三大组件,处理器映射器,拦截器,视图解析器这些 ...

  6. Spring IOC和Spring AOP的实现原理(源码主线流程)

    写在前面 正本文参考了<spring技术内幕>和spring 4.0.5源码.本文只描述原理流程的主线部分,其他比如验证,缓存什么可以具体参考源码理解. Spring IOC 一.容器初始 ...

  7. Windows虚拟地址转物理地址(原理+源码实现,附简单小工具)

                                                                                                        ...

  8. Java HashMap实现原理 源码剖析

    HashMap是基于哈希表的Map接口实现,提供了所有可选的映射操作,并允许使用null值和null建,不同步且不保证映射顺序.下面记录一下研究HashMap实现原理. HashMap内部存储 在Ha ...

  9. 【nodejs原理&源码赏析(5)】net模块与通讯的实现

    [摘要] Node.js net模块的原理及使用 示例代码托管在:http://www.github.com/dashnowords/blogs 一. net模块简介 net模块是nodejs通讯功能 ...

随机推荐

  1. node版本控制之nvm

    windows下安装nvm 用nvm-noinstall.zip安装 1.nvm是个啥?nvm是一个可以让你在同一台机器上安装和切换不同版本node的工具linux系统的github地址:点我如果你是 ...

  2. VIM 键盘符号

    :h key-notation //查询键盘符号说明<>> 等于shift + > % 是跳到对应的括号 x 是删除当前字符,即右括号 '' 是跳回左括号 x 删除左括号

  3. 《剑指offer》-找到字符串中第一个只出现一个的字符

    题目描述 请实现一个函数用来找出字符流中第一个只出现一次的字符.例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g".当从该字符流中读出 ...

  4. C#学习网站收集

    1. 大名鼎鼎的CodeGuru 号称代码领头羊 非常著名的关于程序开发的网站,大量的资料.强烈推荐 http://www.codeguru.com/ - 外文 2. Developer.com: A ...

  5. python3 使用SimpleHTTPServer搭建web服务器

    刚刚萌发了一个念头,要用python来做个web服务器,秀出自己的网页.于是,开始了我的搭建web服务器之旅. 首先,如果不想使用Apache.IIS,那就需要一个HTTP服务,而python自带了一 ...

  6. CentOS安装redis-audit 但执行时出错未解决 记录一下安装过程

    网上很多安装过程都太老了,测试很多方法终于成功了,但执行时还是出错,哪位熟悉的可以告知一下. yum install -y ruby rubygems ruby-devel git gcc gem s ...

  7. [转] Java基础知识——Java语言基础

    http://blog.csdn.net/loneswordman/article/details/9905931 http://blog.csdn.net/wanghuan203/article/d ...

  8. [CodeChef]GERALD07/[JZOJ4739]Ztxz16学图论

    题解: 考虑从小到大枚举右端点 对于每个点,令它的权值等于它的编号 那么我们可以用lct维护出一颗最大生成树 维护方法是每次插入一条判断他们在不在一颗树上 若不在直接加,若在就找到链上的最小值 之后看 ...

  9. Linux 僵尸进程的筛选和查杀

    一.筛选 ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]' 二.查杀 ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]' ...

  10. Python sys.stdout sys.stdin

    引用自:https://www.cnblogs.com/keye/p/7859181.html 引用自:https://blog.csdn.net/sxingming/article/details/ ...