一共有三种技术,分别是文件、SharedPreferences
和数据库。
1. 文件
在Context
类中提供了openFileOutput
和openFileInput
的方法,可以存储和读取指定的文件中的数据,默认文件存储在data/data/package name/files/
目录下,故而只需要指定文件名即可。文件操作模式有MODE_PRIVATE
、MODE_APPEND
两种可选。默认为第一个。
这种方式属于程序私有目录下存储和读取,故而不需要读写权限,永久存储,即使应用被卸载,存储的数据依然存在。
1.1 写入
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
Button button = findViewById(R.id.id_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FileOutputStream outputStream = null;
BufferedWriter bufferedWriter = null;
try {
outputStream = openFileOutput("text.md", MODE_PRIVATE);
bufferedWriter = new BufferedWriter(new
OutputStreamWriter(outputStream));
bufferedWriter.write("This is the datas.");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
assert bufferedWriter != null;
bufferedWriter.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
1.2 读取
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
Button button = findViewById(R.id.id_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FileInputStream input = null;
BufferedReader bufferedReader = null;
try {
input = openFileInput("text.md");
bufferedReader = new BufferedReader(new InputStreamReader(input));
String s = bufferedReader.readLine();
button.setText(s);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bufferedReader.close();
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
上面案例中,为了操作字符方便,所以使用了包装流对象。
上面两个案例中,均使用的字符流。那么字符流和字节流有什么区别?
字符流是由Java虚拟机将字节流转换得到的,这个过程比较耗时,并且,如果我们不知道流的编码类型,就很容易出现乱码问题。如果音频、图片、视频等文件使用字节流比较好,而如果涉及到字符,使用字符流比较好。
这里衍生下:
1.3 在Java
中获取键盘输入的两种方式
在Java
中键盘输入也是数据流的方式,通常有两种方式获得:
Scanner
BufferedReader
// 方式一:
Scanner input = new Scanner(System.in);
String s = input.nextLine();
// 方式二:
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
try {
String s1 = bf.readLine();
} catch (IOException e) {
e.printStackTrace();
}
第二种方式需要自己处理异常,而第一种不需要。故而通常第一种方式比较常用。
2. SharedPreferences
存储
SharedPreferences
采用键值对的方式进行存储。同时,支持多种不同的数据类型存储。
SharedPreferences
文件默认存储在data/data/package name/shared_prefs/
目录下。操作模式目前只有MODE_PRIVATE
一种模式。其值为0,表示只有当前程序才可以对这个文件进行读写。其余几种操作模式已经被废弃。默认是没有考虑同步互斥,它基于单个文件,不支持多进程。在getSharedPreferences
的时候,会强制让SharedPreferences
进行一次读取操作,从而保证数据是最新的。是轻量级的存储类,特别适合用于保存软件配置参数。
1. 优点:
- 轻量级,以键值对的方式进行存储,使用方便,易于理解;
- 采用的是
xml
文件形式存储在本地,程序卸载后会也会一并被清除,不会残留信息;
2. 缺点:
-
由于是对文件
IO
读取,因此在IO
上的瓶颈是个大问题,因为在每次进行getSharedPreferences
和commit
时都要将数据从内存写入到文件中,或从文件中读取; -
多线程场景下效率较低,在
getSharedPreferences
操作时,会锁定SharedPreferences
对象,互斥其他操作,而当put
,commit
时,则会锁定Editor
对象,使用写入锁进行互斥,在这种情况下,效率会降低 -
不支持跨进程通讯;
-
由于每次都会把整个文件加载到内存中,因此,如果
SharedPreferences
文件过大,或者在其中的键值对是大对象的json
数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁GC
,导致的界面卡顿。
以上优缺点总结来自:https://blog.csdn.net/qq_24349189/article/details/99674662
在Android
种提供了两种方式来获取SharedPreferences
对象:
Context
类中的getSharedPreferences()
方法;Activity
类中的getPreferences()
方法;
在SharedPreference
种提交数据有两种方式,分别是apply
和commit
。下面是两种方式的差别:
SharedPreference
相关修改使用 apply
方法进行提交会先写入内存,然后异步写入磁盘,commit
方法是直接写入磁盘。如果频繁操作的话 apply
的性能会优于 commit
,apply
会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit
。
commit
和apply
虽然都是原子性操作,但是原子的操作不同,commit
是原子提交到数据库,所以从提交数据到存在Disk
中都是同步过程,中间不可打断。- 而
apply
方法的原子操作是原子提交的内存中,而非数据库,所以在提交到内存中时不可打断,之后再异步提交数据到数据库中,因此也不会有相应的返回值。 - 所有
commit
提交是同步过程,效率会比apply异步提交的速度慢,但是apply
没有返回值(commit
会返回一个boolean
值),永远无法知道存储是否失败。 - 在不关心提交结果是否成功的情况下,优先考虑
apply
方法。
以上四条总结来源:SharedPreferences中的commit和apply方法
2.1 使用Activity
的getPreferences
方式:
// TestActivity.java
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor edit = preferences.edit();
edit.putString("key", "value");
edit.commit();
}
});
可以发现,这里我并没有指定文件名,但是操作提交后:
不难发现使用Activity
的getPreferences
来获取SharedPreference
对象,不用指定xml
文件名,但是会使用当前的Activity
的类名作为当前SharedPreference
的文件名。
2.2 使用Context
类的getSharedPreferences()
方法;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences preferences = getApplicationContext().
getSharedPreferences("weizu", MODE_PRIVATE);
SharedPreferences.Editor edit = preferences.edit();
edit.putString("key", "value");
edit.commit();
}
});
不用指定文件后缀名,会自动添加.xml
后缀。效果一样。
2.3 读取数据
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences preferences = getApplicationContext().
getSharedPreferences("weizu", MODE_PRIVATE);
// key defaultValue
String key = preferences.getString("key", "123");
button.setText(key);
}
});
一共支持如下类型:
当需要清除所有数据的时候,可以使用editor.clear()
,然后apply
:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences preferences = getApplicationContext().
getSharedPreferences("weizu", MODE_PRIVATE);
SharedPreferences.Editor edit = preferences.edit();
edit.clear();
edit.apply();
}
});
对应的xml
文件就会变成空文件,如:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map />
3. SQLite
数据库
3.1 创建数据库和数据库表
在Android
中为管理数据库提供了SQLiteOpenHelper
,这个类是一个抽象类,我们使用需要继承它,然后重写里面的对应方法onCreate
和onUpgrade
。
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
private Context context;
private String name;
public MySQLiteOpenHelper(@Nullable Context context,
@Nullable String name,
@Nullable SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
this.context = context;
this.name = name;
}
@Override
public void onCreate(SQLiteDatabase db) {
// 创建数据库表
String sql = "create table Book (id integer primary key autoincrement, " +
"name text, pages integer)";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
在上面构造器中我们需要传入上下文对象,数据库名字,游标工厂(SQLiteDatabase.CursorFactory
)和版本。按照逻辑,我们来创建数据库:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mySQLiteOpenHelper = new MySQLiteOpenHelper(getApplicationContext(),
"BookDatabase.db", null, 1);
// 调用getWritableDatabase开始创建,得到可写数据库对象
SQLiteDatabase database = mySQLiteOpenHelper.getWritableDatabase();
}
});
使用mySQLiteOpenHelper.getWritableDatabase()
就可以创建数据库,或者mySQLiteOpenHelper.getReadableDatabase()
。这两个方法都可以创建或者打开一个现有数据库。
我们运行下程序,为了查看数据库方便,安装下Database Navigator
这个插件。
找到我们刚刚运行后的程序包下的数据库目录,:
然后将BookDatabase.db
数据库导出,然后使用顶部出现的DB Browser
来进行打开:
可以看见数据库和数据库表均自动创建成功。
数据库文件位于:data/data/package name/database/
目录下。
上面的案例中,尝试了使用onCreate(SQLiteDatabase db)
方法来创建数据库表格,注意到还有一个方法为onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
方法。
该方法主要用于数据库的升级,对数据库的管理工作起着比较重要的作用。具体应用场景为,当我们不是第一次创建数据库,也就是已经存在数据库,那么我们在我们定义的MySQLiteOpenHelper
类中onCreate
方法将不会执行。
那么,如果我们需要新增一个表格,就不能再onCreate
方法中继续使用db.execSQL(sql);
来创建。除非我们先将程序卸载,然后重新运行。这相当不方便,故而提供了onUpgrade
方法。当我们提供的newVersion
大于之前使用的版本号,这个方法就可以执行。
比如此时我们需要添加一个User
表:
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
private Context context;
private String name;
private String bookSql = "create table Book (id integer primary key autoincrement, " +
"name text, pages integer)";
private String userSql = "create table User (name text, age integer)";
public MySQLiteOpenHelper(@Nullable Context context,
@Nullable String name,
@Nullable SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
this.context = context;
this.name = name;
}
@Override
public void onCreate(SQLiteDatabase db) {
// 创建数据库表
db.execSQL(bookSql);
db.execSQL(userSql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("drop table if exists Book");
db.execSQL("drop table if exists User");
onCreate(db); // 重新执行一下onCreate方法
}
}
// Activity.java中修改版本号为3
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mySQLiteOpenHelper = new MySQLiteOpenHelper(getApplicationContext(),
"BookDatabase.db", null, 3);
SQLiteDatabase database = mySQLiteOpenHelper.getWritableDatabase();
}
});
打印下日志:
版本号增大会执行onUpgrade
方法。但是上面我们删除了数据表Book
这中方式确实不好,所以我们通常在onUpgrade
中进行版本判断,新增对应的数据库表即可。
3.2 增删改查(CRUD
)操作
发现Database Inspector
更加方便,且不用每次导出。
1. 增加
insert.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ContentValues values = new ContentValues();
values.put("name", "张三");
values.put("pages", 234);
db.insert("Book", null, values);
values.clear();
values.put("name", "李四");
values.put("pages", 80);
db.insert("Book", null, values);
}
});
结果如上图。
2. 删除
delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
db.delete("Book", "pages > ?", new String[]{"100"});
}
});
3. 修改
update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ContentValues values = new ContentValues();
values.put("name", "王武");
values.put("pages", 345);
db.update("Book", values, "pages = ?", new String[]{"234"});
}
});
4. 查询
query.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Cursor book = db.query("Book", new String[]{"id", "name", "pages"},
null, null, null, null, null);
if(book.moveToFirst()){
do{
int id = book.getInt(book.getColumnIndex("id"));
String name = book.getString(book.getColumnIndex("name"));
int pages = book.getInt(book.getColumnIndex("pages"));
Log.d("TAG", "query: id=" + id + " name="+name+" pages="+pages);
}while(book.moveToNext());
}
book.close();
}
});
3.3 注
注意到db
对象可以直接执行sql
语句,故而我们可以使用sql
语句来直接进行CRUD
操作。
drop table if exists Book
insert into Book (name, pages) values(value1, value2)
select * from Book where name='张三'
update Book set name='战三' where pages > 100
delete from Book where pages > 129
3.4 SQLite
的事务
在SQLite
中的操作默认是开启了事务的,比如前面我们的截图:
我们可以看见生成了一个对应的BookDatabase.db-journal
文件,当使用CRUD
的时候,是在这个临时文件上进行的,只有顺利完成了才会更新BookDatabase.db
数据库,否则就会被回滚。
这个临时文件BookDatabase.db-journal
在Android
中不会删除,当没有事务时候,大小为0,存在事务时候,使用该文件进行回滚。
但是,因为默认SQLite
开启了事务处理,故而当进行批量处理的时候,大大影响性能。有两种处理方式:
- 手动开启事务,批量操作,提交事务;
- 使用
db.complieStatement
返回的SQLiteStatement
进行sql
语句操作;
比如:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
db.beginTransaction();
ContentValues values = new ContentValues();
for (int i = 0; i < datas.size(); i++) {
Book book = datas.get(i);
values.put("name", book.getName());
values.put("pages", book.getPages());
db.insert("Book", null, values);
values.clear();
}
db.setTransactionSuccessful();
db.endTransaction();
}
});
3.5 SQLite
的优化
- 因为默认支持事务,故而我们可以使用手动开启事务方式做批量操作;
- 及时关闭
Cursor
,避免内存泄露; - 数据库的操作较多,可以使用子线程来执行;
- 在
ContentValues
初始化时候,如果可以预估容量,就直接填入预估容量。因为ContentValues
底层使用HashMap
来进行存储数据,故而当容量不够需要扩容操作,预估容量值可以减少不必要的扩容操作。
Thanks