IT技术之家

首页 > 移动

移动

Android入门(12)| 数据持久化_android 持久化存储_·Jormungand

发布时间:2023-11-28 20:26:48 移动 35次 标签:android java android studio
文章目录数据持久化文件存储将数据存储进文件实例从文件中读取数据实例SharedPreferences存储数据持久化保存在内存中的数据是属于瞬时状态的,而保存在存储设备中的数据上处于持久状态的,持久化技术提供了一种可以让数据在瞬时状态和持久状态之间转换的机制。Android系统中主要提供了3种常用方式用于简单地实现数据持久化功能,即文件存储、SharedPreference存储以及数据库存储。文件存储将数据存储进文件Context类 中提供了一个 openFileOutput 方法,用于将数据...

文章目录

数据持久化文件存储将数据存储进文件实例 从文件中读取数据实例 SharedPreferences存储将数据存储进文件实例 从文件中读取数据实例 实现记住密码的功能 SQLite数据库存储创建自己的帮助类调用自己的帮助类补全 onUpgrade() 方法增删查改增:SQLiteDatabase.insert()改:SQLiteDatabase.update()删:SQLiteDatabase.delete()查:SQLiteDatabase.query() 通过 SQL语句 实现增删查改


数据持久化

保存在内存中的数据是属于瞬时状态的,而保存在存储设备中的数据上处于持久状态的,持久化技术提供了一种可以让数据在瞬时状态和持久状态之间转换的机制。

Android系统中主要提供了3种常用方式用于简单地实现数据持久化功能,即文件存储SharedPreference存储以及数据库存储


文件存储

将数据存储进文件

Context类 中提供了一个 openFileOutput 方法,用于将数据存储到指定的文件中。这个方法接收两个参数:

第一个参数是文件名:在文件创建的时候使用的就是这个名称,文件名不可以包含路径,因为所有的文件都是默认存储到 /data/data/<packagename>/files/ 目录下的。第二个参数是文件的操作模式:主要有两种模式可以选,MODE_PRIVATE 默认的操作模式,写入的内容会覆盖原文件的内容; MODE_APPEND 则表示如果该文件已经存在,就往文件里面追加内容,不存在创建新文件

该方法返回一个 FileOutputStream 对象,得到了这个对象之后就可以使用 Java流 的方式将数据写入到文件中了。

实例

在布局文件中添加输入框 EditText 控件:

在活动文件中,定义不同生命周期的不同行为:

onCreate 方法:获取 EditText 实例;onDestroy 方法:获取 EditText 中的内容,并通过自定义方法 save 保存到名为 data 的文件中。
public class SecondActivity extends AppCompatActivity {
    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);
        editText = findViewById(R.id.edit);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = editText.getText().toString();
        save(inputText);
    }

    public void save(String inputText){
        FileOutputStream out = null; // 文件字节输出流,继承OutputStream类
        BufferedWriter writer = null; // 将文本写入字符输出流
        try {
            // 获得一个 字节输出流对象,规定数据存储到名为data的文件中,文件的操作模式为MODE_PRIVATE
            out = openFileOutput("data", Context.MODE_PRIVATE);
            // 借助out构建OutputStreamWriter临时对象,作为从字符流到字节流的桥接
            // 再通过临时对象构建 字符输出流对象 以便将文本内容写入到字节流中
            writer = new BufferedWriter(new OutputStreamWriter(out));
            // 将文本内容写入到字符流中
            writer.write(inputText);
        } catch (IOException e){
            e.printStackTrace();
        } finally {
            try {
                if(writer != null){
                    writer.close();
                }
            } catch (IOException e){
                e.printStackTrace();
            }
        }
    }
}

PS:对于上述将文本存入文件的流程,一开始我理解错了,顺着代码顺寻看以为是字节流转成字符流再写入文件,把 inputText 当保存文本的文件了。。。

其实正确逻辑是:

运行结果:

在文本框内输入内容:

退出程序后,在AS中通过如下操作打开文件页面:

在下图路径中找到 data 文件,查看其内容:


从文件中读取数据

Context 类中还提供了一个 openFileInput 方法,用于从文件中读取数据。这个方法只接受一个参数:

要读取的文件名:然后系统会自动到 /data/data/<packagename>/files 目录下去加载这个文件。

该方法返回一个 FileInputStream 对象,得到了这个对象之后再通过 Java流 的方式就可以将数据读取出来了。

实例

EditText 为空则将文件中的文本读入到 EditText 中:

