我的安卓第一课:内存数据持久化存储

0 阅读8分钟

引言

在现代应用程序开发中,数据的存储与管理是不可或缺的一部分。持久化(Persistence)指的是将程序运行时的数据保存到某种存储介质中,以便在程序关闭、重启甚至设备断电后仍能保留这些数据。

持久化方式

安卓有以下很多种持久化方式。但持久化终归就是文件存储或者数据库存储。

内部文件存储

最基本的存储方式就是文件存储了,不过在Android的文件存储中我们可以不填写存储路径。只需指定文件名即可,Android 有自己的默认存储位置。 写入文件

button.setOnClickListener(v -> {
    try (
        FileOutputStream stream = openFileOutput("example.txt", MODE_PRIVATE);
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream))
    ) {
        writer.write("Hello, World!");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

文件默认存储在/data/data/packageName/files/下,属于内部目录。

同理,可以使用openFileInput读取文件。 读取文件

findViewById(R.id.btn_get).setOnClickListener(v -> {
    try (
        FileInputStream stream = openFileInput("example.txt");
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream))
    ){
        StringBuilder content = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            content.append(line).append("\n");
        }
        EditText text = findViewById(R.id.edit_text);
        text.setText(content);
    } catch (IOException e) {
        e.printStackTrace();
    }
});

文件存储是最原始的一种存储方式,操作灵活。但缺点是难以存储具备复杂结构的数据。

安卓内部目录与外部目录有关信息可看附录

SharedPreferences 存储

SharedPreferences使用键值对的方式来存储数据,同时SharedPreferences也会保存值的数据类型,存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那 么读取出来的数据仍然是字符串。该存储方式保存了数据类型信息。

button.setOnClickListener(v -> {
    // 第一个参数为filename,第二个为操作模式,当前为默认模式
    SharedPreferences.Editor edit = getSharedPreferences("app_data", Context.MODE_PRIVATE).edit();
    edit.putInt("example_key", 42);
    edit.putString("example_string", "Hello, World!");
    edit.apply();
});
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="example_string">Hello, World!</string>
    <int name="example_key" value="42" />
</map>

可以看到,数据在该存储方式下,以XML的形式进行存储。存储在/data/data/packageName/shared_prefs 读取数据

findViewById(R.id.btn_get).setOnClickListener(v -> {
    SharedPreferences data = getSharedPreferences("data", Context.MODE_PRIVATE);
    String dataString = data.getString("example_string", "Default Value");
    Toast.makeText(this, dataString, Toast.LENGTH_SHORT).show();
});

读取与写入操作类似,不过读取操作不需要.edit()。默认即可进行读取。可以看出,操作SharedPreferences就像是操作HashMap

外部文件存储

与内部文件存储相对应,都是用于存储普通文件,但是存储位置不同。

if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}

File publicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (!publicDir.exists()) {
    publicDir.mkdirs();
}

File file = new File(publicDir, "my_image.jpg");

try {
    file.createNewFile();
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
    FileOutputStream stream = new FileOutputStream(file);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
    stream.close();
} catch (IOException e) {
    e.printStackTrace();
}

SQLite 数据库

SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要你以前使用过其他的关系型数据库,就可以很快地上手SQLite。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。SQLite 数据库广泛适用于嵌入式设备,Android 作为一种特殊的嵌入式设备,内部同样使用了该数据库。

熟悉MySQL的都知道,对数据库的操作往往比操作普通文件复杂。Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建和升级。

SQLiteOpenHelper是一个抽象类,所以如果我们想要使用它,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法:onCreate()onUpgrade()。我们必须在自己的类里重写这两个方法,然后分别在这两个方法中实现创建和升级数据库的逻辑。

随后,还有两个方法,getReadableDatabase()getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。

不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()方法则将出现异常。

SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。 这个构造方法中接收 4个参数:第一个参数是Context,这个没什么好说的;第二个参数是数据库名,创建数据库时使用的就是这里指定的名称;第三个参数允许在查询数据的时候返回一个自定义的 Cursor一般传入null即可;第四个参数表示当前数据库的版本号可用于对数据库进行升级操作

构建出SQLiteOpenHelper的实例之 后,再调用它的getReadableDatabase()getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data/packageName/databases/目录下。此时,重写的onCreate()方法也会得到执行,所以通常会在这里处理一些创建表的逻辑

示例

