SharedPreferences
MoMo Lv5
  1. SharedPreference是线程安全的吗?⭐⭐⭐
  2. SharedPreference的apply和commit的区别?commit 一定会在主线程操作嘛?⭐⭐⭐⭐⭐
  3. SharedPreferences 是如何初始化的,它会阻塞主线程吗?⭐⭐⭐
  4. 每次获取 SP 对象真的会很慢吗?⭐⭐⭐
  5. 在使用时需要注意哪些问题,以及有什么优化点呢?⭐⭐⭐⭐

SharedPreferences 是什么?怎么用?

  1. 数据格式:XML格式保存
  2. 初始化:子线程使用I0读取整个文件,进行XML解析,存入内存Map集合
  3. 保存:commit同步提交,阻塞主线程;apply异步提交,无法获取结果且可能数据丢失
  4. 更新:把Map中的数据,全部序列化为XML,覆盖文件保存。(全量更新)

Android的永久性存储方式有哪些:

  • File
  • SharedPreferences
  • SQlite
  • 网络
  • ContentProvider

SharedPreferences是基于key-value 键值对生成的一个xml文件,保存在/data/data/packageName/shared_prefs目录下,适合保存少量数据,且数据格式相对简单。

1
2
3
4
5
6
7
SharedPreferences sharedPreferences = context.getSharedPreferences(“xurui”, Context.MODE_PRIVATE); //1
SharedPreferences.Editor editor = sharedPreferences.edit(); //2
editor.putString("key", "value"); //3
editor.apply(); //4

//使用
String value = sharedPreferences.getString("key", "defaultValue"); //5

SharedPreferences本身是一个接口,可以通过getSharedPreferences()获取实例,原型是:

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

其中name是最终生成的xml文件的文件名,mode代表不同的存储方式,查看源码有:

1
2
3
4
5
6
7
8
@IntDef(flag = true, prefix = { "MODE_" }, value = {
MODE_PRIVATE,
MODE_WORLD_READABLE,
MODE_WORLD_WRITEABLE,
MODE_MULTI_PROCESS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PreferencesMode {}

mode分成以下5种类型:

  • MODE_PRIVATE:表示该 SharedPreferences数据只能被本应用读写,或者有相同用户ID的应用读写;
  • MODE_WORLD_READABLE:全局读,允许所有其他应用程序对创建的文件具有读访问权限,该模式官方强烈建议不使用,因为会造成安全问题;
  • MODE_WORLD_WRITEABLE:全局写,允许所有其他应用程序对创建的文件有写访问权限,官方同样建议不使用;
  • MODE_MULTI_PROCESS:可以实现多进程访问SharedPreferences数据的问题,但是这种方式的多进程共享数据可能会出现数据不一致的问题,也不可靠,现在也不使用了;
  • MODE_APPED:该模式不在规定的PreferencesMode模式里,但也可以使用,如果文件存在且对应的key也存在,则可以在对应的value数值追加新的内容,不同于MODE_PRIVATE,MODE_PRIVATE会把旧的内容覆盖掉。

接着,获取到的SharedPreferences实例本身仅支持获取数据,如果需要修改或者储存,需要通过SharedPreferences.Editor来实现,同样Editor也是接口,可以通过sharedPreferences.edit()获取实例,并通过putString修改内容,最后执行apply()即可(apply()下面会分析)。如此我们就完成了一对key-value 键值对的存放,到/data/data/packageName/shared_prefs下就可以找到一个叫“xurui.xml”的文件,里面就存放着刚添加的键值对。

源码分析

获取SharedPreferencesImpl实例

context.getSharedPreferences(“xurui”, Context.MODE_PRIVATE):
/frameworks/base/core/java/android/app/ContextImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public SharedPreferences getSharedPreferences(String name, int mode) { //6
if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>(); //7
}
file = mSharedPrefsPaths.get(name); //8
if (file == null) { //9
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode); //10
}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); //11
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
sp = new SharedPreferencesImpl(file, mode); //12
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}

@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>(); //13
}

final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName); //14
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}

上面总共有3个函数,环环相扣,context.getSharedPreferences(“xurui”, Context.MODE_PRIVATE)对应注释6.如上所述,这里“xurui”是指一个xml文件的名字,注释7的mSharedPrefsPaths是一个Map数据,其中key对应文件名称,value则是对应的xml文件。如果输入的文件名存在,则获取出对应文件,如注释8.如果文件不存在则创建并保存,对应注释9.最后获取到的文件传入注释10,也就是上面的第2个函数。

