设备存储
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文件里进行声明权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
也可以进行动态申请,语法如下:
ActivityCompat .requestPermissions( this, new
String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
这里需要填入一个REQUEST_CODE,因为如果授权得到了响应,会回调函数通知Activity,在回调时会传回申请所用的REQUEST_CODE,通过比较就可以知道申请的是哪项权限。
如果希望在授权请求被响应时做一些动作,需要重写Activity中的onRequestPermissionsResult方法,例如:
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (permissions.length == 0 || grantResults.length == 0) {
return;
}
// 这里使用requestcode帮助判断申请到的对应哪个权限
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
int state = grantResults[0];
if (state == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(DebugActivity.this, "permission granted",
Toast.LENGTH_SHORT).show();
} else if (state == PackageManager.PERMISSION_DENIED) {
Toast.makeText(DebugActivity.this, "permission denied",
Toast.LENGTH_SHORT).show();
}
}
}
另外,还需要在使用前对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来提交修改事务。使用例子如下:
public class SettingActivity extends AppCompatActivity {
private static final String KEY_COMMENT = "key_comment";
private Switch commentSwitch;
private SharedPreferences mSharedPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
// 获取对象
mSharedPreferences = getSharedPreferences("custom_settings", Context.MODE_PRIVATE);
// 读取
boolean isOpen = mSharedPreferences.getBoolean(KEY_COMMENT, false);
commentSwitch = findViewById(R.id.switch_comment);
commentSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// 写入
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(KEY_COMMENT, isChecked);
editor.commit();
}
});
commentSwitch.setChecked(isOpen);
}
}
数据库
在安卓中使用基础的SQLite数据库通常有以下步骤:
- 定义Contract静态类
- 继承SQLiteOpenHelper,自定义Helper
- (可选)实现数据库升级的逻辑
- 操作数据库:查询、插入、删除、更新
- 断开数据库连接,释放资源
Contract
首先要定义一个XXContract类来规范数据库中的表。其中要定义表中每行数据的数据类,以及创建表和删除表的SQL语句。例如:
public final class TodoContract {
// TODO 定义表结构和 SQL 语句常量
private TodoContract() {
}
// 定义表结构
public static class TodoEntry implements BaseColumns{
// 表名
public static final String TABLE_NAME="todolist";
// 列名
public static final String NOTE_DATE="date";
public static final String NOTE_STATE="state";
public static final String NOTE_CONTENT="content";
}
// 定义表创建语句
public static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + TodoEntry.TABLE_NAME + " (" +
TodoEntry.NOTE_CONTENT+" TEXT," +
TodoEntry.NOTE_STATE + " INTEGER," +
TodoEntry.NOTE_DATE + " INTEGER)";
// 定义表删除语句
public static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TodoEntry.TABLE_NAME;
}
Helper
在Helper中,定义数据库表的储存文件名,版本号,并简单实现创建数据库和升级数据库的方法。例如:
public class TodoDbHelper extends SQLiteOpenHelper {
// TODO 定义数据库名、版本;创建数据库
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "TodoList.db";
public TodoDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
}
使用数据库
之后对数据库进行操作的时候只需要用Helper协助得到SQLiteDatabase对象即可。
TodoDbHelper mDbHelper = new TodoDbHelper(this);
SQLiteDatabase database=mDbHelper.getReadableDatabase(); // 获得只读数据库
SQLiteDatabase database=mDbHelper.getWritableDatabase(); // 获得可写的数据库
查询
String[] columns=new String[]{TodoContract.TodoEntry.NOTE_CONTENT, TodoContract.TodoEntry.NOTE_DATE, TodoContract.TodoEntry.NOTE_STATE};
String sortOrder = TodoContract.TodoEntry.NOTE_DATE + " DESC";
Cursor cursor = database.query(
TodoContract.TodoEntry.TABLE_NAME, // 第一个参数,表名
, // 第二个参数,查询的列集合。null表示select *
null, // 第三个参数,筛选条件。例如id>? and name=?,参数用?代替。null表示不筛选
null, // 第四个参数,筛选条件参数,上一条的?部分,如{"10","xiaoming"}
null, // groupBy
null, // having
sortOrder // 排序方式
)
查询的结构是一个游标,可以用cursor.moveToNext()进行遍历,取出所有结果数据,例如:
List<Note> result = new ArrayList<>();
while (cursor.moveToNext()) {
String content = cursor.getString(cursor.getColumnIndex(TodoContract.TodoEntry.NOTE_CONTENT));
long dateMs = cursor.getLong(cursor.getColumnIndex(TodoContract.TodoEntry.NOTE_DATE));
int intState = cursor.getInt(cursor.getColumnIndex(TodoContract.TodoEntry.NOTE_STATE));
Note note = new Note();
note.setContent(content);
note.setDate(new Date(dateMs));
note.setState(State.from(intState));
result.add(note);
}
插入
借助ContentValues类,先将数据和键添加到ContentValues对象中,再把这个对象插入数据库即可。
SQLiteDatabase db = mDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TodoContract.TodoEntry.NOTE_CONTENT, content);
values.put(TodoContract.TodoEntry.NOTE_DATE, System.currentTimeMillis());
values.put(TodoContract.TodoEntry.NOTE_STATE, 0);
// 返回一个long类型,如果>0则成功,否则失败
long newRowId = db.insert(TodoContract.TodoEntry.TABLE_NAME, null, values);
删除
适用delete方法,传入参数为表名、筛选条件、筛选条件的参数部分。例如:
SQLiteDatabase database = mDbHelper.getWritableDatabase();
// 使用?代替参数部分
String selection = TodoContract.TodoEntry.NOTE_DATE+" LIKE ?";
String[] selectionArgs = {String.valueOf(note.getDate().getTime())};
int 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数据类
// JavaBean类
@Entity
public class User {
@PrimaryKey
public int uid;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
定义Dao接口,直接把对应的SQL语句写在注解里面:
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LMIMT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
最后添加这个数据库:
@Datebase(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
就可以使用了。