zoukankan      html  css  js  c++  java
  • Sp效率分析和理解

    目录介绍

    • 01.Sp简单介绍
      • 1.1 Sp作用分析
      • 1.2 案例分析思考
    • 02.Sp初始化操作
      • 2.1 如何获取sp
      • 2.2 SharedPreferencesImpl构造
    • 03.edit方法源码
    • 04.put和get方法源码
      • 4.1 put方法源码
      • 4.2 get方法源码
    • 05.commit和apply
      • 5.1 commit源码
      • 5.2 apply源码
    • 06.总结分析

    好消息

    • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
    • 链接地址:https://github.com/yangchong211/YCBlogs
    • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

    01.Sp简单介绍说明

    1.1 Sp作用分析

    • sp作用说明
      • SharedPreferences是Android中比较常用的存储方法,它可以用来存储一些比较小的键值对集合,并最终会在手机的/data/data/package_name/shared_prefs/目录下生成一个 xml 文件存储数据。
    • 分析sp包含那些内容
      • 获取SharedPreferences对象过程中,系统做了什么?
      • getXxx方法做了什么?
      • putXxx方法做了什么?
      • commit/apply方法如何实现同步/异步写磁盘?
    • 分析sp包含那些源码
      • SharedPreferences 接口
      • SharedPreferencesImpl 实现类
      • QueuedWork 类

    1.2 案例分析思考

    1.2.1 edit用法分析
    • 代码如下所示
    • 然后开始执行操作
      • A操作和B操作,在代码逻辑上应该是一样的,都是想SP中写入200次不同字段的数据,区别只是在于,A操作每次都去获取新的Editor,而B操作是只使用一个Eidtor去存储。两个操作都分别执行两次。
      • A操作和C操作,在代码逻辑上应该是一样的,都是想SP中写入200次不同字段的数据,区别只是在于,A操作每次都去获取新的Editor,而C操作是只使用一个Editor去存储,并且只commit一次。两个操作都分别执行两次。
      • B和C的操作几乎都是一样的,唯一不同的是B操作只是获取一次preferencesB对象,而C操作则是获取200次preferencesC操作。
    • 然后看一下执行结果
    • 结果分析
      • 通过A和B操作进行比较可知:使用commit()的方式,如果每次都使用sp.edit()方法获取一个新的Editor的话,新建和修改的执行效率差了非常的大。也就是说,存储一个从来没有用过的Key,和修改一个已经存在的Key,在效率上是有差别的。
      • 通过B和C操作进行比较可知:getSharedPreferences操作一次和多次其实是没有多大的区别,因为在有缓存,如果存在则从缓存中取。
    • 然后看看里面存储值
      • 其存储的值并不是按照顺序的。
    1.2.2 commit和apply
    • 代码如下所示
    • 然后看一下执行结果
    • 得出结论
      • 从执行结果可以发现,使用apply因为是异步操作,基本上是不耗费时间的,效率上都是OK的。从这个结论上来看,apply影响效率的地方,在sp.edit()方法。
    • 可以看出多次执行edit方法还是很影响效率的。
      • 在edit()中是有synchronized这个同步锁来保证线程安全的,纵观EditorImpl.java的实现,可以看到大部分操作都是有同步锁的,但是只锁了(this),也就是只对当前对象有效,而edit()方法是每次都会去重新new一个EditorImpl()这个Eidtor接口的实现类。所以效率就应该是被这里影响到了。
    1.2.3 给出的建议
    • edit()是有效率影响的,所以不要在循环中去调用吃方法,最好将edit()方法获取的Editor对象方在循环之外,在循环中共用同一个Editor()对象进行操作。
    • commit()的时候,「new-key」和「update-key」的效率是有差别的,但是有返回结果。
    • apply()是异步操作,对效率的影响,基本上是ms级的,可以忽略不记。

    02.Sp初始化操作

    2.1 如何获取sp

    • 首先看ContextWrapper源码
    • 然后看一下ContextImpl类
    • 然后接着看一下getSharedPreferences(file, mode)方法源码
    • 这段源码的流程还是清晰易懂的,注释已经说得很明白,这里我们总结一下这个方法的要点:
      • 缓存未命中, 才构造SharedPreferences对象,也就是说,多次调用getSharedPreferences方法并不会对性能造成多大影响,因为又缓存机制。
      • SharedPreferences对象的创建过程是线程安全的,因为使用了synchronize关键字。
      • 如果命中了缓存,并且参数mode使用了Context.MODE_MULTI_PROCESS,那么将会调用sp.startReloadIfChangedUnexpectedly()方法,在startReloadIfChangedUnexpectedly方法中,会判断是否由其他进程修改过这个文件,如果有,会重新从磁盘中读取文件加载数据。

    2.2 SharedPreferencesImpl构造

    • 看SharedPreferencesImpl的构造方法,源码如下所示
      • 将传进来的参数file以及mode分别保存在mFile以及mMode中
      • 创建一个.bak备份文件,当用户写入失败的时候会根据这个备份文件进行恢复工作
      • 将存放键值对的mMap初始化为null
      • 调用startLoadFromDisk()方法加载数据
    • 然后看一下调用startLoadFromDisk()方法加载数据
    • 对startLoadFromDisk()方法进行了分析,有分析我们可以得到以下几点总结:
      • 如果有备份文件,直接使用备份文件进行回滚
      • 第一次调用getSharedPreferences方法的时候,会从磁盘中加载数据,而数据的加载时通过开启一个子线程调用loadFromDisk方法进行异步读取的
      • 将解析得到的键值对数据保存在mMap中
      • 将文件的修改时间戳以及大小分别保存在mStatTimestamp以及mStatSize中(保存这两个值有什么用呢?我们在分析getSharedPreferences方法时说过,如果有其他进程修改了文件,并且mode为MODE_MULTI_PROCESS,将会判断重新加载文件。如何判断文件是否被其他进程修改过,没错,根据文件修改时间以及文件大小即可知道)
      • 调用notifyAll()方法通知唤醒其他等待线程,数据已经加载完毕

    03.edit方法源码

    • 源码方法如下所示

    04.put和get方法源码

    4.1 put方法源码

    • 就以putString为例分析源码。通过sharedPreferences.edit()方法返回的SharedPreferences.Editor,所有我们对SharedPreferences的写操作都是基于这个Editor类的。在 Android 系统中,Editor是一个接口类,它的具体实现类是EditorImpl:
    • 从EditorImpl类的源码我们可以得出以下总结:
      • SharedPreferences的写操作是线程安全的,因为使用了synchronize关键字
      • 对键值对数据的增删记录保存在mModified中,而并不是直接对SharedPreferences.mMap进行操作(mModified会在commit/apply方法中起到同步内存SharedPreferences.mMap以及磁盘数据的作用)

    4.2 get方法源码

    • 就以getString为例分析源码
    • getString方法代码很简单,其他的例如getInt,getFloat方法也是一样的原理,直接对这个疑问进行总结:
      • getXxx方法是线程安全的,因为使用了synchronize关键字
      • getXxx方法是直接操作内存的,直接从内存中的mMap中根据传入的key读取value
      • getXxx方法有可能会卡在awaitLoadedLocked方法,从而导致线程阻塞等待(什么时候会出现这种阻塞现象呢?前面我们分析过,第一次调用getSharedPreferences方法时,会创建一个线程去异步加载数据,那么假如在调用完getSharedPreferences方法之后立即调用getXxx方法,此时的mLoaded很有可能为false,这就会导致awaiteLoadedLocked方法阻塞等待,直到loadFromDisk方法加载完数据并且调用notifyAll来唤醒所有等待线程)

    05.commit和apply

    5.1 commit源码

    • commit()方法分析
      • commit()方法的主体结构很清晰简单:
        • 首先将写操作记录同步到内存的SharedPreferences.mMap中(将mModified同步到mMap)
        • 然后调用enqueueDiskWrite方法将数据写入到磁盘上
        • 同步等待写磁盘操作完成(这就是为什么commit()方法会同步阻塞等待的原因)
        • 通知监听者(可以通过registerOnSharedPreferenceChangeListener方法注册监听)
        • 最后返回执行结果:true or false
    • 接着来看一下它调用的commitToMemory()方法:
      • commitToMemory()方法主要做了这几件事:
        • mDiskWritesInFlight自增1(mDiskWritesInFlight代表“此时需要将数据写入磁盘,但还未处理或未处理完成的次数”,提示,整个SharedPreferences的源码中,唯独在commitToMemory()方法中“有且仅有”一处代码会对mDiskWritesInFlight进行增加,其他地方都是减)
        • 将mcr.mapToWriteToDisk指向mMap,mcr.mapToWriteToDisk就是最终需要写入磁盘的数据
        • 判断mClear的值,如果是true,清空mMap(调用clear()方法,会设置mClear为true)
        • 同步mModified数据到mMap中,然后清空mModified最后返回一个MemoryCommitResult对象,这个对象的mapToWriteToDisk参数指向了最终需要写入磁盘的mMap
    • 对调用的enqueueDiskWrite方法进行分析:
      • writeToFile这个方法大致分为三个过程:
        • 先把已存在的老的 SP 文件重命名(加“.bak”后缀),然后删除老的 SP 文件,这相当于做了备份(灾备)
        • 向mFile中一次性写入所有键值对数据,即mcr.mapToWriteToDisk(这就是commitToMemory所说的保存了所有键值对数据的字段) 一次性写入到磁盘。
        • 如果写入成功则删除备份(灾备)文件,同时记录了这次同步的时间如果往磁盘写入数据失败,则删除这个半成品的 SP 文件

    5.2 apply源码

    • apply()方法分析
      • 总结一下apply()方法:
        • commitToMemory()方法将mModified中记录的写操作同步回写到内存 SharedPreferences.mMap 中。此时, 任何的getXxx方法都可以获取到最新数据了
        • 通过enqueueDiskWrite方法调用writeToFile将方法将所有数据异步写入到磁盘中

    06.总结分析

    • SharedPreferences是线程安全的,它的内部实现使用了大量synchronized关键字
    • SharedPreferences不是进程安全的
    • 第一次调用getSharedPreferences会加载磁盘 xml 文件(这个加载过程是异步的,通过new Thread来执行,所以并不会在构造SharedPreferences的时候阻塞线程,但是会阻塞getXxx/putXxx/remove/clear等调用),但后续调用getSharedPreferences会从内存缓存中获取。如果第一次调用getSharedPreferences时还没从磁盘加载完毕就马上调用getXxx/putXxx,那么getXxx/putXxx操作会阻塞,直到从磁盘加载数据完成后才返回
    • 所有的getXxx都是从内存中取的数据,数据来源于SharedPreferences.mMap
    • apply同步回写(commitToMemory())内存SharedPreferences.mMap,然后把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。apply不需要等待写入磁盘完成,而是马上返回
    • commit同步回写(commitToMemory())内存SharedPreferences.mMap,然后如果mDiskWritesInFlight(此时需要将数据写入磁盘,但还未处理或未处理完成的次数)的值等于1,那么直接在调用commit的线程执行回写磁盘的操作,否则把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。commit会阻塞调用线程,知道写入磁盘完成才返回
    • MODE_MULTI_PROCESS是在每次getSharedPreferences时检查磁盘上配置文件上次修改时间和文件大小,一旦所有修改则会重新从磁盘加载文件,所以并不能保证多进程数据的实时同步
    • 从 Android N 开始,,不支持MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定, 直接抛异常

    其他介绍

    01.关于博客汇总链接

    02.关于我的博客

  • 相关阅读:
    Python 学习 —— 进阶篇(装饰器、类的特殊方法)
    Python 基础学习的几个小例子
    MyBatis——特殊传参问题小结
    为什么要有分布式事务 分布式事务解决的什么问题 一次解答
    2pc事务和3pc事务区别详解
    SPEL语法
    分布式事务框架 TX-LCN 使用
    分布式事务解决方案
    excel 使用总结
    nginx 常用配置
  • 原文地址:https://www.cnblogs.com/yc211/p/11435317.html
Copyright © 2011-2022 走看看