进入函数2,看看注释11,这一行需要重点理,其中:

  • File:xml文件名;
  • SharedPreferencesImpl:xml文件名在内存对应的实例,源码并非简单的对xml文件进行读写,而是每个文件会创建专门的实例来执行各种操作和判断;

getSharedPreferencesCacheLocked()做了什么?
首先会创建sSharedPrefsCache,这也是一个Map,其key是包名,value是packagePrefs,对应注释14,其中:

  • sSharedPrefsCache:定义如下,也是在ContextImpl.java中定义,因为一个进程只有一个ContextImpl对象,一个ContextImp只定义一个sSharedPrefsCache,所以一个进程也就只有一个sSharedPrefsCache;
1
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
  • packageName:一个进程只有一个sSharedPrefsCache,但是可能有多个应用,因此需要用包名做区分;
  • SharedPreferencesImpl:xml文件名在内存对应的实例,源码并非简单的对xml文件进行读写,而是每个文件会创建专门的实例来执行各种操作和判断;
  • packagePrefs: SharedPreferences 文件与 SharedPreferences 实例对象之间的映射关系;

如果getSharedPreferencesCacheLocked()中找不到输入文件名对应的SharedPreferencesImpl实例,则在注释12根据文件名和模式,新创建一个实例并返回。

过程

首先sSharedPrefsCache会保存从磁盘加载到内存的 SharedPreferences 对象,该对象保存所在进程所有应用程序的SharedPreferences 对象,通过当前进程的包名获取对应的cache,对应注释11。获取到后,又因为一个应用程序可能有多个SharedPreferences的xml文件,可通过传入文件名获取到最终的SharedPreferencesImpl实例,对应注释10.

同时有一点需要注意,ContextImpl 类中没有定义将 SharedPreferences 对象移除 sSharedPrefsCache 的方法,因此sSharedPrefsCache一旦从磁盘加载到内存后,直到进程销毁之前都会保存在内存,因此SP对象的使用都是在内存中使用,而不会第二次出现从磁盘读取

SharedPreferencesImpl实例分析

拿到SharedPreferencesImpl实例后
frameworks/base/core/java/android/app/SharedPreferencesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk(); //15
}

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

private void loadFromDisk() { //17
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}

// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}

Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}

synchronized (mLock) {
mLoaded = true; //18
mThrowable = thrown;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll(); //19
}
}
}

通过文件名获取该文件对应的SharedPreferencesImpl实例,此时从构造函数执行到[注释15]后会创建一个子线程开始读取磁盘中的xml文件,也就是[注释17]的loadFromDisk(),此时就会在子线程里读取磁盘里的xml文件转换为内存中的SharedPreferencesImpl实例。此时迎来一个关键问题:

SharedPreferences实例初始化过程虽然在子线程执行,但真的不会对主线程造成阻塞吗?

既然这么问了,想必就是子线程中的执行SharedPreferences实例初始化过程确实可能对主线程造成阻塞了。先注意[注释16]的mLoaded = false,然后一开始就有说SharedPreferences修改或者储存,需要通过SharedPreferences.Editor来实现,看看Editor的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   @Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}

private void awaitLoadedLocked() {
...
while (!mLoaded) { //20
try {
mLock.wait(); //21
} catch (InterruptedException unused) {
}
}
...
}

因此,如果我们在主线程使用SP存储一个数据时:

1
2
SharedPreferences.Editor editor = sharedPreferences.edit();  //22
editor.putString("key", "value").apply();

执行到[注释22]时最终会执行到[注释20],此时有两种情况:

  • mLoader = true:此时正常返回EditorImpl,可正常存储数据;
  • mLoader = false:此时会执行[注释21]的wait()开始阻塞,只有[注释17]的loadFromDisk()在子线程中完成从磁盘加载完数据到内存之后,mLoaded 才会被置为 true,对应[注释18],并最终执行到[注释19],重新唤醒edit()方法。

因此,虽然 SharedPreferences实例初始化过程虽然在子线程执行,此时自然不会阻塞主线程,但是如果初始化过程还没完成,主线程就要开始修改或者读取某个SharedPreferences的数值时,主线程就会被阻塞!!直到子线程中加载好文件

读写数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SharedPreferencesImpl(File file,int mode){
mFile = file
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}

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

