当前位置:   article > 正文

Android数据持久化技术_android studio filepersistencetest怎么查看

android studio filepersistencetest怎么查看

数据持久化

为什么需要数据持久化?由于保存在内存中的数据是瞬时数据,有可能会因为程序关闭或者其他原因导致内存被回收而丢失数据。数据持久化技术则可以让数据在瞬时状态和持久状态之间转换。数据持久化是指将内存中的数据保存到设备中,这样可以保证即使设备在关机的情况下,也不会丢失数据。

文件存储

文件存储简单来说就是通过java流的方式,把数据存储到文件中,
它不对存储的内容做任何格式化处理,所有的数据都原封不动的保存到文件中,它比较适合存储一些简单的文本,和二进制数据。

将数据存储到文件中

Content类中提供了一个openFileOutput()方法,可以将数据存储到文件中,
这个方法中要传递两个参数。 第一个参数是文件名,在创建文件的时候会使用这个文件名。
注意这里的文件名不可以包含路径,因为所有的文件都是默认存储到
data/data /(package name)/files/目录下的。 第二个参数是文件的操作模式,有两种模式可选:MODE_PRIVATE、MODE_APPEND。
MODE_PRIVATE 是指当有新内容存入该文件中的时候,会覆盖掉之前文件中的内容,而MODE_APPEND 是指当文件已经存在就往文件中追加内容,不存在就创建新的文件。

