Android基础06——存储
设备存储
Android设备(通常是智能手机)的存储物理介质通常有手机设备内部储存(Device Storage)和便携式外部储存介质Portable Storage(如SD卡)。现在更多的手机已经取消了SD卡的设定,只有设备内部储存空间可以使用。
对于应用,设备中的储存空间按照逻辑上进行划分,分为Internal storage内部存储和External storage外部存储。两者有以下的区别:
Internal storage | External storage |
---|---|
物理上一般只存在于Device Storage | 物理上位于Device或者Protable Storage |
应用私有空间 | 外部可访问(需授权) |
用户不能直接读写 | 用户可以直接读写 |
卸载时随之删除 | 卸载后仍可以保留(默认清除) |
保证可用 | 不保证可用(可能物理移除) |
有且仅有一个 | 可以有多个 |
App在内部储存和外部储存两个分区中都会有一个自己专用的目录,而且这两个目录都有files和cache这样的相似结构。但是App除了能够访问属于自己的目录,还有一块公共外部目录也是可以访问的(需要权限),例如图片目录、下载目录等,称为External Public储存区。
对于App的三个可以访问的位置,有如下的区别:
Internal Private | External Private | External Public | |
---|---|---|---|
本应用可访问 | O | O(4.3之前需要授权) | O(授权) |
其他应用可访问 | X | O(授权) | O(授权) |
用户可访问 | X(root可以) | O | O |
可用性保证 | O | X | X |
卸载后自动清除 | O | O | X |
文件操作
File类
通过Java的File类可以方便地完成对文件的定位和操作。需要注意的是,File本身既可以是目录也可以是普通文件,毕竟在Linux中目录也是文件。File的主要构造方法有:
- File(String pathname) 根据路径构造文件对象
- File(String parent, String child) 根据给出的父级目录和其内部的路径构造文件对象
- File(String parent, String child) 根据给出的父级目录文件对象和内部路径构造文件对象
- File(URI uri) 根据一个URI来构造文件对象
所有构造出来的这些文件都可以是事先不存在的,此时写入可以新建文件。
然后便可以使用一些工具类来帮助进行文件读写。
File的一些常用方法:
- exists() 判断文件是否存在
- createNewFile() 创建这个文件
- mkdir() 创建这个目录 mkdirs() 如果父级目录不存在,递归创建目录
- listFiles() 列出目录下的文件
在读写文件的时候,可以使用I/O流FileReader和FileWriter,有File对象为参数的构造函数可以使用。
Directory的获取
内部私有目录:
- files目录:getFilesDir()
- cache目录:getCacheDir()
- 自定义目录:getDir(name, mode) mode通常用MODE_PRIVATE即可
外部私有目录:
- files目录:getExternalFilesDir(String type) 如果type不为空,可以返回一个对应类型的子目录(在file目录下)。类型在Environment中有定义,比如Environment.DIRECTORY_PICTURES
- cache目录:getExternalCacheDir()
外部共有目录:
- Environment.getExternalStoragePublicDirectory(String type) 获得用于储存对应类型数据的共有目录。类型在Environment中有定义,比如Environment.DIRECTORY_PICTURES
- Environment.getExternalStorageDirectory() 可以获得根目录。
在getExternalFilesDir和Environment.getExternalStoragePublicDirectory方法中需要传入type参数,表示获得标准目录。type的类型在Environment中有定义,常用的类型有:
- DIRECTORY_ALARMS 储存铃声
- DIRECTORY_DCIM 照片
- DIRECTORY_DOCUMENTS 文档
- DIRECTORY_DOWNLOADS 下载
- DIRECTORY_MOVIES 电影
授权
在使用External Storage的时候通常需要获得相应的权限。
可以在src/main/AndroidManifest.xml文件里进行声明权限:
1<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
也可以进行动态申请,语法如下:
1ActivityCompat .requestPermissions( this, new
2String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
这里需要填入一个REQUEST_CODE,因为如果授权得到了响应,会回调函数通知Activity,在回调时会传回申请所用的REQUEST_CODE,通过比较就可以知道申请的是哪项权限。
如果希望在授权请求被响应时做一些动作,需要重写Activity中的onRequestPermissionsResult方法,例如:
1@Override
2public void onRequestPermissionsResult(int requestCode,
3 @NonNull String[] permissions,
4 @NonNull int[] grantResults) {
5 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
6 if (permissions.length == 0 || grantResults.length == 0) {
7 return;
8 }
9 // 这里使用requestcode帮助判断申请到的对应哪个权限
10 if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
11 int state = grantResults[0];
12 if (state == PackageManager.PERMISSION_GRANTED) {
13 Toast.makeText(DebugActivity.this, "permission granted",
14 Toast.LENGTH_SHORT).show();
15 } else if (state == PackageManager.PERMISSION_DENIED) {
16 Toast.makeText(DebugActivity.this, "permission denied",
17 Toast.LENGTH_SHORT).show();
18 }
19 }
20}
另外,还需要在使用前对External Strorage进行可用性检查,因为外部存储是不能保证可用的。
SharedPreferences
通常用来储存一些应用的配置设置、偏好信息。本质上是用xml的格式存在文件里,SharedPreferences类进行了封装。
获得SharedPreferences:getSharedPreferences(name, Context.MODE_PRIVATE);或者getActivity().getPreferences(Context.MODE_PRIVATE); 可以返回一个SharedPreferences对象,对这个对象进行读写操作即可。
读取:SharedPreferences类提供了读出各种数据类型的方法,都是两个参数:键和默认值。例如String getString(String key, String defValue);
.
写入:通过Editor来提交修改事务。使用例子如下:
1public class SettingActivity extends AppCompatActivity {
2
3 private static final String KEY_COMMENT = "key_comment";
4
5 private Switch commentSwitch;
6 private SharedPreferences mSharedPreferences;
7
8 @Override
9 protected void onCreate(Bundle savedInstanceState) {
10 super.onCreate(savedInstanceState);
11 setContentView(R.layout.activity_setting);
12
13 // 获取对象
14 mSharedPreferences = getSharedPreferences("custom_settings", Context.MODE_PRIVATE);
15
16 // 读取
17 boolean isOpen = mSharedPreferences.getBoolean(KEY_COMMENT, false);
18
19 commentSwitch = findViewById(R.id.switch_comment);
20 commentSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
21 @Override
22 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
23
24 // 写入
25 SharedPreferences.Editor editor = mSharedPreferences.edit();
26 editor.putBoolean(KEY_COMMENT, isChecked);
27 editor.commit();
28
29
30 }
31 });
32 commentSwitch.setChecked(isOpen);
33 }
34
35}
数据库
在安卓中使用基础的SQLite数据库通常有以下步骤:
- 定义Contract静态类
- 继承SQLiteOpenHelper,自定义Helper
- (可选)实现数据库升级的逻辑
- 操作数据库:查询、插入、删除、更新
- 断开数据库连接,释放资源
Contract
首先要定义一个XXContract类来规范数据库中的表。其中要定义表中每行数据的数据类,以及创建表和删除表的SQL语句。例如:
1public final class TodoContract {
2
3 // TODO 定义表结构和 SQL 语句常量
4
5 private TodoContract() {
6 }
7
8 // 定义表结构
9 public static class TodoEntry implements BaseColumns{
10 // 表名
11 public static final String TABLE_NAME="todolist";
12 // 列名
13 public static final String NOTE_DATE="date";
14 public static final String NOTE_STATE="state";
15 public static final String NOTE_CONTENT="content";
16 }
17
18 // 定义表创建语句
19 public static final String SQL_CREATE_ENTRIES =
20 "CREATE TABLE " + TodoEntry.TABLE_NAME + " (" +
21 TodoEntry.NOTE_CONTENT+" TEXT," +
22 TodoEntry.NOTE_STATE + " INTEGER," +
23 TodoEntry.NOTE_DATE + " INTEGER)";
24 // 定义表删除语句
25 public static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TodoEntry.TABLE_NAME;
26}
Helper
在Helper中,定义数据库表的储存文件名,版本号,并简单实现创建数据库和升级数据库的方法。例如:
1public class TodoDbHelper extends SQLiteOpenHelper {
2
3 // TODO 定义数据库名、版本;创建数据库
4
5 public static final int DATABASE_VERSION = 1;
6 public static final String DATABASE_NAME = "TodoList.db";
7
8 public TodoDbHelper(Context context) {
9 super(context, DATABASE_NAME, null, DATABASE_VERSION);
10 }
11
12 @Override
13 public void onCreate(SQLiteDatabase db) {
14 db.execSQL(SQL_CREATE_ENTRIES);
15 }
16
17 @Override
18 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
19 // This database is only a cache for online data, so its upgrade policy is
20 // to simply to discard the data and start over
21 db.execSQL(SQL_DELETE_ENTRIES);
22 onCreate(db);
23 }
24}
使用数据库
之后对数据库进行操作的时候只需要用Helper协助得到SQLiteDatabase对象即可。
1TodoDbHelper mDbHelper = new TodoDbHelper(this);
2SQLiteDatabase database=mDbHelper.getReadableDatabase(); // 获得只读数据库
3SQLiteDatabase database=mDbHelper.getWritableDatabase(); // 获得可写的数据库
查询
1String[] columns=new String[]{TodoContract.TodoEntry.NOTE_CONTENT, TodoContract.TodoEntry.NOTE_DATE, TodoContract.TodoEntry.NOTE_STATE};
2String sortOrder = TodoContract.TodoEntry.NOTE_DATE + " DESC";
3
4Cursor cursor = database.query(
5 TodoContract.TodoEntry.TABLE_NAME, // 第一个参数,表名
6 , // 第二个参数,查询的列集合。null表示select *
7 null, // 第三个参数,筛选条件。例如id>? and name=?,参数用?代替。null表示不筛选
8 null, // 第四个参数,筛选条件参数,上一条的?部分,如{"10","xiaoming"}
9 null, // groupBy
10 null, // having
11 sortOrder // 排序方式
12)
查询的结构是一个游标,可以用cursor.moveToNext()进行遍历,取出所有结果数据,例如:
1List<Note> result = new ArrayList<>();
2while (cursor.moveToNext()) {
3 String content = cursor.getString(cursor.getColumnIndex(TodoContract.TodoEntry.NOTE_CONTENT));
4 long dateMs = cursor.getLong(cursor.getColumnIndex(TodoContract.TodoEntry.NOTE_DATE));
5 int intState = cursor.getInt(cursor.getColumnIndex(TodoContract.TodoEntry.NOTE_STATE));
6
7 Note note = new Note();
8 note.setContent(content);
9 note.setDate(new Date(dateMs));
10 note.setState(State.from(intState));
11
12 result.add(note);
13}
插入
借助ContentValues类,先将数据和键添加到ContentValues对象中,再把这个对象插入数据库即可。
1SQLiteDatabase db = mDbHelper.getWritableDatabase();
2
3ContentValues values = new ContentValues();
4values.put(TodoContract.TodoEntry.NOTE_CONTENT, content);
5values.put(TodoContract.TodoEntry.NOTE_DATE, System.currentTimeMillis());
6values.put(TodoContract.TodoEntry.NOTE_STATE, 0);
7
8// 返回一个long类型,如果>0则成功,否则失败
9long newRowId = db.insert(TodoContract.TodoEntry.TABLE_NAME, null, values);
删除
适用delete方法,传入参数为表名、筛选条件、筛选条件的参数部分。例如:
1SQLiteDatabase database = mDbHelper.getWritableDatabase();
2// 使用?代替参数部分
3String selection = TodoContract.TodoEntry.NOTE_DATE+" LIKE ?";
4String[] selectionArgs = {String.valueOf(note.getDate().getTime())};
5int deletedRows =database.delete(TodoContract.TodoEntry.TABLE_NAME,selection,selectionArgs);
更新
Update的时候需要依次传入表名、一个新的ContentValues对象、筛选条件和筛选条件的参数。ContentValues的创建和插入时一样的,而筛选的条件和参数和删除时一样。
将筛选的条件和参数进行分离的主要原因是避免SQL注入漏洞。
数据库调试
可以使用Stetho进行调试,非常方便。
首先添加依赖:implementation 'com.facebook.stetho:stetho:1.2.1'
然后在Activity的Oncreate()中添加Stetho.initializeWithDefaults(this);
即可。开始调试后,就可以在浏览器中输入chrome://inspect/#devices访问到调试页面。
数据库关闭
在合适的时机关闭数据库可以避免资源的浪费和一些bug产生。
通常的做法是:在用到数据库的Activity创建的过程中创建Helper对象,然后在Activity的Ondestroy方法中调用Helper.close()方法关闭数据库。
Room Library
可以看出,直接使用SQLite数据库有大量的模板代码要写,而Room Library正是对这些内容进行了进一步的封装,将数据库的使用直接简化到三步:定义JavaBean类、定义Dao接口、添加数据库,非常方便。
举个例子:
先定义一个User数据类
1// JavaBean类
2@Entity
3public class User {
4 @PrimaryKey
5 public int uid;
6
7 @ColumnInfo(name = "first_name")
8 public String firstName;
9
10 @ColumnInfo(name = "last_name")
11 public String lastName;
12}
定义Dao接口,直接把对应的SQL语句写在注解里面:
1@Dao
2public interface UserDao {
3 @Query("SELECT * FROM user")
4 List<User> getAll();
5
6 @Query("SELECT * FROM user WHERE uid IN (:userIds)")
7 List<User> loadAllByIds(int[] userIds);
8
9 @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LMIMT 1")
10 User findByName(String first, String last);
11
12 @Insert
13 void insertAll(User... users);
14
15 @Delete
16 void delete(User user);
17}
最后添加这个数据库:
1@Datebase(entities = {User.class}, version = 1)
2public abstract class AppDatabase extends RoomDatabase {
3 public abstract UserDao userDao();
4}
就可以使用了。