startLoadFromDisk中启动子线程执行IO操作(耗时)
loadFromDisk()方法中同样使用IO线程,加载FileInputStream再包装成BufferedInputstream,解析成xml格式保存在map中

1
2
3
4
str = new BufferedInputstream(
new FileInputStream(mFile),size:16*1024);
)
map=(Map<String,object>)XmlUtils.readMapXml(str);

最终通过map读取数据

1
2
3
4
5
6
7
8
@Override
public int getInt(string key, int defValue){
synchronized(mLock){
awaitLoadedLocked();
Integer v=(Integer)mMap.get(key)
returnv!=null?v:defValue;
}
}

每次编辑都会创建一个新的EditorImpl并且进行一次IO操作,严重影响性能,需要避免频繁的调用。
如果在一个循环里,或者在onDraw中调用,会导致程序出现卡顿

创建EditorImpl过程中如果没有发现对象逃逸,会导致gc频繁的运行,最终导致卡顿
如果是标记清除算法会导致OOM

1
2
3
4
5
6
7
@Override
public Editoredit(){
synchronized(mLock){
awaitLoadedLocked();
}
return new EditorImpl();
}

存入到临时变量mModifiéd,通过apply/commit写入到文件实现持久化

1
2
3
4
5
6
7
8
9
@0verride
public Editor putInt(string key, int value) {
synchronized(mEditorLock){
mModifiéd.put(key, value);
return this;
}
}

edit.putInt(String.value0f(i),random.nextInt()).commit();

commit和apply

上面我们执行了editor.putString(“key”, “value”).apply(),也就是设置好数值后,需要执行apply()才生效,那么除了apply()还有commit()
/frameworks/base/core/java/android/app/SharedPreferencesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void apply() {
final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory(); //23
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await(); //25
} catch (InterruptedException ignored) {
}

if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};

QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //24

// 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);
}

public boolean commit() {
long startTime = 0;

if (DEBUG) {
startTime = System.currentTimeMillis();
}

MemoryCommitResult mcr = commitToMemory(); //23

SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); //24
try {
mcr.writtenToDiskLatch.await(); //25
} 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;
}

对比两个函数,都有相同的[注释23]commitToMemory()和[注释24]enqueueDiskWrite(),前者从命名就可以知道是将数据提交到内存里,然后再通过后者将需要写入磁盘的任务进行排队。看看后者的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null); //26

final Runnable writeToDiskRunnable = new Runnable() { //27
@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) { //28
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1; //29
}
if (wasEmpty) {
writeToDiskRunnable.run(); //30
return;
}
}

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); //31
}

从[注释26]知道第二个参数如果为 null 就说明来自 commit,如果非空就说明来自 apply。然后再[注释27]创建一个writeToDiskRunnable将数据从内存写入磁盘,并在[注释30]执行,从代码可以知道只有在[注释28]isFromSyncCommit为true时,也就是此时是该函数是来自commit。以及[注释29]mDiskWritesInFlight == 1。那么mDiskWritesInFlight何时为1呢?

通过阅读源码知道,当apply()和commit()执行了commitToMemory()时,mDiskWritesInFlight会加1,代表写入内存的数据数量加1,当每次创建一个writeToDiskRunnable时,mDiskWritesInFlight会减1。 因此mDiskWritesInFlight为1就代表前面的提交到内存的修改都已经提交的磁盘上了。此时如果是来自commit,写磁盘任务就直接在当前线程即主线程里执行了,对应[注释30]。然而如果此时是来自commit,但mDiskWritesInFlight不等于1,则执行[注释31]和 apply 一样添加到 QueueWork 里,其实就是异步执行了。

这时候可以发现commit()方法如果执行[注释30]是在主线程执行的,如果执行[注释31]则是在子线程执行的。为何commit不全部都在主线程执行呢?

这是因为如果commit在主线程执行,apply在子线程执行,那么当执行了apply后马上执行commit,就可能会导致apply 的数据在 commit 之后被写入到磁盘中,磁盘中的数据是错误的,而且和内存中的数据不一致。而当两者在同一个线程中执行时,会通过[注释25] mcr.writtenToDiskLatch.await()来保持同步。