openFileOutput()方法会返回一个FileOutputStream()对象,有了这个对象就可以使用Java IO流的方式将数据写入文件了。 以下是一段代码示例,展示了如何将一段文本内容保存到文件中

   public void save() {
    String data = "Data to save";
        FileOutputStream fos = null;
        BufferedWriter bfw = null;

        try {
            fos = openFileOutput("data",MODE_PRIVATE);
            bfw = new BufferedWriter(new OutputStreamWriter(fos));
            bfw.write(data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bfw!=null){
                try {
                    bfw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

首先通过openFileOutput()方法等到了FileOutputStream 对象,在通过它构建一个OutputStreamWriter()对象,再使用它构建出BufferedWriter 对象
这样就可以使用BufferedWriter中的write方法将文本写入到文件中了。

下面再通过一个完整的例子,展示如何在Android项目中使用文件存储技术。
新建一个FilePersistenceTest 项目
修改activity_main.xml中的代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

   <EditText
       android:id="@+id/edit"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在布局文件中只是加了一个EditText控件,用于输入文本内容。当程序运行,在文本框中输入内容然后按下back键,此时活动会被销毁,文本框中的内容已经丢失了,因为它是瞬时数据,活动被销毁后就会被回收,我们要做就是在数据被回收前,将它存储到文件中。

修改MainActivity.java文件


public class MainActivity extends AppCompatActivity {
    private EditText editText;

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


    }

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

    }

    public void save(String InputText) {
        FileOutputStream fos = null;
        BufferedWriter bfw = null;
        try {
            fos = openFileOutput("data",MODE_PRIVATE);
            bfw = new BufferedWriter(new OutputStreamWriter(fos));
            bfw.write(InputText);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bfw != null){
                try {
                    bfw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

首先在Oncreate方法中获取了EditText 实例 然后在onDestroy 获取了EditText 控件中输入的文本,并重写了onDestroy()方法,在onDestroy()方法中调用了save()方法将文本写入文件,save()方法中的代码和上面的实例代码基本相同,这里就不再解释了。

运行程序后,输入一段文本,点击back键关闭程序,会执行onDestroy 方法将文本保存至文件。

在这里插入图片描述

如何查看我们保存到文件中的数据呢?我们可以通过点击Android studio右侧的Device File Explorer
进入到/data/data/com.example.filepersistencetest/files 目录进行查看

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
可以看到此时数据已经写进文件中了 。

从文件中读取数据

光是把数据写进文件还不够,我们还需要下次启动时将数据恢复到EditText中
Content类中还提供了一个openFileInput()方法,用于从文件中读取数据。
openFileInput只接受一个参数,就是要读取的文件名,然后系统会自动从
data/data /(package name)/files/ 目录下加载文件。 这个方法会返回一个FileInputStream对象 ,接着使用Java 流的方式读取文件即可。
下面是一段代码示例,展示了如何从文件中读取数据。

 public String load(){
        FileInputStream fis = null;
        BufferedReader br = null;
        StringBuilder sb = new StringBuilder();
        try {
            fis = openFileInput("data");
            br = new BufferedReader(new InputStreamReader(fis));

            String Line = null;
            while ((Line = br.readLine())!=null){
                sb.append(Line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return sb.toString();
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

首先 通过openFileInputStrame()方法,得到了一个FileInputStream对象,然后通过FileInputStream对象构建出 InputSreameRead对象,再用InputSreameRead构建出BufferedReader对象 然后使用readLine方法按行读取文本数据。 把文本数据存储到StringBuilder 对象中 最后返回读取到的内容。

了解了如何从文本中读取数据,我们接着继续完善上面的例子。 使得重新启动应用时EditText能够保留上一次的内容。

首先修改MainActivity.java中的代码


public class MainActivity extends AppCompatActivity {
    private EditText editText;

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


	/**
	新增的代码
	**/
        String load = load();
      if (!TextUtils.isEmpty(load)){
            editText.setText(load);
            editText.setSelection(load.length());
            Toast.makeText(this, "数据已恢复", Toast.LENGTH_SHORT).show();
        }

    }

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

    }

    public void save(String InputText) {

        FileOutputStream fos = null;
        BufferedWriter bfw = null;
        try {
            fos = openFileOutput("data",MODE_PRIVATE);
            bfw = new BufferedWriter(new OutputStreamWriter(fos));
            bfw.write(InputText);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bfw!=null){
                try {
                    bfw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

    }

	/**
	新增的代码
	**/
    public String load(){
        FileInputStream fis = null;
        BufferedReader br = null;
        StringBuilder sb = new StringBuilder();
        try {
            fis = openFileInput("data");
            br = new BufferedReader(new InputStreamReader(fis));

            String Line = null;
            while ((Line = br.readLine())!=null){
                sb.append(Line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (br!=null){
                    br.close();
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return sb.toString();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

可以看到在MainActivity.java中增加了一个load方法 ,用于读取文件中的数据,这段代码在上面已经解释过了。 在Oncreate方法中调用load方法,来读取文本内容,如果读取的内容不为null 就调用EditText中的setText方法将内容设置EditText中。 并调用setSelection方法将光标移动到文本末尾,便于继续输入。
上述代码在对字符串进行判空的时候用了,TextUtils.isEmpty()方法,这是一个非常好用的方法,他可以一次性进行两种空值的判断,当传入的字符串等于null 或等于字符串空串的时候都会返回true,因此不需要单独判断这两种空值再使用逻辑运算符连接起来了。

以上就是关于文件存储方面的知识,其实就是使用Context类中的openFileOutputSream,和openFileInputSream方法,然后利用java流对数据进行读写。
接下来学习的是另一种数据持久化技术,SharedPreferences,它要比使用文件方便很多。

SharedPreferences存储

想要使用SharedPreferences存储数据,就需要先获取 SharedPreferences对象,Android中提供了三种获取SharedPreferences对象的方法。

  1. Context类中的getSharedPreferences方法 此方法接收两个参数,第一个参数用于指定SharedPreferences的文件名,SharedPreferences文件都放在data/data(package name)/files/目录下。
    第二个参数用于指定操作模式,目前只有一种模式可选MODE_PRIVATE ,它是默认的操作模式和直接传入0效果是相同的,它表示只有当前应用程序才可以对这个SharedPreferences文件进行读写。

  2. Activity类中的getSharedPreferences方法

    这个方法和Context类中的getSharedPreferences和相似,不过它只接收一个操作模式参数,因为这个方法会自动将当前活动的类名做为文件名。

  3. PreferenceManager类中的getDefaultSharedPreferences方法
    它是一个静态方法,它只接收一个Context参数,并自动将当前应用程序的包名作为文件名。

得到了SharedPreferences对象后,就可以想SharedPreferences文件中存储数据了。 主要可以分三步实现。

  1. 调用SharedPreferences对象中的方法edit()获取一个 SharedPreferences.Editor 对象
  2. 向 SharedPreferences.Editor中添加数据,比如添加一个String类型,就调用putString,添加布尔数据类型就调用putBoolean
  3. 接着调用 apply()方法将数据提交,从而完成数据存储的操作。

下面将通过一个例子,来使用SharedPreferences存储数据。

使用SharedPreferences存储数据

首先创建一个SharedPreferencesTest 项目

修改MainActivity.xml中的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textAllCaps="false"
       android:text="Save data"
       />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这里只是放置了一个按钮,用于将一些数据存储到SharedPreferences文件中。 然后修改MainActivity.java中的代码

public class MainActivity extends AppCompatActivity {

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

        ((Button)findViewById(R.id.button)).setOnClickListener((V)->{
            SharedPreferences.Editor edit = getSharedPreferences("data", MODE_PRIVATE).edit();
            edit.putString("name","David");
            edit.putInt("age",20);
            edit.apply();
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里首先给按钮注册了一个点击事件,然后在getSharedPreferences()方法中给文件指名为data,并调用edit()方法获取了SharedPreferences.Editor对象,接着向这个对象添加了两条数据,最后调用apply()方法提交数据。

现在运行程序,点击按钮后,数据就已经保存成功了。

可以通过 点击右侧的Device File Explorer,进入到/data/data/com.example.sharedpreferencestest/shared_prefs 目录下进行查看。

在这里插入图片描述
可以看到,我们在点击事件中添加的数据已经存储进来了,并且发现SharedPreferences是通过XML文件来对数据进行管理的。
下面是使用SharedPreferences读取数据的操作。

从SharedPreferences读取文件

修改MainActivity.xml中的代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textAllCaps="false"
       android:text="Save data"
       />

   <Button
       android:id="@+id/button2"
       android:text="restore_data"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textAllCaps="false"
       />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这里增加了一个按钮,希望通过这个按钮可以读取文件中的数据。
修改MainActivity.java中的代码

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
		.....
	
        ((Button)findViewById(R.id.button2)).setOnClickListener((V)->{
            SharedPreferences spf = getSharedPreferences("data", MODE_PRIVATE);
            String name = spf.getString("name", "");
            int age = spf.getInt("age", 0);

            Log.d("data","name is :"+name);
            Log.d("data","age is :"+age);


        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这里给读取数据按钮添加了点击事件,然后通过getSharedPreferences()方法获取到了SharedPreferences 对象,分别调用他的getString()和getInt()方法获取前面存储的name和age,最后通过Log将这些值打印出来。

重新运行程序 点击按钮,可以在Logcat中看到这些值已经被打印出来了。
在这里插入图片描述
下面将运用上面的知识,做一个记住密码功能。

记住密码功能实现

首先新建一个remember_password 项目
然后在项目中新建一个登录界面活动 新建,LoginActivity,然后编辑布局界面activity_login_activity如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">
    
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account:"
            />
        <EditText
            android:id="@+id/account"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password:"
            />
        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            android:inputType="textPassword"/>

    </LinearLayout>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:text="Login"
        />
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >

        <CheckBox
            android:id="@id/checkbox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="记住密码"
            />

    </LinearLayout>
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

这里使用LinearLayout布局写出了一个登录界面,最外层是一个纵向的LinearLayout,里面包含了四行子元素,第一行子元素是用于输入账号信息,第二行子元素用于输入密码信息,第三行子元素是一个登录按钮,第四行子元素是一个复选框按钮,使用这个控件来表示用户是否需要记住密码。

然后修改LoginActivty.java中的代码如下:


public class LoginActivity extends AppCompatActivity {
    private SharedPreferences spf = null;
    private SharedPreferences.Editor editor = null;
    private CheckBox rememberPass;
    private EditText account;
    private EditText password;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        account = findViewById(R.id.account);
        password = findViewById(R.id.password);
        rememberPass = findViewById(R.id.checkbox);
        spf = PreferenceManager.getDefaultSharedPreferences(this);

        boolean remember_password = spf.getBoolean("remember_password", false);

        if (remember_password) {
            String accountText = spf.getString("account", "");
            String passwordText = spf.getString("password", "");
            account.setText(accountText);
            password.setText(passwordText);
            rememberPass.setChecked(true);
        }

        ((Button)findViewById(R.id.login)).setOnClickListener((V)->{
            String accountText = account.getText().toString();
            String passwordText = password.getText().toString();
            if ("admin".equals(accountText) && "123456".equals(passwordText)){
                editor = spf.edit();
                if (rememberPass.isChecked()){
                   editor.putString("account",accountText);
                   editor.putString("password",passwordText);
                   editor.putBoolean("remember_password",true);
                }else {
                    editor.clear();
                }
                editor.apply();
                Intent intent = new Intent(LoginActivity.this,MainActivity.class);
                startActivity(intent);
            }else {
                Toast.makeText(this, "用户名或密码无效!", Toast.LENGTH_SHORT).show();
            }
        });


    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

首先在Oncreate()方法中获取了SharedPreferences对象,调用它的getBoolean方法获取remember_password对应的值,由于一开始什么都没有存储,自然也是获取不到的,所以会默认值false ,这里就什么都不会发生。

接着输入用户名和密码,点击登录按钮之后,会判断用户名密码是否正确,如果正确,则会调用SharedPreferences对象的edit()方法得到 一个SharedPreferences.Editor对象,接着会调用CheckBox的isChecked方法来检查用户是否勾选了记住密码复选框,如果勾选了则表示用户想要记住密码,那么就将用户名和密码存储到 SharedPreferences文件中,并将remember_password设置为true。

当用户选中了记住密码复选框,并成功登陆了,那么用户名密码就会被存储起来,而且此时remember_password对应的值已经为true,这个时候重新启动登陆界面,那么就会从SharedPreferences读取用户名和密码,并且恢复到文本输入框中,并且会把记住密码复选框设置为选择状态。

别忘了还有一个MainActivity活动,当登录成功后会跳转到MainActivity
修改MainActivity.xml中的代码 如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
<Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"

    />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

MainActivity.xml中的代码只有一个Button控件,我们希望当点击这个按钮之后再跳转到LoginActivty。
修改MainActivty.java中的代码 如下:

public class MainActivity extends AppCompatActivity {

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

        ((Button)findViewById(R.id.button)).setOnClickListener((V)->{
            Intent intent = new Intent(MainActivity.this,LoginActivity.class);
            startActivity(intent);

        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里仅仅是给Button控件注册了一个点击事件,当点击这个按钮之后又回会到LoginActivty活动中,这个时候LoginActivty会被重新创建一次,所以Oncreate方法会得到执行,假如用户点击了记住密码复选框,那么就会读取文件中保存的账户和密码,并且恢复到复选框中。

别忘了到AndroidManifest.xml,将主活动修改为LoginActivity

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.remember_password">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Remember_password">
        <activity android:name=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

        </activity>
        <activity android:name=".MainActivity">

        </activity>
    </application>

</manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

现在启动应用程序,可以看到出现了一个登录界面。账号输入admin,密码输入123456勾选记住密码复选框后,点击登录。

在这里插入图片描述
点击登录之后会跳转到MainActivity界面
在这里插入图片描述
接着点击MainActivity中的按钮,又会跳转到LoginActivty中 ,可以看到此时账号和密码已经自动填充到界面上了。
在这里插入图片描述

可以勾掉记住密码复选框,再进行登录,再次回到LoginActivity,会发现账号和密码处已经没有内容了。

关于SharedPreferences技术就讨论到这里,接下来将继续学习Android中的数据库技术。

SQLite 数据库技术

SQLite是Android系统内置的数据库,是一种轻量级的数据库,自身大小只有几KB,因此非常适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,所以只要使用过其他关系型数据库,就会很快的上手SQLite数据库。SQLite数据库要比其他的数据库简单的多,它甚至不需要设置用户名和密码就可以使用,Android正是把这个强大的数据库嵌入到了系统当中,使得本地持久化功能有了一次质的飞跃。

前面所学习的文件存储和SharedPreferences存储只适合存储一些简单的数据和键值对,当需要存储一些复杂的数据的时候,以上两种方式就会很难对付了。比如我们的手机短信程序中有很多个会话,每个会话又包含了很多信息,并且大部分会话还都各自对应了电话簿中的联系人。很难想象如何使用SharedPreferences或文件存储来保存数据,
这时候就需要使用SQLite来解决这个问题了。

创建数据库

Android为了让我们更加方便的管理数据库,给我们提供了一个SQLiteOpenHelper帮助类,使用它可以轻松的对数据库进行创建和升级。SQLiteOpenHelper是一个抽象类,这意味着,要想使用这个类就必须找一个类来继承它。
SQLiteOpenHelper类中有两个抽象方法onCreate()和()方法,我们必须在自己的类中实现这两个方法,来对数据库创建和升级。
SQLiteOpenHelper中还有两个实例方法,getWritableDatabase()和getReadableDatabase()。这两个方法都可以创建或打开一个现有的数据库。他们唯一不同的是当数据库不可写入的时候如(磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法将出现异常。

SQLiteOpenHelper 给我们提供了一些构造方法,供我们重写,一般会使用四个参数的构造方法,第一个参数是Context,第二个参数是数据库名,创建数据库使用的就是这个名,第三个参数允许我们在查询的时候返回一个自定义的Cursor,一般传入null,第四个参数是数据库的版本号,用于对数据库进行升级操作。
需要重写的构造方法

构建出SQLiteOpenHelper 的实例之后,调用他的getWritableDatabase()或getReadableDatabase()方法就能够创建数据库,数据库文件会存放在/data/data/(package name)/databases/目录下,重写的Oncreate方法也会得到执行。

接下来通过一个例子来使用一下SQLite数据库,首先创建一个DatabaseTest项目。
并在数据库中创建一张book表 建表语句如下:

create table book (
id integer primary key autoincrement ,
author text, price real, pages integer,
name text)

如果有了解过SQL方面的知识,你会很容易的理解上面的语句,SQLite不像其他数据库,有着众多繁杂的数据类型,他的数据类型很简单,integer表示整形,real表示浮点型,text表示文本,blob表示二进制类型。在上述建表语句中我们还使用了primary key将id设置为主键,使用autoincrement 设置id列为自动增长。
然后在代码中执行这段SQL语句才能完成创建表的操作。

首先新建MyDatabaseHelper类继承SQLiteOpenHelper 代码如下所示:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_BOOK = "create table book (" +
            " id integer primary key autoincrement , " +
            "author text, price real, pages integer," +
            " name text)";

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

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
    }

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

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

首先把建表语句定义成了字符串常量,然后在Oncreate方法中调用了SQLiteDatabase中的execSQL去执行了这条语句,并弹出了一个Toast提示创建成功。

修改MainActivitity.xml中的代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create DataBase"
        android:id="@+id/button"
        android:textAllCaps="false"
        />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里只是添加了一个Button控件

接着修改MainActivity.java中的代码 如下:

public class MainActivity extends AppCompatActivity {
    MyDatabaseHelper dHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dHelper = new MyDatabaseHelper(this, "BOOK.db", null, 1);

        ((Button)findViewById(R.id.button)).setOnClickListener((V)->{
            dHelper.getWritableDatabase();
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

首先在onCreate()方法中创建了一个MyDatabaseHelper对象,并通过构造函数将数据库命名为BOOK.db,版本号指定为1,然后在Create DataBase按钮的单击事件里调用了getWritableDatabase()方法,当第一次点击时,就会检测到当前程序并没有BOOK.db这个数据库,于是就会创建数据库并调用MyDatabaseHelper中的onCreate方法,同时弹出Toast,提示创建成功。当第二次点击,由于程序中已经存在了BOOK.db数据库了所以不会再创建一次

运行程序后,点击Create DataBase按钮

在这里插入图片描述
此时数据库已经创建完毕,数据库中应该有了一张BOOK表,那么如何证实它们已经创建成功了呢?
其实有很多种方式可以证实,这里说一下我个人比较喜欢的一种方式。

使用Glance开源库查看数据库中的内容:
将Glance进入到项目中以后,就可以以图形界面的方式查看数据库里的内容,非常的好用。

另外,在 Android Studio 4.1 Canary 5 以及更高版本 上,内置了 Database Inspector 工具也可以查看数据库, 可以点击链接了解,这里就不再说了 动态数据库工具——Database Inspector

我这里只说如何使用Glance开源库来查看数据库
也可以点击Glance介绍及使用方式学习如何使用

其实只需要将以下语句引入到你的项目中就可以使用了。

dependencies {
    debugImplementation 'com.glance.guolindev:glance:1.0.0-alpha01'
}
  • 1
  • 2
  • 3

(将dependencies 闭包中的语句,粘贴到你项目中的 app 下的 build.gradle文件中的dependencies闭包中)
在这里插入图片描述
然后重启你的程序,返回到模拟器的桌面你会发现这个桌面上多了这个图标
在这里插入图片描述

点击进去以后,发现刚才创建好的数据库已经出现了,点击它。
在这里插入图片描述
会发现有三张表,这里点击book表就可以了,其他两个表是自带的不用管它。

在这里插入图片描述

这样就打开了刚才创建的book表,由于还没有向表中添加任何数据所以是空的。

在这里插入图片描述

这样就已经成功使用Glance开源库,查看了数据库中的内容。

当然还有很多其他优秀的开源库也可以用来查看数据库,可以自行了解。

升级数据库

在MyDatabaseHelper类中还有一个空方法还没有使用,onUpgrade()方法是用来对数据库进行升级的,比如现在Database项目中已经有一张book表用来存放图书信息,那么我还想再加一张Category表用于记录图书的分类,就可以这么做。

比如Category表中有id(主键)、分类名和分类代码这几列,那么建表语句就可以这么写。
create table category( id integer primary key autocrement,
category_name text,
category_code integer)

接下来修改MyDatabaseHelper.java中的代码


public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_BOOK = "create table book (" +
            " id integer primary key autoincrement , " +
            "author text, price real, pages integer," +
            " name text)";
            
            /*
				新增的代码
			*/
    public static final String CREATE_CATEGORY = "create table category( id integer primary key  autoincrement," +
            "category_name text," +
            "category_code integer)";
            
    private Context mContext;
    
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
     
       		/*
				新增的代码
			*/
        db.execSQL(CREATE_CATEGORY);
        
        Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("drop table if exists book");
            db.execSQL("drop table if exists Category");
            onCreate(db);
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

在onUpgrade方法中执行了两条drop语句,如果发现数据库中已经存在book表和Cate
gory表,就先将这两张表删除,最后再调用MyDatabaseHelper中的onCreate()方法创建表,这里要先将已经存在的表删掉,因为如果创建的时候发现这张表已经存在就会直接报错。

那么如何让onUpgrade()方法执行呢,SQLiteOpenHelper中的构造方法的第四个参数代表当前数据库的版本号,之前传入的是1,现在只需要传入一个比1大的数,就可以让onUpgrade()方法得到执行了!

修改MainActivity.java中的代码如下:


public class MainActivity extends AppCompatActivity {
    MyDatabaseHelper dHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //将第四个参数改为2
        dHelper = new MyDatabaseHelper(this, "BOOK.db", null, 2);

        ((Button)findViewById(R.id.button)).setOnClickListener((V)->{
            dHelper.getWritableDatabase();
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

现在重新运行程序,按下Create DataBase按钮,这时会再次提示创建成功。这时候打开Glance查看一下。
在这里插入图片描述
发现Category表已经创建成功了。

添加数据

掌握了如何升级和管理数据库,那么如何对表中的数据进行操作呢。其实我们对数据进行的操作无非有四种即 CRUD。其中C代表添加(Create) ,R代表查询(Retrieve),U代表更新(Update),D代表删除(Delete)。每一种操作又各自对应了一种SQL命令,如果你比较熟悉SQL语言的话,就知道添加数据时使用insert,查询数据时使用selelct,更新数据时使用update,删除数据时使用delete。但是开发者的水平总是参差不齐的,未必每一个人都能非常熟练的使用SQL语言,因此Android也提供了一套辅助性的方法,使得在Android中不去编写SQL语句也能轻松完成所有的CRUD操作。

调用SQLiteOpenHelper中的getWritableDatabase()和getReadableDatabase()可以返回一个SQLiteDatabase对象,借助这个对象我们就可以对数据进行CRUD操作了。

在SQLiteDatabase中提供了一个insert()方法,使用这个方法就可以向表中添加数据了,这个方法有三个参数,第一参数是表名,想要在哪张表添加数据,就写入该表的名字,第二个参数用于在未指定添加数据的情况下给某些可为空的列自动复制为null,一般我们不用这个功能传入null即可,第三个参数需要传入一个ContentValues对象,它提供了一系列的put方法重载,用于向表中添加数据,只需要将表中每个列名以及相应待添加数据传入即可。

下面通过一个例子来体会一下。

修改MainActivity.xml中的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

 	...

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="Add data"
        android:textAllCaps="false"
        />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在布局文件中又添加了一个按钮。待会点击这个按钮就会执行添加数据的逻辑

接着修改MainActivity.java中的代码


public class MainActivity extends AppCompatActivity {
    MyDatabaseHelper dHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dHelper = new MyDatabaseHelper(this, "BOOK.db", null, 2);

  		...
 ((Button)findViewById(R.id.button2)).setOnClickListener((V)->{
            SQLiteDatabase db = dHelper.getWritableDatabase();
            ContentValues cvs = new ContentValues();
            cvs.put("name","Thinking in Java");
            cvs.put("price",80.5);
            cvs.put("pages",1000);
            cvs.put("author"," Bruce Eckel");
            db.insert("book",null,cvs);
            cvs.clear();

            //开始封装第二条数据
            cvs.put("name","Core Java Volume I");
            cvs.put("price",70);
            cvs.put("pages",1000);
            cvs.put("author","Cay S. Horstmann");
            db.insert("book",null,cvs);
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

首先给刚才添加的按钮注册一个点击事件,在点击事件了获取到了SQLiteDatabase对象,然后创建了一个ContentValues 对象,然后把要添加的数据存入对象。这里添加了两行数据,上述代码中ContentValues 分别组装了不同的内容,调用了两次insert(), 又由于id列是自动增长的所以我们不需要为它添加数据,只需要为其他四列添加即可。

现在重新运行程序点击Add data按钮,此时数据已经添加进去了。
在这里插入图片描述

更新数据

SQLiteDatabase中有一个update()方法,用于对数据进行更新,第一个参数指定要更新的报名,第二个参数是ContentValues对象,把要更新的数据在这里组装进去,第三第四个参数用于约束更新某一行或某几行的数据,不指定就更新全部。

下面来实践一下,修改Mainactivity.xml中的代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

 	...

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:text="Update data"
        />

</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在布局文件中又新增了一个按钮

下面修改MainActivity.java中的代码


public class MainActivity extends AppCompatActivity {
    MyDatabaseHelper dHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dHelper = new MyDatabaseHelper(this, "BOOK.db", null, 2);
        
   		...
   		
        ((Button)findViewById(R.id.button3)).setOnClickListener((V)->{
            SQLiteDatabase db = dHelper.getWritableDatabase();
            ContentValues cvs = new ContentValues();
            cvs.put("price",100);
            db.update("book",cvs,"name = ?",new String[]{"Core Java Volume I"});
        });




  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

还是为按钮注册了一个点击事件,在点击事件中获取了SQLiteDatabase ,然后在ContentValues 对象中添加了一组数据,表示我们要将表中price列中的值更新为100,然后调用了update方法,第三第四个参代表了具体要更新哪几行,表示要更新name等于?的行,而?是一个占位符,而第四个参数表示要给第三参数中的每个占位符指定相应的内容。上述代码中要表达的意图是,要把名字是Core Java Volume I这本书的价格更新为100。

现在运行一下程序,点击Update data按钮,然后再查看一下数据库
在这里插入图片描述
可以看到数据已经更新了

删除数据

在SQLiteDatabase中有一个delete()方法可对数据库中数据进行删除。这个方法有三个参悟,第一个参数仍然是表名,第二三个参数指定删除某一行,或某几行的数据,如果不指定就默认删除所有行。

修改MainActivity.xml中的代码
仍然是在布局文件中增加一个按钮

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
 	...

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button4"
        android:text="delete data"
        />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在 MainActivity.java文件中为其增加点击事件:


public class MainActivity extends AppCompatActivity {
    MyDatabaseHelper dHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dHelper = new MyDatabaseHelper(this, "BOOK.db", null, 2);
   		
			...
	

        ((Button)findViewById(R.id.button4)).setOnClickListener((V)->{
            SQLiteDatabase db = dHelper.getWritableDatabase();
           db.delete("book","price < ?" ,new String[]{"100"});
        });

    }
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

为按钮注册点击事件,在点击事件中,获取到了SQLiteDatabase 对象,然后调用delete,去指名要删除book表中的数据,并通过第二,三个参数指定要删除价格小于100的书,查看一下数据库发现目前只有Thinking in Java 这本书的价格小于100,也就是说当点击按钮时这条记录会被删除。

运行程序,点击delete data按钮 再查询一下数据库,发现数据成功被删除了。

在这里插入图片描述

查询数据

SQLiteDatabase中还提供了一个Query()方法,用于对数据库进行查询,这个方法的参数比较复杂,最短的一个方法重载也需要7个参数。第一个参数,是表名,第二个参数指定去 查询哪些列,如果不指定默认查询所有列。第三四个参数指定查询某一行,或某几行的数据,不指定则查询所有数据。第五个参数用于指定需要去group by的列,不指定则表示不进行group by操作。第六个参数用于对group by之后的列进行过滤,不指定则不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认排序方式,

下面继续修改MainActivity.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

     ...

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button5"
        android:text="Query data"
        />

</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

修改MainActivity.java中的代码


public class MainActivity extends AppCompatActivity {
    MyDatabaseHelper dHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dHelper = new MyDatabaseHelper(this, "BOOK.db", null, 2);
  		
  		...

        ((Button)findViewById(R.id.button5)).setOnClickListener((V)->{
            SQLiteDatabase db = dHelper.getWritableDatabase();
            Cursor cursor = db.query("book", null, null, null, null, null, null);
            if (cursor != null){
                while (cursor.moveToNext()){
                    String name = cursor.getString(cursor.getColumnIndex("name"));
                    String author = cursor.getString(cursor.getColumnIndex("author"));
                    String price = cursor.getString(cursor.getColumnIndex("price"));
                    String pages = cursor.getString(cursor.getColumnIndex("pages"));

                    Log.d("MainActivity","name: "+name);
                    Log.d("MainActivity","author: "+author);
                    Log.d("MainActivity","price:"+price);
                    Log.d("MainActivity","pages: "+pages);
                }
            }

            cursor.close();
        });
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

首先在按钮的点击事件里面调用了SQLite中的Query()方法查询数据。这里的Query方法,只用了第一个参数指明要去查询book表,后面的参数全部为null就表示希望查询这张表的全部数据。
查询完之后会返回一个Cursor 对象。
接着对查询出来的Cursor,进行循环,查询出来的结果集会有一个指针指向表头的位置,moveToNext表示让指针移动到下一行,这样才可以读取到每一行的数据。getColumnIndex方法可以获取到某一列在表中的索引,然后将这个索引传入到相应的取值方法中,就可以从数据库中读取到数据了。
这里使用Log的方式将取出来的数据打印出来,最后调用close()方法来关闭cursor

再次重新启动程序,点击Query data按钮,在Logcat中可以看到数据已经被打印出来了。
在这里插入图片描述
‘’

使用SQL操作数据库

虽然Android已经给我们提供很多非常方便的API用于操作数据库,不过总有一些人不习惯用这些辅助性的方法,而是更青睐与直接使用SQL来操作数据库。这种人一般都属于SQL大牛,如果你也是其中之一的话,那么恭喜,Android充分考虑了你们的编程习惯,同样提供了一系列的方法,使得可以直接通过SQL来操作数据库。

添加数据的方法如下:

db.execSQL("insert into book(author,price,pages,name) values(?,?,?,?) ",new String[]{"GuoLin","70.5","570","DiYiHangDaiMa"});

db.execSQL("insert into book(author,price,pages,name) values(?,?,?,?) ",new String[]{"DaLao","150.5","1500","Thinking in Java"});
  • 1
  • 2
  • 3

更新数据的方法:

db.execSQL("update Book set price = ?  where name  = ?" , new String []{"10.99", "The Da Vinci Code"} );
  • 1

删除数据的方法:

db.execSQL("delete from Book   where pages > ?"  , new String [] {"500"}) ;
  • 1

查询数据的方法:

db.rawQuery("select * from Book" , null);
  • 1

使用LitePal操作数据库

LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成建表和增删改查的操作。LitePal的项目主页上也有详细的使用文档。 地址是 https://github.com/guolindev/LitePal。

配置LitePal

如何使用LitePal开源库呢?只需要在app/build.gradle文件中声明该开源库的引用就可以了。

(新建一个LitePalTest项目)

因此第一步要做的就是在dependencies闭包中添加如下内容:

dependencies {

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    debugImplementation 'com.glance.guolindev:glance:1.0.0-alpha01'


    <!--要添加的内容-->
    implementation 'org.litepal.guolindev:core:3.2.2'
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

添加的这一行声明中,前面部分是固定的,最后的3.3.0是版本号的意思,最新版可以到LitePal的项目主页上去查看。

这样就把LitePal成功的引入项目中了,接下来需要配置litepal.xml文件。
右击app/src/main 目录→New→Directory,创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件接着编辑litepal.xml中的内容,如下所示:

<?xml version="1.0" encoding="UTF-8" ?>

<litepal>
    <dbname value="Book"></dbname>

    <version value="1"></version>

    <list>
        <mapping class="com.example.litepaltest.Book"></mapping>
    </list>
</litepal>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

其中,< dbname>标签用于指定数据库名,< version>标签用于指定数据库版本号,< list>标签用于指定所有的映射模型,稍后会用到。

最后还需要配置一下LitePalApplication,修改AndroidManifest.xml中的代码,如下:

    <application
        android:name="org.litepal.LitePalApplication"
     
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LitePalTest">
   		...
    </application>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这里将项目中的application配置为org.litepal.LitePalApplication,这样才能让Litepal的所有功能都能正常工作。

创建和升级数据库

我们之前创建数据库是通过自定义的一个类继承自SQLiteOpenHelper,然后在Oncreate()方法中编写建表语句来实现的,而使用LitePal就不用再这么麻烦了。

先将上一个DatabaseTest项目中的activity_main中的布局复制到LitePal项目中来。
LitePal采取的是对象关系映射(ORM)的模式,什么是对象关系映射呢?简单点来说,我们使用的编程语言是面向对象语言,而使用的数据库是关系型数据库,那么将面向对象语言和面向关系的数据库之间建立的一种映射,这就是对象关系映射了。

对象关系映射它赋予了我们一个强大的功能,就是可以用面向对象的思维来操作数据库,而不用和SQL打交道了。

在LitePalTest项目中定义一个Book类:


public class Book  {
     private int id;
     
    private String author;
    
    private double price;
    
    private int pages;
    
    private String name;

    public Book() {
    }

    public Book(int id, String author, double price, int pages, String name) {
        this.id = id;
        this.author = author;
        this.price = price;
        this.pages = pages;
        this.name = name;
  
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

 

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

这是一个典型的JavaBean,在book类中定义了,id,author,price,pages,name几个字段,并生成了相应的getter和setter方法。book类就会对应数据库中的book表,而类中每一个字段分别对应了表中的每一个列,这就是对象关系最直观的体验。

接下来还需要将book类添加到映射模型列表中,修改litepal.xml中的代码,如下所示:

<litepal>
    <dbname> value="Book"</dbname>

    <version value="!"></version>

    <list>
        <mapping class="com.example.litepaltest.Book"></mapping>
    </list>
</litepal>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里使用< mapping>标签来声明我们要配置的映射模型类,注意一定要写完整的类名。不管有多少模型类需要映射,都使用同样的方式配置在< list>标签下即可。

这样就已经把工作全部做完了,现在只要进行任意一次数据库的操作,book数据库应该就会被自动创建出来。修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ((Button)findViewById(R.id.button)).setOnClickListener((V)->{
            LitePal.getDatabase();
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

其中,调用getDatabase()方法就是最简单的一次数据库操作,只要点击按钮,数据库就会被自动创建。
运行一下程序,点击Create DataBase按钮,接着通过Glance来查看一下数据库。

在这里插入图片描述
在这里插入图片描述
此时数据库文件,和表已经创建完成了!

在使用SQLiteOpenHelper来升级数据库时,需要把之前的表drop掉,然后再重新创建才行。这其实是一个非常严重的问题,因为这样会造成数据的丢失,每当升级一次数据库,之前表中的数据全没了。

当然如果你是非常有经验的程序员,也可以通过逻辑控制来避免这种情况,但成本很高。而有了LitePal,这些都不是问题了,使用LitePal来升级数据库非常简单,你完全不用思考任何逻辑,只需要改你想添加的内容,然后版本号加1就行了。

比如在book表中添加一个press(出版社)列,直接修改Book类中的代码,添加一个press字段即可。
如下所示:


public class Book  {
 ...

    private String press;
    
    public String getPress() {
        return press;
    }

    public void setPress(String press) {
        this.press = press;
    }
 ...
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

于此同时,再添加一张category表,那么只需要新建一个Category类就可以了,代码如下所示。


public class Category {
    private int id;
    private String categoryName;
    private int categoryCode;

    public Category() {
    }

    public Category(int id, String categoryName, int categoryCode) {
        this.id = id;
        this.categoryName = categoryName;
        this.categoryCode = categoryCode;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCategoryName() {
        return categoryName;
    }

    public void setCategoryName(String categoryName) {
        this.categoryName = categoryName;
    }

    public int getCategoryCode() {
        return categoryCode;
    }

    public void setCategoryCode(int categoryCode) {
        this.categoryCode = categoryCode;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

改完了所有想改的东西,只需要记得版本号加1就行了。当然由于这里添加了一个新的模型类,因此也需要将它添加到映射模型的列表中,修改litepal.xml中的代码,如下所示:

<?xml version="1.0" encoding="UTF-8" ?>

<litepal>
    <dbname value="Book"></dbname>

    <version value="2"/>

    <list>
        <mapping class="com.example.litepaltest.Book"></mapping>
        <mapping class="com.example.litepaltest.Category"></mapping>
    </list>
</litepal>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在这里插入图片描述
在这里插入图片描述

可以看到,book表中新增了一个press列,category表也创建成功了,当然LtiePal还帮我们做了一项非常重要的工作,就是保留所有的数据,这样就再也不用担心数据丢失的问题了。

使用LitePal添加数据

注意使用LitePal进行表管理操作的时候不需要模型类有任何继承结构的,但是进行CRUD操作时就不行了,因为要使用到save()方法,所以必须要继承LitePalSupport类才行,因此这里需要先把继承结构给加上 如下:


public class Book  extends LitePalSupport {
 
}

  • 1
  • 2
  • 3
  • 4
  • 5

接着向Book表中增加数据,修改MainActivity.java中的代码

public class MainActivity extends AppCompatActivity {

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

        ...

        ((Button)findViewById(R.id.button2)).setOnClickListener((V)->{
            Book book = new Book();
            book.setId(1);
            book.setAuthor("Bruce Eckel");
            book.setName("Thinking in Java");
            book.setPages(1000);
            book.setPress("unknow");
            book.setPrice(100.9);
            book.save();
        }); 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在按钮的点击事件中,创建了一个book实例,然后调用book中的各种set方法对数据进行设置,然后再调用save就可以完成添加数据的操作了,那么这个save()方法是从哪儿来的呢?当然是从LitePalSupport类中继承而来的了。
现在重新运行程序,单击Add data按钮,此时数据已经添加成功了。
在这里插入图片描述

使用LitePal更新数据

最简单的种更新方式是对已存储对象重新设值,然后重新调用save()方法即可。

那么到底什么是已存储对象?
对于LitePal来说,对象是否已存储是根据调用model.isSaved()方法的结果来判断的,
返回true就表示已存储,返回false就表示为存储。那么接下来的问题是,什么情况下返回true,什么情况下返回false呢?

实际上只有两种情况下model.isSaved()方法才会返回true,一种情况是model对象是通过LitePal提供的查询Api查询出来的,由于是从数据库中查到的对象,因此也会被认为是已存储对象。
由于查询Api暂时还没学到,因此只能先通过第一种情况来进行验证。修改MainActivity.java中的代码


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
		...
        ((Button)findViewById(R.id.button2)).setOnClickListener((V)->{
            Book book = new Book();
            book.setId(2);
            book.setAuthor("Cay S. Horstmann");
            book.setPrice(70.5);
            book.setPress("unknow");
            book.setPages(1000);
            book.setName("Core Java Volume I");
            book.save();
            book.setPrice(70);
            book.save();
        });

   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在点击事件里面,先是添加了一条数据,然后调用setPrice()方法对这条数据进行修改,之后调用了save()方法。此时LitePal会发现当前的book对象是已存储的,因此不会再向数据库去添加一条新数据,而是直接更新当前的数据。

重新运行程序,点击Updata data按钮

在这里插入图片描述

可以看到数据表中又增加了一条书的数据,但这本书的价格并不是一开始的70.5,而是更新后的70.

但是这种更新方式只能对已存储数据进行操作,限制性比较大,接下来将使用更灵巧的方式更新。

修改MainActivity.java中的代码

public class MainActivity extends AppCompatActivity {

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

        ((Button)findViewById(R.id.button3)).setOnClickListener((V)->{
            Book book = new Book();
            book.setPrice(14.5);
            book.setPress("Anchor");
            book.updateAll("name = ? and author = ?","Thinking in Java","Bruce Eckel");
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到,这里首先new出了一个Book实例,然后直接调用setPrice()和setPress()来设置要更新的数据,最后调用updateAll()去执行更新操作。注意updata()方法中可以指定一个约束条件,和SQLiteDatabase中updata()方法的where参数部分有点类似,但更简洁,如果不指定条件语句的话,就表示更新全部数据。这里将所有书名为Thinking in Java并且作者是Bruce Eckel的书的价格更新为14.95,出版社更新为Anchor。
现在重新运行程序,并点击Updata data按钮,再来查一下表中数据的情况,结果如图:

在这里插入图片描述
发现Thinking in Java这本书的价格已经被更新为14.5了。

不过,如果你想把一个字段的值更新为默认值时,是不可以用上面的set方法的,因为java中任何一种数据类型的字段都会有默认值,例如int的默认值是0,boolean类型的默认值是false,String的默认值是null。那么当new出一个book对象时,其实所有的字段都已经被初始化成默认值了,比如说pages字段的值就是0,。因此即使不调用这行代码,pages字段本身也是0,LitePal此时是不会对这个列进行更新的。对于所有想要将数据改为默认值的操作,LitePal提供了一个setToDefault()方法,然后传入响应的列名就可以实现了。
比如可以这样写:
Book book = new Book();
book.setToDefault();
book.updataAll();
这段代码的意思是,将所有书页都更新为0,因为updata()方法中没有直达约束条件美因此更新操作对所有数据都生效了。

使用LitePal删除数据

使用LitePal删除数据的方式主要有两种,第一种比较简单,就是直接调用已存储对象的delete()方法就可以了,这种比较简单,在这里直接演示第二种删除数据的方式。


public class MainActivity extends AppCompatActivity {

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

        ((Button)findViewById(R.id.button4)).setOnClickListener((V)->{
            LitePal.deleteAll("book","price < ?" ,"70");
        });
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在点击事件中调用了LitePal中的deleteAll()方法,这个方法第一个参数指定了要删除的表名,第二个参数指定了删除的约束条件。
这行代码的意思是将价格低于70的书籍信息删掉删掉。

在这里插入图片描述

可以看到只有Bruce Eckel书的价格低于70,所以这条数据应该会被删掉。
重新运行程序,点击delete data按钮。
在这里插入图片描述
重新查看数据库,现在数据库中只剩下一本书的信息了,说明删除操作生效了。

使用LitePal查询数据

终于又到了最复杂的查询数据部分了,不过这个“”最复杂“”只是相对于过去而言,因为LitePal来查数据一点都不复杂。我一直认为LitePal在查询Api方面设计的极为人性化,想想我们之前使用的queryf()方法,冗长的代码让人看的头疼,技术多数参数都是用不到的,也不得不 传入null,如下所示:
Cursor cursor = db.query(“Book”,null,null,null,null,null,null);
像这样的代码恐怕是没人会喜欢的。为此LitePal在查询方面做了非常多的优化。

首先分析一下上述代码,query()方法中使用了第一个参数去查询Book表后面的参数全部为null,就表示希望查询这张表中所有的数据。那么使用LitePal如何完成同样的功能呢?非常简单只需要这样写:

List< Book> books = LitePal.findAll(Book.class);
另外findAll()方法的返回值是一个List集合,也就是说我们不用像之前那样通过Cursor对象一行行去取值了,LitePal已经帮我们自动完成了赋值操作。

修改MainActivity.java中的代码


public class MainActivity extends AppCompatActivity {

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

        ((Button)findViewById(R.id.button5)).setOnClickListener((V)->{
            List<Book> books = LitePal.findAll(Book.class);
            for (Book book : books) {
                Log.d("MainActivity","name:"+book.getName());
                Log.d("MainActivity","Id:"+book.getId());
                Log.d("MainActivity","Author:"+book.getAuthor());
                Log.d("MainActivity","Pages:"+book.getPages());
                Log.d("MainActivity","Press:"+book.getPress());
                Log.d("MainActivity","Price:"+book.getPrice());
            }

        });
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在点击事件里做了查询操作,并且遍历List集合,通过Log的方式打印出来。

点击按钮后,查看Logcat:
在这里插入图片描述
此时数据已经被成功取出。

除了findAll()方法之外,LitePal还提供了很多其他非常有用的API,比如我们想要查询Book表中的第一条数据就可以这样写:

	 Book firstBook = LitePal.findFirst(Book.class);
  • 1

查询Book表中最后一跳数据可以这样写:

  Book lastBook = LitePal.findLast(Book.class);
  • 1

还可以通过连缀查询来定制更多的查询功能。

查name和author两列数据可以这样写:

 List<Book> books = LitePal.select("name", "Author").find(Book.class);
  • 1

where方法用于指定查询约束条件,对应SQL当中的where关键字,比如只查页数大于400的数据,就可以这样写

            List<Book> books = LitePal.where("pages > ?", "400").find(Book.class);
  • 1

order方法用于指定结果的排序方式,对应了SQL当中的order by关键字,查询结果按照书价从高到底排序可以这样写。

 LitePal.order("price desc").find(Book.class);	    
  • 1

其中desc表示降序排列,asc表示升序排列。

limit方法用于指定查询结果的数量,比如只查表中前3条数据,就可以这样写:

List<Book> books = LitePal.limit(3).find(Book.class);	            
  • 1

offset方法用于指定查询结果数量,比如只查表中的第2条,第3条,第4条数据,就可以这样写:

 List<Book> books = LitePal.limit(3).offset(1).find(Book.class);
  • 1

由于limit(3)查询到的是前3条数据,这里我们再加上offset(1)进行一个位置偏移,就能实现查询第2条、第3条、第4条数据的功能了。limit()和offset()方法共同对应了SQL当中的limit关键字。

当然还可以对5个方法进行连缀组合,来完成一个比较复杂的查询操作:

        List<Book> books= LitePal.select("name","author","pages")
                .where("pages > ?","400")
                .order("pages")
                .limit(10)
                .offset(10)
                .find(Book.class);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这段代码就表示,查询Book表中第11~20条满足页数大于400这个条件的name、author和pages这3列数据,并将查询结果按照页数升序排列。

当然如果你实在有特殊需求以上API都满足不了你的时候,LitePal仍然支持原生态的SQL来进行查询。

       Cursor c = LitePal.findBySQL("select * from Book where pages > ? and price < ?", "400", "20");
  • 1

调用LitePal的findBySQL()方法,第一个参数指定SQL语句,后门的参数给占位符赋值,返回一个 Cursor对象,使用之前的方式取值就可以啦。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/494863
推荐阅读
相关标签
  

闽ICP备14008679号