public class SecondActivity extends AppCompatActivity {
    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);

        editText = findViewById(R.id.edit);
        String inputText = load();
        if(!TextUtils.isEmpty(inputText)){
            editText.setText(inputText);
            editText.setSelection(inputText.length());
            Toast.makeText(this, "loading successed", Toast.LENGTH_LONG).show();
        }
    }
    
    public String load(){
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try {
            in = openFileInput("data"); // 文本字节输入流
            // InputStreamReader作为字符流到字节流的桥接
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            // 从字符流中读取数据,每次读取文件的一行
            while((line = reader.readLine()) != null){
                content.append(line);
            }
        } catch (IOException e){
            e.printStackTrace();
        } finally {
            if(reader != null){
                try {
                    // 处理完文本后关闭流
                    reader.close();
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
        return content.toString();
    }
}

PS:在判空时使用了 TextUtils.isEmpty() 而非 String.isEmpty(),这是因为:

String 类下的 isEmpty() 返回的只是 字符串的长度是否为0,如果 字符串为null 就会直接报 空指针。源码如下:
public boolean isEmpty() { return count == 0; }
TextUtils.isEmpty() 会对 null长度 进行判断,所以 不会报空指针。源码如下:
public static boolean isEmpty(CharSequence str) { 
	if (str == null || str.length() == 0) return true; 
	else return false; 
}

此时,一打开界面即可显示:


SharedPreferences存储

将数据存储进文件

大致分为两步,第一步,获取对象:

SharedPreferences 通过 键值对 的方式来存储数据的。要想存储数据,需要先获取 SharedPreferences对象,Android 中主要提供了三种方法用于得到 SharedPreferences 对象:

Context类 中的 getSharedPreferences 方法:此方法接受两个参数,
    第一个参数用于指定 SharedPreferences 文件的名称,如果文件不存在则创建一个。文件都存放在 /data/data/<packagename>/shared_prefs/ 目录下。第二个参数用于指定 操作模式,目前只有 MODE_PRIVATE 这种默认的操作模式可选,和直接传入 0 效果是相同的,表示只有当前的应用程序才可以对这个 SharedPreferences 文件进行读写
Acitvity类 中的 getPreferences 方法:只有一个参数——操作模式自动使用当前活动的类名来作为 SharedPreferences 的文件名。PreferenceManager类 中的 getDefaultSharedPreferences 方法:静态方法,接收一个 Context 参数,并自动使用当前应用程序的包名作为前缀来命名 SharedPreferences 文件。

第二步,存储数据:

    调用 SharedPreferences对象 的 edit方法 来获取一个 SharedPreferences.Editor对象。向 SharedPreferences.Editor对象 中添加数据,比如添加一个布尔型数据就使用 putBoolean 方法,添加一个字符串则使用 putString 方法。调用 apply 方法将添加的数据提交,从而完成数据存储操作。

实例

实现点击按钮保存数据的功能:

        Button button_share = findViewById(R.id.button_share);
        button_share.setOnClickListener((View v)->{
            SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
            editor.putString("name", "cmy");
            editor.putInt("weight", 120);
            editor.putBoolean("married", false);
            editor.apply();
            Toast.makeText(this, "share over", Toast.LENGTH_LONG).show();
        });

点击红框所示按钮:

即可将数据存在如下图所示的路径中:


从文件中读取数据

第一步仍是获取对象,上文已经讲过,这里不再赘述。

第二步,通过对应的 get**()方法 获取对应类型数据,如字符串使用 getString() 方法,这些 get 方法都接受两个参数:

第一个参数是:也就是 KV模型 中的 Key;第二个参数是默认值:当传入的找不到对应值时,以默认值返回。

实例

点击 get sharePreferences 按钮从 SharedPreferences文件 中读取数据:

再通过 Toast 显示出来:

        Button button_getShare = findViewById(R.id.button_getShare);
        button_getShare.setOnClickListener((View v)->{
            SharedPreferences preferences = getSharedPreferences("data", MODE_PRIVATE);
            String name = preferences.getString("name", "");
            int weight = preferences.getInt("weight", 0);
            boolean married = preferences.getBoolean("married", false);
            String res = name+" "+String.valueOf(weight)+" "+String.valueOf(married);
            Toast.makeText(this, res, Toast.LENGTH_LONG).show();
        });

实现记住密码的功能

之前在本博客里实现过登陆界面,这里为登陆界面新加入一个记住密码的功能。

修改布局文件,添加以下代码,实现右侧所示布局:

这里使用到了一个新控件 复选框:CheckBox ,用户可以通过点击来进行选中/取消,以表是否需要记住密码。

修改 LoginActivity.java 代码,结合 SharedPreferences 实现记住密码的功能:


增添的内容主要是:

三个相关对象 CheckBox、SharedPreferences、SharedPreferences.Editor 的创建和实例化;初始化布尔型对象 isRemember 作为 判断记住密码功能是否生效 的辅助变量;
    一开当然不存在 remember_password 这个键对应的值,isRemember 为默认值 false;成功登陆一次后,remember_password 这个键对应的值就是 true 了。

以及登陆成功后的操作:

调用 CheckBoxisChecked() 方法检查复选框是否被选中,被选中则返回 true;为 true 时表示用户希望记住密码,此时:
    remember_password 对应的值设为 true;把 accountpassword 对应的值都存入到 SharedPreferences 文件中并提交。
false 表示用户并不想记住密码,此时要调用 clean() 方法清楚掉 SharedPreferences 文件中的所有数据。

运行结果:

输入正确的账户和密码,并选中记住密码,点击登录:
通过强制下线跳转回登陆界面,此时发现账号密码已经自动填充了。如果此时取消选中复选框,再点击登录:
再次返回登陆界面就不会被填充了:

PS:这里只做示例,实际项目中不能将密码以明文形式存储到 SharedPreferences 文件中,因为会被轻易盗取,必须结合加密算法对密码进行加密。

SQLite数据库存储

为了管理数据库,安卓专门提供了一个 SQLiteOpenHelper 帮助类,这是个抽象类,如果要使用它,需要创建一个 自己的帮助类 去继承它。下面列举几个该类中常用的方法:

两个抽象方法onCreateonUpgrade,我们必须在 自己的帮助类 里重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。两个实例方法getReadableDatabasegetWritableDatabase。这两个方法都可以创建或者打开一个现有的数据库,数据库文件存放在 /data/data/<packagename>/databases/ 目录下,并返回一个可对数据库进行读写的对象。不同的是,当数据库不可写入的时候,getReadableDatabase 方法返回的对象会用只读的方式打开数据库,而 getWritableDatabase 会出现异常。两个构造函数:常用的一个构造方法接收4个参数:第一个是Context;第二个是数据库名;第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传入null;第四个参数表示当前数据库的版本号,可以用于升级数据库。

创建自己的帮助类

public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_STUDENT = "create table Student ("
            + "id integer primary key autoincrement,"
            + "gender text,"
            + "weight real,"
            + "age integer,"
            + "name text)";
    private Context context;

    public MyDatabaseHelper( Context context,  String name,
                             SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        this.context = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_STUDENT);
        Toast.makeText(context, "create succeeded", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}
把建表语句定义成一个字符串常量,integer 表示整型,real 表示浮点型,text 表示文本类型,blob 表示二进制类型。此外,使用了 primary key 将 id 设置为主键,并且用 autocrement 关键字表示 id 列是自增长的。在 onCreate 方法中有调用了 SQLiteDatabaseexecSQL 方法去执行这条建表语句。

调用自己的帮助类

创建一个活动 SQLiteActivity,其布局内有一个按钮,点击即可创建 Student.db 数据库:

public class SQLiteActivity extends AppCompatActivity {
    private MyDatabaseHelper myDatabaseHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sqlite_layout);

		myDatabaseHelper = new MyDatabaseHelper(this, "Student.db", null, 1);
        Button button_create = findViewById(R.id.button_create);
        button_create.setOnClickListener((View v)->{
            myDatabaseHelper.getWritableDatabase();
            Toast.makeText(this, "创建数据库成功", Toast.LENGTH_LONG).show();
        });
    }
}
第一次点击按钮时,会检测到当前程序并没有 Student.db 这个数据库,于是会创建该数据库并调用 MyDatabaseHelper 中的 onCreate 方法,得以创建 Student 表。之后点击按钮就不会再调用 MyDatabaseHelper 中的 onCreate 方法了,因为 Student.db 已经存在了。