还有最后一点要注意,apply没有返回值,但commit返回boolean表明修改是否提交成功。最后汇总下两者的区别:

  • apply没有返回值,commit有返回值(Boolean):需要确保提交成功且有后续操作的话,建议用commit;
  • apply是异步提交到磁盘,commit是同步提交到磁盘,因此commit效率比较低(数据量大的时候会出现ANR),如果不需要返回值,或者希望效率高,建议使用apply。
  • commit不一定在主线程执行;
    • 会把一次提交包装成Runnable(任务),将这个任务放到QueueWork(队列)中,在QueueWork的子线程中排队执行
    • 涉及到Activity之间的跳转,以AB两个Activity为例,从A跳转到B经过的生命周期是A:onPause B:onCreate onStart onResume A:onStop
    • handlePauseActivity中会执行QueuedWork.waitToFinish();方法。也就是说如果QueueWork中排队执行的任务没有执行完,这个waitToFinish会一直等待直到执行结束。即使apply是异步执行的,但是在发生Activity的跳转时会在主线程等待才能执行后面的操作,所以也有可能导致ANR,导致未执行完的任务数据会丢失

生命周期怎么被调起的? / Activity启动流程

AMS通过Binder机制,通知到ActivityThread#ApplicationThread
ApplicationThread中对应的方法通过handler发message调起Activity

总结

  1. 在主线程中调用apply方法,在子线程中直接调用commit方法。

  2. 一个进程只有一个sSharedPrefsCache,但是可能有多个应用,因此需要用包名做区分;一个应用有可能有多个SP的xml文件,因此需要用文件名做区分。最后得到某个SP的xml文件在内存中的实例SharedPreferencesImpl。该实例一旦加载到内存后就一直保存在内存,直到这个进程销毁。但也因此如此,在SharedPreferencesImpl初始化的过程中会产生大量的临时对象,导致频繁GC,引起界面卡顿,同时占用大量内存。好处就是每次获取SP对象都很快,只是文件太大的话,加载时就比较慢,因此大量数据时不建议用SP,有助于减少卡顿 / ANR。JSON或者HTML也不建议用SP,应该直接用JSON。

  3. SharedPreferences 和 Editor 都只是接口,真正的实现在 SharedPreferencesImpl 和 EditorImpl ,SharedPreferences 只能读数据,它是在内存中进行的,Editor 则负责存数据和修改数据,分为内存操作和磁盘操作。且不要高频调用edit(),而是每次执行edit()后多次执行putXXX;

  4. SharedPreferencesImpl 实例化过程会启动子线程从磁盘读取xml文件内容,读取时因是在子线程运行,所以不会阻塞主线程,但如果此时没读取完就开始获取或者修改数值,就会引起主线程阻塞;

  5. 修改完内容后,在提交的时候分为 commit 和 apply,两者都会先把修改保存到内存,只不过 apply 是异步写磁盘,而 commit 可能是同步写磁盘也可能是异步写磁盘;不要每次putString后都apply,而是批量修改后一起提交;

  6. SP 的读写操作是线程安全的,内部用了很多synchronized锁来实现线程同步。但进程不安全。文章一开始说的MODE_MULTI_PROCESS模式,也只是保证API 11以前的老系统上,每次获取这sharedPreference都会重新读一遍文件罢了。所以Google官方推荐使用ContentProvider来实现SharedPreference的进程同步,ContentProvider中有一个Bundle call(String method, String arg, Bundle extras)方法,我们可以重写该方法,根据传入的不同参数实现对sp的增删改查。

  7. ANR 容易发生的地方:

    • 修改或者获取数据时,执行edit()会调用 awaitLoadedLocked ,等待首次SP文件创建与读取操作完成;

    • apply 虽然是异步的但是可能会在 Service Activity 等生命周期期间mcr.writtenToDiskLatch.await() 等待过久

    • commit 最终会调用writeToFile(),该方法是磁盘操作,如果数据量太大会很耗时;

    • 如果主线程直接getSharedPreferences(),此时如果SP文件很大,那么自然也会很耗时;

    • 所以SP用于轻量数据存储操作,就非常的合适。

多进程方案

ContentProvider

使用ContentProvider进行跨进程通信,把其它进程的读写sp都统一到某个进程上。

MMKV

使用共享内存进行读写sp。

优化

  1. 比XML更精简的数据格式

    XML繁琐,冗余信息多,数据量大,占用空间多

  2. 高效的文件操作

    sp使用传统的方式:子线程+IO的方式
    其他的方式:NIO(零拷贝技术):FileChannel.transformTO

  3. 更优的数据更新方式

    局部更新

Powered by Hexo & Theme Keep
Unique Visitor Page View