0%

Android 详解 SharedPreferences

1. Android 中提供的数据持久化方式有(不考虑存储到手机 SD 卡的情况)

  • 文件存储

    • Android 中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有文件都是原封不动保存到文件中的,适合存储一些简单的文本数据或二进制数据。如果使用文件存储的方式存储一些较为复杂的文本文本数据,需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来(不常用)
    • 核心技术是 Context 类中提供的 openFileInput() 方法和 openFileOutput() 方法,加上 Java 的各种流操作
    • 默认存储到 /data/data/<package name>/files/ 目录下,可使用 AS 的 Android Device Monitor 工具的 File Explorer 标签页查看
  • SharedPreferences 存储

    • 使用键值对的方式存储数据,支持 intfloatlongboolean 4 个数据类型和字符串 String 的存储
    • 核心技术是 SharedPreferences 类,SharedPreferences 是一个单例
    • 默认存放在 /data/data/<package name>/shared_prefs/ 目录下,同样可以借助 AS 的 ADM 工具查看
  • SQLite 数据库存储

    • SQLite 数据库是内置在 Android 系统中的一款轻量级的关系型数据库,运算速度快,占用资源少,通常只需要几百 K 的内存就足够了,特别适合在移动设备上使用。 SQLite 支持标准的 SQL 语法,遵循数据库的 ACID 事务,简单易用,甚至不用设置用户名和密码就可以
    • 核心技术是 SQLiteOpenHelper 抽象类,需要创建它的实现类,实现 onCreate() 方法和 onUpgrade() 方法
    • 默认存放在 /data/data/<package name>/databases/ 目录下,同样可以借助 AS 的 ADM 工具查看

2. 写一个使用文件存储技术实现数据持久化的 Demo

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
public class MainActivity extends AppCompatActivity {
private EditText edit;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
String inputText = load();
if(!TextUtils.isEmpty(inputText)) {
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeed", Toast.LENGTH_SHORT).show();
}
}

public String load() { // 读操作
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();

// Context 类的 openFileInput() 方法只接收一个参数,即要读取的文件名,然后系统会自动到 "/data/data/<package name>/files/" 目录下去加载这个文件
try {
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while((line=reader.readLine()) != null) {
content.append(line);
}
} catch(IOException e) {
e.printStackTrace();
} finally {
if(reader != null) {
try {
reader.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return content.toString();
}

@Override
protected void onDestroy() {
super.onDestroy();
String inputText = edit.getText().toString();
save(inputText);
}

public void save(String inputText) { // 写操作
FileOutputStream out = null;
BufferedWriter writer = null;

// Context 类的 openFileOutput() 方法接收两个参数,第一个是文件名,这个文件名不可以包含路径,因为所有的文件都是默认存储到 "/data/data/<package name>/files/" 目录下的
// 第二个参数是文件的操作模式,主要有两种模式可选:MODE_PRIVATE 和 MODE_APPEND
// 其中 MODE_PRIVATE 是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容;MODE_APPEND 表示如果该文件存在,就忘文件里追加内容,不存在就创建新文件
// 其实文件的操作模式本来还有两种:MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全漏洞,已在 Android 4.2 版本中被废弃
try {
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
} catch(IOException e) {
e.printStackTrace();
} finally {
try {
if(writer != null) {
writer.close();
}
} catch(IOException e) {
e.printStackTrace();
}
}
}
}

3. Android 中获取 SharedPreferences 对象的方法有

  • Context 类中的 getSharedPreferences() 方法

    • 此方法接收两个参数,第一个参数用于指定 SharedPreferences 文件的名称,如果指定的文件不存在则会创建一个
    • 第二个参数用于指定操作模式目前只有 MODE_PRIVATE 这一种模式可选,它是默认的操作模式,和直接传入 0 的效果相同,表示只有当前的应用程序才可以对这个 SP 文件进行读写
    • 其他几种模式都已被废弃MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 这两种模式是在 Android 4.2 版本中被废弃的;MODE_MULTI_PROCESS 模式是在 Andorid 6.0 版本中被废弃的,即禁止在多进程之间使用 SharedPreferences 共享数据
  • Activity 类中的 getPreferences() 方法:这个方法只接收一个操作模式参数,因为使用这个方法时会自动将当前 Activity 的类名作为 SP 的文件名

  • PreferenceManager 类中的 getDefaultSharedPreferences() 方法:这是一个静态方法,它接收一个 Context 参数,并自动使用当前应用程序的包名作为前缀来命名 SP 文件

4. 写一个使用 SharedPreferences 存储技术实现数据持久化的 Demo

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
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button saveDate = (Button) findViewById(R.id.save_data);
saveData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreference.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name", "Tom");
editor.putInt("age", 28);
editor.putBoolean("married", false);
editor.apply();
}
});

