SharedPreference原理,commit与apply的区别是什么?使用时需要有哪些注意?
这道题想考察什么?
1、对SharedPreference原理的掌握;
2、项目开发中对使用的技术是否有深入了解,掌握其隐患,而不仅仅只是调API;
考察的知识点
IO、并发编程、数据序列化、数据持久化与性能优化
考生如何回答
SharedPreferences
作为Android系统的轻量级数据存储方式之一,能够比较方便的存取一些简单的Key-Value数据。先来对SP按照使用流程进行梳理:
SharedPreferences
保存的文件为XML数据,其内容如下:
初始化
首先SharedPreference
本身是一个接口,在Android当中的实现类为:android.app.SharedPreferencesImpl
。context.getSharedPreferences(String name, int mode)
获得一个SP实例的时候,会以传入的name
作为文件名,创建**/data/data/<程序包名>/shared_prefs/{name}.xml** 对应的File对象,同时以此File对象,执行SharedPreferencesImpl
构造方法并且将加入到缓存中。
而SharedPreferencesImpl
的构造方法为:
构造方法里面创建了mMap
的内存缓存,后面存放及获取值的时候都是从这个缓存里面维护的, startLoadFromDisk()
就是从文件读取存储内容赋值给mMap
的,看看其实现:
所以SP在初始化时,其实就是进行利用IO对一个XML文件进行读取,IO操作本身时耗时操作(为什么IO是耗时操作?),因此需要开启子线程。在子线程中需要对读取到的内容进行XML解析,将解析到的数据保存在Map集合当中。
读取数据
在初始化完成之后,获得了mMap集合,读取数据则可以直接从此map集合中获取,以获取Int型数据为例:
因为初始化是在IO子线程中进行,因此我们需要考虑多线程并发问题,在getInt
与初始化时均有使用同一对象:mLock
进行了加锁的操作。并且我们注意到在IO子线程中,读取文件代码时未加锁,为了防止在其他线程中调用getInt
获取值时进入synchronized
代码块,但是mMap还没有赋值,在synchronized
代码块中还进行了wait()
等待。那么此处如若SP在进行初始化时,我们在主线程读取数据,读取数据需要等待sp的初始化完成。因此我们应该避免使用SP保存复杂、大量的数据。
写入数据
保存新数据或者更新原本的Key需要使用SharedPreferences.edit()
:可以返回一个Editor
对象,而Editor
的接口实现类为EditorImpl
。
此处我们需要小心的是避免频繁的调用edit()
方法,因为每次调用都会产生一个EditorImpl
对象,存在内存抖动的风险。
在获得Editor对象后,使用putXX
方法进行新数据的写入:
在写入时,putXX
仅仅只是在内存中使用Map集合mModified
记录新数据。最终需要将数据持久化到文件中,需要执行commit或者apply方法。
commit提交
apply提交
从源码中能够发现,其实无论是 SP 的 commit 还是 apply 最终都会调用 enqueueDiskWrite
方法,区别是 commit 方法调用传递的第二个参数为 null。enqueueDiskWrite
方法内部也是根据第二个参数来区分 commit 和 apply 的,如果是 commit 则会同步的执行 writeToFile,apply 则会将 writeToFile 加入到一个任务队列中异步的执行:
writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork
中的这个等待任务移除。
SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork(QueuedWork.addFinisher(awaitCommit))
中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。而Activity onPuase、onStop 以及 Service 处理 onStop,onStartCommand 等情况下,就会执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。 因此apply 调用次数过多也会容易引起 ANR 问题。
commit与apply的区别
- apply没有返回值,因此无法获知提交结果,而commit返回boolean表明修改是否提交成功;
- apply使用异步真正提交到文件, 而commit是同步的提交,因此需要等待正在处理的commit保存到文件后才会执行后续的代码。
使用注意事项
1、getXXX() 方法可能会导致主线程阻塞
根据上文中初始化得知,getXXX需要等待初始化完成,因此如果SP数据量过多,需要更多的时间,此时调用getXXX会等待初始化完成。
2、SP 不能保证类型安全
因为使用相同的 key 进行操作的时候,putXXX 方法可以使用不同类型的数据覆盖掉相同的 key。
3、SP 加载的数据会一直留在内存中
初始化时缓存在mMap中,占用内存。
4、避免频繁调用editor与commit方法
每次editor都会创建EditorImpl对象,而每次commit则会将所有数据进行序列化并且使用IO(写文件)。
5、apply方法也可能ANR
因为apply会调用QueuedWork.addFinisher(awaitCommit),在Activity执行onStop时会需要等待apply完成执行完awaitCommit才能继续往后执行。