布局文件 sqlite_layout.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button_create"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="create database"/>
</LinearLayout>

运行结果:

create succeeded 只会在数据库首次创建时出现:

创建数据库成功 会在每次点击按钮后出现:

查看数据库和表的创建情况

在环境变量中添加好 platform-tools 后:


打开 cmd,输入 adb shell 进入设备控制台:

通过 su 获取管理员权限,否则无法进入 /data/data/<packagename>/databases/ 路径:

通过 cd 命令进入数据库文件所在目录:

该目录下有两个数据库文件,一个是我们创建的 Student.db ;一个是为了让支持事务的临时日志文件 Student.db-journal

打开数据库:

查看数据库中有哪些表:

PS:android_metadata 是每个数据库自动生成的。

查看建表语句:

通过 .exit.quit 退出数据库:


补全 onUpgrade() 方法

该方法用于升级数据库,目前项目中已经有了一张 Student 表用于存放学生的各种详细数据,如果想再添加一张 Class 表用于记录学生的班级,怎么做呢?

将建表语句添加到自己的帮助类 MyDatabaseHelper 中:

该如上图所示在 onCreate 阶段执行一次 Class 的建表语句吗?

不是的,正如上一个实例中,数据库创建完成后,我们再点击按钮,只会弹出 创建数据库成功 而不会弹出 create succeeded 一样,两者的根本原因都是 onCreate 方法只会在创建数据库时执行一次,创建成功后不会再次执行。