Button restoreData = (Button) findViewById(R.id.restore_data);
restoreData.setOnClickListener(new View.OnClickListener() {
SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
String name = pref.getInt("name", "default value");
int age = pref.getInt("age", 0);
boolean married = pref.getBoolean("married", false);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "age is " + age);
Log.d("MainActivity", "married is " + married);
});
}
}

5. 使用 SharedPreferences 提交数据时,apply() 方法和 commit() 方法的区别是?各自使用的场景是

  • 参考:《阿里巴巴 Android 开发手册》

  • 结论:使用 SP 提交数据时,尽量使用 Editor#apply() 方法,而不是 Editor#commit() 方法(IDE 也提示使用 apply() 方法)。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit() 方法

  • 说明

    • 使用 apply() 方法进行提交会先写入内存,然后异步写入磁盘;commit() 方法是直接写入磁盘。因为 IO 操作一般比较费时,所以如果操作频繁的话,apply() 的性能会优于 commit() 的性能
    • apply() 会将最后修改内容写入磁盘,但是如果希望立刻获取存取的结果,并据此做相应的其他操作,应当使用 commit()
    • apply() 方法可能会执行失败,如果失败不会受到错误回调
  • 举例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 正例
    public void updateSettingsAsync() {
    SharedPreferences sp = getSharedPreferences("settings", Activity.MODE_PRIVATE);
    SharedPreferences.Editor editor = sp.edit();
    editor.putString("id", "foo");
    editor.apply();
    }

    public void updateSettings() {
    SharedPreferences sp = getSharedPreferences("settings", Activity.MODE_PRIVATE);
    SharedPreferences.Editor editor = sp.edit();
    editor.putString("id", "foo");
    if(!editor.commit()) {
    Log.e(LOG_TAG, "Failed to commit settings changes");
    }
    }

    // 反例
    editor.putLoong("key_name", "long value");
    editor.commit();

6. 滥用 SharedPreferences 的场景有哪些?后果是?解决方案?(性能优化相关)

  • 场景1:存储超大的 value

    • 后果:会占用内存,引起界面卡顿、频繁 GC
    • 方案:Don’t do this
  • 场景2:存储 JSON/HTML 等特殊符号很多的 value

    • 后果:会导致读取速度急剧下降
    • 方案:应直接使用 JSON
  • 场景3:多次 edit()、多次 apply()/commit()

    • 后果:占用内存,界面卡顿(尤其是 commit() 方法)
    • 方案:应该尽量批量修改一次提交,避免产生过多 Editor 对象
  • 场景4:使用 MODE_MULTI_PROCESS 跨进程

    • 后果: Androd 6.0 版本已废弃这个 flag
    • 方案:Don’t do this,应使用类似 ContentProvider 这些
  • 参考:请不要滥用 SharedPreferences

7. SharedPreferences 的缺点

  • 跨进程不安全。由于没有使用跨进程的锁,即使使用 MODE_MULTI_PROCESS,SP 在跨进程频繁读写有可能导致数据全部丢失。根据线上统计,SP 大约有 1/10000 的损坏率。Android 系统不把 SP 设计成跨进程安全的原因之一是因为官方更希望开发者在这个场景使用 ContentProvider
  • 加载缓慢。SP 文件的加载使用了异步线程,而且加载线程并没有设置线程优先级,如果此时主线程读取数据就需要等待文件加载线程的结束。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50~100 ms,建议提前用异步线程预加载启动过程用到的 SP 文件
  • 全量写入。无论是调用 commit() 还是 apply(),即使只改动其中的一个条目,都会把整个内容全部写到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,即没有使用缓存 buffer 的策略,这也是性能差的重要原因之一
  • 卡顿。由于提供了异步落盘的 apply 机制,在崩溃或其他一些异常情况可能会导致数据丢失。所以当应用受到系统广播或者被调用 onPause() 等一些时机,系统会强制把所有的 SP 对象数据落地到磁盘。如果没有落地完成,此时主线程会被一直阻塞。这样非常容易造成卡顿,甚至是 ANR,从线上数据来看 SP 卡顿占比一般会超过 5%

8. 微信开源的 MMKV 概述

-------------------- 本文结束感谢您的阅读 --------------------