前言
由于笔者目前水平限制,表达能力有限,尽请见谅。
SharedPreferences
提供了一种轻量级的数据存储方式,允许保存和获取简单的键值对。它适用于保存少量的数据,如用户设置或应用程序的配置信息。
ContextImpl
是Context
抽象类的一个具体实现。在Android中,Context
是一个抽象类,它提供了访问应用资源、启动活动、发送广播、接收意图等一系列操作的接口。Context
是一个场景描述符,它提供了与操作系统交互的接口。
本文将继续探索Framework层ContextImpl类对于getSharedPreferences的具体实现,以及SharedPreferencesImpl类的实现等。
正文
Part1
具体实现源码如下
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
对于null
名称的处理: 方法的开始部分处理了一个应用传入null
作为SharedPreferences
的名称特殊情况。
有趣的:)注释说明:在Android KitKat(API级别19)之前,如果传入的名称为null
,系统会将其转换为字符串"null.xml"作为文件名。为了保持向后兼容性,这段代码在目标SDK版本小于KitKat时,如果传入的name
为null
,会将其改为字符串"null",来保证应用的兼容性。
线程安全的实现: 方法中使用了synchronized (ContextImpl.class)
来确保线程安全。这意味着在同一时刻,只有一个线程可以执行这个同步代码块内的代码。这对于防止并发问题是必要的,因为SharedPreferences
的文件是被多个线程共享的资源。
synchronized
是Java中的一个关键字,用于控制对共享资源的并发访问,确保在同一时刻只有一个线程可以执行特定代码段。它是Java语言内置的线程同步机制的一部分。synchronized
可以用于方法或代码块,以确保线程安全,即防止多线程同时修改同一资源导致的数据不一致问题。
当一个线程进入一个同步方法或同步代码块时,它会自动获取锁。如果其他线程试图进入被锁定对象的另一个同步方法或代码块,它们将被阻塞,直到锁被释放。当当前线程离开同步区域时,锁会被自动释放,此时其他线程可以尝试获取锁。
(使用synchronized
时需要谨慎,因为不当的使用可能会导致死锁或降低系统的性能。)
另外需要注意锁定的是ContextImpl.class
这个对象,而不仅仅是它内部处理SharedPreferences
部分的代码。
缓存SharedPreferences文件路径: mSharedPrefsPaths
用于缓存每个SharedPreferences
文件的路径。如果请求的SharedPreferences
文件路径已经在mSharedPrefsPaths
中,就直接使用缓存的路径;如果不在,就生成新的文件路径,并将其添加到mSharedPrefsPaths
中。
Part2
可以发现实际上,通过name和mode获取sharedPreferences最后调用了通过file和mode获取sharedPreferecnes。
缓存: 首先,方法使用了一个名为cache
的ArrayMap
,这个映射着维护文件路径到SharedPreferencesImpl
对象的映射。这个缓存的目的是为了避免重复创建SharedPreferences
实例,提高性能。
线程安全: 使用synchronized (ContextImpl.class)
来确保操作的线程安全。这保证了在同一时间内,只有一个线程可以执行这段代码。
查找或创建实例: 方法首先尝试从缓存中获取指定文件的SharedPreferencesImpl
实例。如果实例不存在,它会进行几个检查,包括验证模式(checkMode(mode)
),并针对Android O及以上版本,检查是否在加密存储中并且用户已解锁(如果SharedPreferences
存储在加密存储中,并且用户尚未解锁,那么会抛出一个IllegalStateException
异常,提示SharedPreferences
在用户解锁之前不可用)。通过这些检查后,它会创建一个新的SharedPreferencesImpl
实例,将其添加到缓存中,并返回这个新实例。
MODE_MULTI_PROCESS : 在返回SharedPreferencesImpl
实例之前,如果指定了Context.MODE_MULTI_PROCESS
模式或应用的目标SDK版本低于Android Honeycomb(3.0),则会调用sp.startReloadIfChangedUnexpectedly()
。这是为了处理多进程访问和兼容旧版本Android的历史行为,确保如果SharedPreferences
文件在外部被修改,应用能够重新加载这些变化。MODE_MULTI_PROCESS
在新版本中已被废弃,因为SharedPreferences
默认不支持跨进程同步。
Part2 -0
这个方法获取了维护文件路径到SharedPreferencesImpl
对象的映射
@GuardedBy("ContextImpl.class")用于告诉开发者,任何访问getSharedPreferencesCacheLocked
方法内资源的操作都应该在synchronized (ContextImpl.class)
块内进行,确保线程安全。
sSharedPrefsCache
是一个全局缓存,用于存储所有应用程序包的SharedPreferences缓存。
使用应用程序包名从sSharedPrefsCache
中获取对应的缓存。如果找不到,说明这是该应用程序第一次请求SharedPreferences,需要为这个包名创建一个新的缓存并添加到sSharedPrefsCache
中。
Part3
对于SharedPreferencesImpl类,代码较多,不全部展示。
大致有如下重要变量:
- mFile & mBackupFile:表示SharedPreferences数据存储的文件及其备份文件。
- mMode:访问模式,如私有模式。
- mLock & mWritingToDiskLock:用于同步控制的锁对象。
- mMap:内存中保存的键值对数据,它在文件加载后存储SharedPreferences的当前状态。
- mLoaded:表示数据是否已从磁盘加载到内存中。
- mListeners:监听SharedPreferences变更的监听器集合。
数据加载
startLoadFromDisk() & loadFromDisk()在构造函数中调用startLoadFromDisk()
方法启动一个异步任务,从磁盘加载SharedPreferences数据到内存中。loadFromDisk()
方法实际完成加载逻辑,包括读取文件、解析XML并更新内存中的mMap
。
数据访问
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
实现了SharedPreferences
接口的各种数据访问方法(如getString
、getInt
等),这些方法首先确保数据已加载(调用awaitLoadedLocked()
),然后从mMap
中读取相应的值。
数据修改和异步提交
-
edit() & EditorImpl:
edit()
方法返回一个Editor
对象,用于修改SharedPreferences的值。EditorImpl
实现了Editor
接口,提供了修改数据和提交更改的方法(apply()
和commit()
)。 -
apply() & commit():
apply()
方法异步提交更改到磁盘,而commit()
方法同步提交。它们都使用内部的MemoryCommitResult
对象来表示提交结果,并处理磁盘写入逻辑。
磁盘写入
- enqueueDiskWrite() & writeToFile():这些方法处理将更改写入磁盘的逻辑。
enqueueDiskWrite()
方法将写入任务加入队列,而writeToFile()
实际执行磁盘写入操作,包括文件备份、写入新数据、同步文件系统等。
监听器通知
- notifyListeners():在数据成功提交到磁盘后,此方法通知所有注册的监听器数据已更改。
Part3 -0
SharedPreferences允许注册监听器来监听偏好设置的更改的实现方式:
维护一个监听器的WeakHashMap
存储所有注册的OnSharedPreferenceChangeListener
监听器。当SharedPreferences的编辑器(Editor
)提交更改时(无论是通过apply()
还是commit()
方法),会检查这个映射表,并通知所有监听器偏好设置的更改。
- 注册监听器:调用
SharedPreferences.registerOnSharedPreferenceChangeListener()
方法 - 通知监听器:在
EditorImpl
的notifyListeners()
方法中,当apply()
或commit()
成功将更改提交到内存和磁盘后,会遍历mListeners
映射表,为每个监听器调用onSharedPreferenceChanged()
方法,通知它们偏好设置已更改。
Part3 -1
apply()与commit()的区别和使用场景:
-
apply():
- 异步执行,立即返回。
- 将更改提交到内存,然后异步写入磁盘,不会告知写入成功与否。
- 如果有异常,不会中断应用,因为它不抛出异常。
- 使用场景:当需要修改SharedPreferences并希望这个操作立即返回,不需要确认数据是否成功写入磁盘时使用
apply()
。
-
commit():
- 同步执行,直到数据完全写入磁盘后返回。
- 如果写入成功,返回
true
;如果有异常,返回false
。 - 可能会阻塞调用线程直到操作完成,特别是在磁盘操作较慢的设备上。
- 使用场景:当需要确保更改被立即写入磁盘,并且关心操作的成功与否时使用
commit()
。适用于对数据完整性要求较高的情况。
Part3 -2
数据的读取和存储
SharedPreferencesImpl
通过mMap
字段来缓存SharedPreferences的数据。当应用读取偏好设置时(如getString
或getInt
等方法),SharedPreferencesImpl
会首先确保数据已从磁盘加载到内存中(通过调用awaitLoadedLocked()
方法),然后直接从mMap
中读取相应的值。
- 加载数据:通过
startLoadFromDisk()
方法启动后台线程从磁盘读取XML文件,解析后存储到mMap
中。 - 修改数据:
edit()
方法返回一个EditorImpl
实例,允许修改mMap
中的数据。修改操作首先在内存中进行,然后通过apply()
或commit()
方法将更改异步或同步地写入磁盘。 - 写入磁盘:更改提交后,
SharedPreferencesImpl
使用enqueueDiskWrite()
方法安排磁盘写入。实际写入是通过writeToFile()
方法完成的,它将内存中的数据写入到一个XML文件中。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
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;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
MemoryCommitResult mcr封装了待写入磁盘数据以及其他相关信息的对象,例如哪些键被修改了,是否有监听器需要通知等。
Runnable postWriteRunnable是一个可选的Runnable,仅在apply()
方法中提供。它在数据成功写入磁盘后执行,主要用于异步操作的场景。如果这个参数为null
,表示当前操作是一个commit()
调用,即同步写入磁盘。
核心逻辑
writeToDiskRunnable
封装了将更改写入磁盘的逻辑。首先获取mWritingToDiskLock
锁以保证写入操作的线程安全,然后调用writeToFile
方法实际写入磁盘,完成后递减mDiskWritesInFlight
计数器并可能执行postWriteRunnable
- 如果是同步提交,并且当前没有其他写入任务在执行(
mDiskWritesInFlight == 1
),则立即在当前线程执行writeToDiskRunnable
,不需要排队。 - 如果是异步提交或者存在其他写入任务,将
writeToDiskRunnable
任务排队到QueuedWork
,它会在一个后台线程上异步执行。