Claws Garden

Android基础06——存储

设备存储

Android设备(通常是智能手机)的存储物理介质通常有手机设备内部储存(Device Storage)和便携式外部储存介质Portable Storage(如SD卡)。现在更多的手机已经取消了SD卡的设定,只有设备内部储存空间可以使用。

对于应用,设备中的储存空间按照逻辑上进行划分,分为Internal storage内部存储和External storage外部存储。两者有以下的区别:

Internal storageExternal storage
物理上一般只存在于Device Storage物理上位于Device或者Protable Storage
应用私有空间外部可访问(需授权)
用户不能直接读写用户可以直接读写
卸载时随之删除卸载后仍可以保留(默认清除)
保证可用不保证可用(可能物理移除)
有且仅有一个可以有多个

App在内部储存和外部储存两个分区中都会有一个自己专用的目录,而且这两个目录都有files和cache这样的相似结构。但是App除了能够访问属于自己的目录,还有一块公共外部目录也是可以访问的(需要权限),例如图片目录、下载目录等,称为External Public储存区。

对于App的三个可以访问的位置,有如下的区别:

Internal PrivateExternal PrivateExternal Public
本应用可访问OO(4.3之前需要授权)O(授权)
其他应用可访问XO(授权)O(授权)
用户可访问X(root可以)OO
可用性保证OXX
卸载后自动清除OOX

文件操作

File类

通过Java的File类可以方便地完成对文件的定位和操作。需要注意的是,File本身既可以是目录也可以是普通文件,毕竟在Linux中目录也是文件。File的主要构造方法有:

所有构造出来的这些文件都可以是事先不存在的,此时写入可以新建文件。

然后便可以使用一些工具类来帮助进行文件读写。

File的一些常用方法:

在读写文件的时候,可以使用I/O流FileReader和FileWriter,有File对象为参数的构造函数可以使用。

Directory的获取

内部私有目录:

外部私有目录:

外部共有目录:

在getExternalFilesDir和Environment.getExternalStoragePublicDirectory方法中需要传入type参数,表示获得标准目录。type的类型在Environment中有定义,常用的类型有:

授权

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

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

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}

就可以使用了。

本节示例工程

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

#Android