设备存储

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数据库通常有以下步骤:

  1. 定义Contract静态类
  2. 继承SQLiteOpenHelper,自定义Helper
  3. (可选)实现数据库升级的逻辑
  4. 操作数据库:查询、插入、删除、更新
  5. 断开数据库连接,释放资源

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

就可以使用了。

本节示例工程

https://github.com/jingjiecb/Chapter-6