因此无法在 Student.db 存在的情况下通过 onCreate 方法添加新表,而应通过 onUpgrade 方法添加新表。 具体做法如下:

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    	//  如果数据库中存在 Student 表或 Class表,就将他们删除。
        db.execSQL("drop table if exists Student");
        db.execSQL("drop table if exists Class");
        // 然后调用 onCreate 方法重新创建
        onCreate(db);
    }

PS:之所以 不跳过删除已有表直接调用 onCreate ,是因为 创建表时如果该表已存在会报错

接下来重新调用 SQLiteOpenHelper 的构造方法,使第四个参数——数据库版本号大于之前传入的 1 即可让 onUpgrade 执行:

运行结果:

PS:使用 AS 时也可以通过一下流程查看数据库及建表情况:


增删查改

CRUD 操作当然可以通过 SQL 语言实现,但 Android 也提供了一系列辅助方法,前面提到 getReadableDatabasegetWriteableDatabase 方法是会返回一个 SQLiteDatabase 对象,借助这个对象就可以对数据进行操作了。

增:SQLiteDatabase.insert()

该方法有三个参数:

    表名用于在未指定添加数据的情况下给某些可为空的列自动赋值为 nullContentValues 对象,它提供了一系列的 put 方法重载,用于向 ContentValues 中添加数据,只需要将表中的每个列名待添加数据传入即可。

在布局中添加了一个按钮用于增加数据:

SQLiteActivityonCreate 方法中添加以下代码:

点击两次按钮的运行结果:



两张表各添加了两行数据。


改:SQLiteDatabase.update()

该方法有四个参数:

    表名;ContentValues 对象;第三个、第四个参数用于约束更新某一行或者某几行的数据,不指定的话默认更新所有行

在布局中添加了一个按钮用于更新数据:

SQLiteActivityonCreate 方法中添加以下代码:

        Button button_update = findViewById(R.id.button_update);
        button_update.setOnClickListener((View v)->{
            SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
            ContentValues values = new ContentValues();
            // 第一条数据
            values.put("weight", 90);
            values.put("name", "zj");
            db.update("Student", values, "id = ?", new String[] {"2"});
			// 第二条数据
			values.put("class_name", "电子");
            values.put("class_num", 183);
            db.update("Class", values, "id = ?", new String[] {"1"});
            Toast.makeText(this, "更新完成", Toast.LENGTH_LONG).show();
        });

以第一条数据为例:

values 用以更新 weightname 两项属性的值;第三、第四个参数指定更新 id=2 的行。

点击按钮后的运行结果:



删:SQLiteDatabase.delete()

该方法接受三个参数:

    表名;第二、三个用于约束删除某几行的数据,不指定则删除所有行

SQLiteActivityonCreate 方法中添加以下代码:

        Button button_delete = findViewById(R.id.button_delete);
        button_delete.setOnClickListener((View v)->{
            SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
            db.delete("Student", "weight < ?", new String[] {"100"});
            Toast.makeText(this, "删除完成", Toast.LENGTH_LONG).show();
        });
删除 weight < 100 的行。

点击按钮后的运行结果:


查:SQLiteDatabase.query()

该方法比前三个复杂一些,最短的一个重载方法也需要传入七个参数:

    表名;用于指定查询哪几列;三四个参数用于约束查询某几行的数据,不指定则默认查询所有行的数据;第五个参数用于指定需要去 group by 的列,不指定则不对查询结果进行分组;第六个参数用于对 group by 之后的数据进一步过滤;第七个参数用于指定查询结果的排序方式。


调用该方法后会返回一个 Cursor 对象,查询到的所有数据都将从这个对象中取出。

        Button button_query = findViewById(R.id.button_query);
        button_query.setOnClickListener((View v)->{
            SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
            // 查询 Class 表中所有数据
            Cursor cursor = db.query("Class", null, null, null,
                    null, null, null);
            if(cursor.moveToFirst()){
                do{
                    String res = "";
                    res += cursor.getString(cursor.getColumnIndex("class_name")) + " ";
                    res += cursor.getString(cursor.getColumnIndex("class_num"));
                    Toast.makeText(this, res, Toast.LENGTH_LONG).show();
                }while(cursor.moveToNext());
            }
            cursor.close();
        });
query方法 首参数设置为 Class,其余参数设置为 null,表示查询 Class表 所有数据。调用 moveToFirst方法 将数据指针移动到第一行的位置;通过 getColumnIndex方法 获取位置索引以遍历每一行数据,并将之通过 Toast 打印到屏幕上;通过 moveToNext方法 移动数据指针遍历下一行数据,如果指针已经到达了数据指针集的尾后位置,此方法将返回 false

通过 SQL语句 实现增删查改

除了查询语句通过 db.rawQuery() 执行之外,其他三种操作都可以通过 db.execSQL() 执行。