public class MyDatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "my_db.db";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "users";
    private static final String COLUMN_ID = "_id";
    private static final String COLUMN_NAME = "name";

    public MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    // 当不存在该数据库时,会创建该数据库,并执行该方法
    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE " + TABLE_NAME + " (" +
                COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_NAME + " TEXT)";
        db.execSQL(createTable);
    }

    // 根据 SQLiteOpenHelper 构造时传入的版本参数来选择性执行
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion <= 1) { 
            db.execSQL(createCategory);
        }
        if (oldVersion <= 2) {
            db.execSQL("alter table Book add column category_id integer");
        }
    }
}

插入和查询数据

MyDatabaseHelper dbHelper = new MyDatabaseHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();

// 插入数据
ContentValues values = new ContentValues();
values.put(MyDatabaseHelper.COLUMN_NAME, "张三");
db.insert(MyDatabaseHelper.TABLE_NAME, null, values);

// 查询数据
Cursor cursor = db.query(
        MyDatabaseHelper.TABLE_NAME,
        new String[]{MyDatabaseHelper.COLUMN_ID, MyDatabaseHelper.COLUMN_NAME},
        null, null, null, null, null
);

while (cursor.moveToNext()) {
    String name = cursor.getString(cursor.getColumnIndexOrThrow(MyDatabaseHelper.COLUMN_NAME));
    Log.d("DB", "Name: " + name);
}
cursor.close();

insert(String table, String nullColumnHack, ContentValues values)

  • table – 要插入到的表
  • nullColumnHack – 可以为 Null,如果提供的 values 为空,此时插入将失败。 如果未设置为 null,则该 nullColumnHack 参数要提供可为 null 的列名,以便在values为空时显式插入 NULL。
  • values– 此映射包含行的初始列值。键应该是列名,值应该是列值

query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)

image.png

更新和删除也是类似操作,android提供的api,本质上是sql语句的api表示。当我们要写复杂SQL时,比如多表联合,android 也支持直接db.execSQL(sql);

Room 持久化库

Room 是 Android 基于 SQLite出的框架,属于Jetpack内的内容,该部分将移入Jetpack部分。

附录

内部存储路径(Internal Storage)

内部存储是 应用私有 的文件系统路径。应用卸载时,该路径下的所有数据会被自动删除。通常不需要任何权限即可访问。

方法路径示例
getFilesDir()/data/data/<package_name>/files/
getCacheDir()/data/data/<package_name>/cache/

这些路径是每个应用独有的,其他应用无法访问(除非设备 root)。 容量较小,适合存储小文件,如配置文件、缓存等。随应用卸载清除,当用户卸载应用时,这些目录下的内容会被自动删除。

外部存储路径(External Storage)

外部存储指的是 共享存储空间,通常是设备内置的“公共”存储区域,也可以是 SD 卡。存储在这里的文件可以被其他应用或用户访问。

方法路径示例
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)/storage/emulated/0/pictures/
Environment.getExternalStorageDirectory()/storage/emulated/0/
getExternalFilesDir(String type)/storage/emulated/0/Android/data/<package_name>/files/
Context.getExternalCacheDir()/storage/emulated/0/Android/data/<package_name>/cache/
MediaStore.Downloads/storage/emulated/0/Download/

共享性高,文件可被其他应用访问。需要权限(Android 9 及以下):

<uses-permission android:name="READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="WRITE_EXTERNAL_STORAGE"/>

Android 10 及以上,不再需要这些权限,但访问公共目录仍需 MANAGE_EXTERNAL_STORAGE(特殊权限)。

容量大,适合存储照片、视频、下载文件等大文件。文件可通过文件管理器查看不随应用卸载删除,除非是 getExternalFilesDir(),否则文件不会随应用卸载而删除。

内部存储 vs 外部存储

比较项内部存储外部存储
是否私有是,仅本应用可访问否,其他应用或用户可访问
是否需要权限是(Android 9 及以下),Android 10+ 使用 Scoped Storage 不再需要
是否需要处理权限请求是(尤其是 Android 6.0+)
适合存储类型小型敏感数据、缓存、配置文件大文件、媒体文件、用户可见数据
是否受用户卸载影响是(包括 getCacheDir() 和 getFilesDir())部分路径不受影响(如 Environment.getExternalStoragePublicDirectory())
是否容易被用户看到是,通常位于公共目录中
API 支持稳定性高,长期稳定在 Android 10+ ,建议使用 MediaStore

选择建议

使用场景推荐方式
存储敏感信息(如 token、密码)内部存储
缓存临时数据内部缓存目录(getCacheDir())
存储用户文档、图片、视频外部存储(建议使用 MediaStore 或 Intent 打开 SAF)
保存应用专属文件(不随卸载删除)不推荐,应使用外部存储
与其他应用共享文件外部存储 + FileProvider 或 ContentProvider