数据存储与访问

一、文件存储读写

1.Android文件的操作模式

在这里插入图片描述

2.文件的相关操作方法

在这里插入图片描述

3.文件读写的实现

Android中的文件读写和Java中的文件I/O相同,流程也很简单,下面我们来写个简单的示例:
在这里插入图片描述
PS:这里用的是模拟器,因为笔者的N5并没有root,看不到文件的存储目录,下面我们打开DDMS 的File Exploer可以看到,在data/data/<包名>/file中有我们写入的文件:
在这里插入图片描述
我们可以点击右上角的响应图标将文件导入到电脑中,并且打开验证写入的内容:
在这里插入图片描述
在这里插入图片描述
代码实现:
首先是布局文件:main_activity.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.jay.example.filedemo1.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/nametitle" />

    <EditText
        android:id="@+id/editname"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/detailtitle" />

    <EditText
        android:id="@+id/editdetail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minLines="2" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnsave"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btnwrite" />

        <Button
            android:id="@+id/btnclean"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btnclean" />
    </LinearLayout>

    <Button
        android:id="@+id/btnread"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btnread" />

</LinearLayout>

然后我们来写一个文件协助类:FileHelper.java

/**
 * Created by Jay on 2015/9/1 0001.
 */
public class FileHelper {

    private Context mContext;

    public FileHelper() {
    }

    public FileHelper(Context mContext) {
        super();
        this.mContext = mContext;
    }

    /*
    * 这里定义的是一个文件保存的方法,写入到文件中,所以是输出流
    * */
    public void save(String filename, String filecontent) throws Exception {
        //这里我们使用私有模式,创建出来的文件只能被本应用访问,还会覆盖原文件哦
        FileOutputStream output = mContext.openFileOutput(filename, Context.MODE_PRIVATE);
        output.write(filecontent.getBytes());  //将String字符串以字节流的形式写入到输出流中
        output.close();         //关闭输出流
    }


    /*
    * 这里定义的是文件读取的方法
    * */
    public String read(String filename) throws IOException {
        //打开文件输入流
        FileInputStream input = mContext.openFileInput(filename);
        byte[] temp = new byte[1024];
        StringBuilder sb = new StringBuilder("");
        int len = 0;
        //读取文件内容:
        while ((len = input.read(temp)) > 0) {
            sb.append(new String(temp, 0, len));
        }
        //关闭输入流
        input.close();
        return sb.toString();
    }

}

最后是MainActivity.java,我们在这里完成相关操作:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editname;
    private EditText editdetail;
    private Button btnsave;
    private Button btnclean;
    private Button btnread;
    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        bindViews();
    }


    private void bindViews() {
        editdetail = (EditText) findViewById(R.id.editdetail);
        editname = (EditText) findViewById(R.id.editname);
        btnclean = (Button) findViewById(R.id.btnclean);
        btnsave = (Button) findViewById(R.id.btnsave);
        btnread = (Button) findViewById(R.id.btnread);

        btnclean.setOnClickListener(this);
        btnsave.setOnClickListener(this);
        btnread.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnclean:
                editdetail.setText("");
                editname.setText("");
                break;
            case R.id.btnsave:
                FileHelper fHelper = new FileHelper(mContext);
                String filename = editname.getText().toString();
                String filedetail = editdetail.getText().toString();
                try {
                    fHelper.save(filename, filedetail);
                    Toast.makeText(getApplicationContext(), "数据写入成功", Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    e.printStackTrace();
                    Toast.makeText(getApplicationContext(), "数据写入失败", Toast.LENGTH_SHORT).show();
                }
                break;
            case R.id.btnread:
                String detail = "";
                FileHelper fHelper2 = new FileHelper(getApplicationContext());
                try {
                    String fname = editname.getText().toString();
                    detail = fHelper2.read(fname);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                Toast.makeText(getApplicationContext(), detail, Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

4.读取SD卡上的文件

读取流程图:
在这里插入图片描述
代码示例:
运行效果图:

在这里插入图片描述
同样打开DDMS的File Explorer,在旧版本的系统上我们可以直接在mmt\sdcard上找到,但是新版本 的就可能需要我们自己找找了,首先我们来到这个路径下:
在这里插入图片描述
点开sdcard,但是没东西,我们继续找唠叨后面这个/storage/emulated/legacy下找:
在这里插入图片描述
好吧,他又跳到别的地方去了,我们继续找/storage/shell/emilated/0
在这里插入图片描述
果然找到了,我们在SD卡里生成的test.txt!导出到电脑看下里面的内容:
在这里插入图片描述
嘿嘿,果然读写SD卡成功~接下来我们来看下代码是怎么写的:
代码实现:
main_activity.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.jay.example.filedemo2.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="清输入文件名" />

    <EditText
        android:id="@+id/edittitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="文件名" />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="清输入文件内容" />

    <EditText
        android:id="@+id/editdetail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="文件内容" />

    <Button
        android:id="@+id/btnsave"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="保存到SD卡" />

    <Button
        android:id="@+id/btnclean"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="清空" />

    <Button
        android:id="@+id/btnread"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="读取sd卡中的文件" />

</LinearLayout>

接着我们来写一个SD操作类: SDFileHelper.java

/**
 * Created by Jay on 2015/9/1 0001.
 */
public class SDFileHelper {

    private Context context;

    public SDFileHelper() {
    }

    public SDFileHelper(Context context) {
        super();
        this.context = context;
    }

    //往SD卡写入文件的方法
    public void savaFileToSD(String filename, String filecontent) throws Exception {
        //如果手机已插入sd卡,且app具有读写sd卡的权限
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            filename = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + filename;
            //这里就不要用openFileOutput了,那个是往手机内存中写数据的
            FileOutputStream output = new FileOutputStream(filename);
            output.write(filecontent.getBytes());
            //将String字符串以字节流的形式写入到输出流中
            output.close();
            //关闭输出流
        } else Toast.makeText(context, "SD卡不存在或者不可读写", Toast.LENGTH_SHORT).show();
    }

    //读取SD卡中文件的方法
    //定义读取文件的方法:
    public String readFromSD(String filename) throws IOException {
        StringBuilder sb = new StringBuilder("");
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            filename = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + filename;
            //打开文件输入流
            FileInputStream input = new FileInputStream(filename);
            byte[] temp = new byte[1024];

            int len = 0;
            //读取文件内容:
            while ((len = input.read(temp)) > 0) {
                sb.append(new String(temp, 0, len));
            }
            //关闭输入流
            input.close();
        }
        return sb.toString();
    }


}

接着MainActivity.java实现相关逻辑:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editname;
    private EditText editdetail;
    private Button btnsave;
    private Button btnclean;
    private Button btnread;
    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        bindViews();
    }

    private void bindViews() {
        editname = (EditText) findViewById(R.id.edittitle);
        editdetail = (EditText) findViewById(R.id.editdetail);
        btnsave = (Button) findViewById(R.id.btnsave);
        btnclean = (Button) findViewById(R.id.btnclean);
        btnread = (Button) findViewById(R.id.btnread);

        btnsave.setOnClickListener(this);
        btnclean.setOnClickListener(this);
        btnread.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btnclean:
                editdetail.setText("");
                editname.setText("");
                break;
            case R.id.btnsave:
                String filename = editname.getText().toString();
                String filedetail = editdetail.getText().toString();
                SDFileHelper sdHelper = new SDFileHelper(mContext);
                try
                {
                    sdHelper.savaFileToSD(filename, filedetail);
                    Toast.makeText(getApplicationContext(), "数据写入成功", Toast.LENGTH_SHORT).show();
                }
                catch(Exception e){
                    e.printStackTrace();
                    Toast.makeText(getApplicationContext(), "数据写入失败", Toast.LENGTH_SHORT).show();
                }
                break;
            case R.id.btnread:
                String detail = "";
                SDFileHelper sdHelper2 = new SDFileHelper(mContext);
                try
                {
                    String filename2 = editname.getText().toString();
                    detail = sdHelper2.readFromSD(filename2);
                }
                catch(IOException e){e.printStackTrace();}
                Toast.makeText(getApplicationContext(), detail, Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

最后别忘记在AndroidManifest.xml写上读写SD卡的权限哦!

<!--SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!--SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

5.关于原生模拟器SD卡的问题

如果是真机调试的话通常都是可以的,对于原生虚拟机的话就问题多多了,再我们前面使用 Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)可能 一直返回的是false,就是SD卡不存在,这个是主要的问题,现在新版本的SDK都会在 创建AVD的时候会同时申请一块SD卡的存储区域的
在这里插入图片描述
对于旧版本的sdk或者其他原因可能需要手动关联下sd卡,设置如下:
①找到创建好的avd的镜像的路径:
点击打开avd界面,点击detail,查看avd镜像的目录下
在这里插入图片描述
②来到avd镜像所在的路径下,复制sdcard.img的路径:
比如我的:-sdcard C:\Users\Administrator.android\avd\Jay4.2.avd\sdcard.img
在这里插入图片描述
③接着点击 来到以下界面:
在这里插入图片描述

6.读取raw和assets文件夹下的文件

相信大家对两个文件夹并不陌生,如果我们不想自己的文件被编译成二进制文件的话, 我们可以把文件放到这两个目录下,而两者的区别如下:
res/raw:文件会被映射到R.java文件中,访问的时候直接通过资源ID即可访问,而且 他不能有目录结构,就是不能再创建文件夹
assets:不会映射到R.java文件中,通过AssetManager来访问,能有目录结构,即, 可以自行创建文件夹

读取文件资源:
res/raw:
InputStream is =getResources().openRawResource(R.raw.filename);

assets:
AssetManager am = getAssets();
InputStream is = am.open(“filename”);

二、SharedPreferences保存用户偏好参数

本节给大家介绍的是第二种存储用户数据的方式,使用SharedPreferences(保存用户偏好参数)保存数据, 当我们的应用想要保存用户的一些偏好参数,比如是否自动登陆,是否记住账号密码,是否在Wifi下才能 联网等相关信息,如果使用数据库的话,显得有点大材小用了!我们把上面这些配置信息称为用户的偏好 设置,就是用户偏好的设置,而这些配置信息通常是保存在特定的文件中!比如windows使用ini文件, 而J2SE中使用properties属性文件与xml文件来保存软件的配置信息;而在Android中我们通常使用 一个轻量级的存储类——SharedPreferences来保存用户偏好的参数!SharedPreferences也是使用xml文件, 然后类似于Map集合,使用键-值的形式来存储数据;我们只需要调用SharedPreferences的getXxx(name), 就可以根据键获得对应的值!使用起来很方便!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.SharedPreferences使用示例:

SharedPreferences是Android的一个轻量级存储工具,它采用的存储结构是Key-Value的键值对方式,类似于Java的Properties,二者都是把Key-Value的键值对保存在配置文件中。不同的是,Properties的文件内容形如Key=Value,而SharedPreferences的存储介质是XML文件,且以XML标记保存键值对。保存共享参数键值对信息的文件路径为:/data/data/应用包名/shared_prefs/文件名.xml。下面是一个共享参数的XML文件例子:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">Mr Lee</string>
<int nane="age" value="30"/>
<boolean name="married" value="true" />
<float name="weight" value="100.0"/>
</map>

基于XML格式的特点,共享参数主要用于如下场合:
(1)简单且孤立的数据。若是复杂且相互关联的数据,则要保存于关系数据库。
(2)文本形式的数据。若是二进制数据,则要保存至文件。
(3)需要持久化存储的数据。App退出后再次启动时,之前保存的数据仍然有效。

实际开发中,共享参数经常存储的数据包括:App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。

共享参数对数据的存储和读取操作类似于Map,也有存储数据的put方法,以及读取数据的get方法。调用getSharedPreferences方法可以获得共享参数实例,获取代码示例如下:
// 从share.xml获取共享参数实例
SharedPreferences shared = getSharedPreferences(“share”, MODE_PRIVATE);

由以上代码可知,getSharedPreferences方法的第一个参数是文件名,填share表示共享参数的文件名是share.xml;第二个参数是操作模式,填MODE_PRIVATE表示私有模式。
往共享参数存储数据要借助于Editor类,保存数据的代码示例如下:

SharedPreferences.Editor editor = shared.edit(); // 获得编辑器的对象
editor.putString("name", "Mr Lee"); // 添加一个名为name的字符串参数
editor.putInt("age", 30); // 添加一个名为age的整型参数
editor.putBoolean("married", true); // 添加一个名为married的布尔型参数
editor.putFloat("weight", 100f); // 添加一个名为weight的浮点数参数
editor.commit(); // 提交编辑器中的修改

从共享参数读取数据相对简单,直接调用共享参数实例的get * * * 方法即可读取键值,注意 get***方法的第二个参数表示默认值,读取数据的代码示例如下:

String name = shared.getString ( "name.","");//从共享参数获取名为name的字符串
int age = shared.getInt ("age",0);// 从共享参数获取名为age 的整型数
boolean married = shared.getBoolean ( "married"false);//从共享参数获取名为married
的布尔数
float weight = shared.getFloat ( "weight"0);//从共享参数获取名为weight的浮点数

下面通过测试页面演示共享参数的存取过程,先在编辑页面录入用户注册信息,点击保存按钮把数据提交至共享参数,如图6-1所示。再到查看页面浏览用户注册信息,App从共享参数中读取各项数据,并将注册信息显示在页面上,如图6-2所示。
在这里插入图片描述

使用流程图:
在这里插入图片描述
实现代码示例:
运行效果图:

流程是输入账号密码后点击登录,将信息保存到SharedPreference文件中, 然后重启app,看到数据已经显示在文本框中了
在这里插入图片描述
另外保存后,我们可以在File Expoler打开data/data/<包名>可以看到在shared_prefs目录下 生成了一个xml文件(因为N5没root,这里找了以前的效果图):
在这里插入图片描述
点击导出到桌面可以看到里面的内容:
在这里插入图片描述
代码实现:
布局文件activity_main.xml的编写:

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="用户登陆" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="请输入用户名" />

    <EditText
        android:id="@+id/editname"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="用户名" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="请输入密码" />

    <EditText
        android:id="@+id/editpasswd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="密码"
        android:inputType="textPassword" />

    <Button
        android:id="@+id/btnlogin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="登录" />
</LinearLayout>

编写简单的SP工具类:SharedHelper.java:

/**
 * Created by Jay on 2015/9/2 0002.
 */
public class SharedHelper {

    private Context mContext;

    public SharedHelper() {
    }

    public SharedHelper(Context mContext) {
        this.mContext = mContext;
    }


    //定义一个保存数据的方法
    public void save(String username, String passwd) {
        SharedPreferences sp = mContext.getSharedPreferences("mysp", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.putString("username", username);
        editor.putString("passwd", passwd);
        editor.commit();
        Toast.makeText(mContext, "信息已写入SharedPreference中", Toast.LENGTH_SHORT).show();
    }

    //定义一个读取SP文件的方法
    public Map<String, String> read() {
        Map<String, String> data = new HashMap<String, String>();
        SharedPreferences sp = mContext.getSharedPreferences("mysp", Context.MODE_PRIVATE);
        data.put("username", sp.getString("username", ""));
        data.put("passwd", sp.getString("passwd", ""));
        return data;
    }
}

最后是MainActivity.java实现相关逻辑:

public class MainActivity extends AppCompatActivity {

    private EditText editname;
    private EditText editpasswd;
    private Button btnlogin;
    private String strname;
    private String strpasswd;
    private SharedHelper sh;
    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        sh = new SharedHelper(mContext);
        bindViews();
    }

    private void bindViews() {
        editname = (EditText)findViewById(R.id.editname);
        editpasswd = (EditText)findViewById(R.id.editpasswd);
        btnlogin = (Button)findViewById(R.id.btnlogin);
        btnlogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                strname = editname.getText().toString();
                strpasswd = editpasswd.getText().toString();
                sh.save(strname,strpasswd);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        Map<String,String> data = sh.read();
        editname.setText(data.get("username"));
        editpasswd.setText(data.get("passwd"));
    }
}

2.读取其他应用的SharedPreferences

核心: 获得其他app的Context,而这个Context代表访问该app的全局信息的接口,而决定应用的唯一标识 是应用的包名,所以我们可以通过应用包名获得对应app的Context 另外有一点要注意的是:其他应用的SP文件是否能被读写的前提就是SP文件是否指定了可读或者 可写的权限,我们上面创建的是MODE_PRIVATE的就不可以了,所以说你像读别人的SP里的数据, 很难,另外,一些关键的信息,比如密码保存到SP里,一般都是会做加密的,所以只能自己写自己玩~ 等下会讲下常用的MD5加密方法!

实现流程图:
在这里插入图片描述
代码示例:
运行效果图:

在这里插入图片描述
代码实现:
我们读取SP的操作放在MainActivity.java中完成,点击按钮后读取SP,并通过Toast显示出来:

public class MainActivity extends AppCompatActivity {

    private Context othercontext;
    private SharedPreferences sp;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btnshow = (Button) findViewById(R.id.btnshow);
        btnshow.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //获得第一个应用的包名,从而获得对应的Context,需要对异常进行捕获
                try {
                    othercontext = createPackageContext("com.jay.sharedpreferencedemo", Context.CONTEXT_IGNORE_SECURITY);
                } catch (PackageManager.NameNotFoundException e) {
                    e.printStackTrace();
                }
                //根据Context取得对应的SharedPreferences
                sp = othercontext.getSharedPreferences("mysp", Context.MODE_WORLD_READABLE);
                String name = sp.getString("username", "");
                String passwd = sp.getString("passwd", "");
                Toast.makeText(getApplicationContext(), "Demo1的SharedPreference存的\n用户名为:" + name + "\n密码为:" + passwd, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

3.使用MD5对SharedPreference的重要数据进行加密

嘿嘿,上面我们这样直接把账号密码保存到sp里,如果没root的手机,别的应用倒无法访问手机, 如果root了,然后数据给其他应用获取到,然后造成了一些后果,这…就不怪我们了,哈哈, 谁叫你root了~,这锅我们不背,的确是这样!但是作为一名有责任心的APP开发人员,我们总不能 这样是吧,我们可以使用一些加密算法对用户密码进行加密,另外我们一般加密的都是用户密码! 下面我们简画个简单的图帮助大家理解下加密的处理的流程:

1.简单的加密处理流程

流程图如下:**
在这里插入图片描述
流程图解析:
Step 1.用户注册账号密码,账号密码校验后(账号是否重复,密码位数 > 6位等), 即账号密码有效,注册成功后,我们提交给服务器的账号,以及本地加密过的密码!
Step 2.服务器端将用户提交的账号,加密过的密码保存到服务端的数据库中,也就是服务 端并不会保存我们的明文密码(原始)密码!
Step 3.说回客户端,如果注册成功或者登陆成功,你想保存账号密码到SP中,保存的的密码 也需要走一趟加密流程!即明文密码——>加密,再保存!如果不保存,每次请求的时候,明文密码 也要走一趟家里流程,然后拿着加密后的密码来请求服务器!
Step 4.服务器验证账号以及加密密码,成功,分配客户端一个session标识,后续客户端可以拿着 这个session来访问服务端提供的相关服务!

嘿嘿,理解了吧,加密的方法有很多种,小猪也不是这方面的高玩,以前使用过的加密方法是MD5 加密,本节也给大家简单介绍一下这个MD5加密,以及演示下用法~

2.MD5简单介绍:

1)MD5是什么鬼?:

答:Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛 使用的一种散列函数,用以提供消息的完整性保护——摘自《百度百科》 简单点说就是一种加密算法,可以将一个字符串,或者文件,压缩包,执行MD5加密后, 就可以生产一个固定长度为128bit的串!这个串基本唯一!另外我们都知道:一个十六进制 需要用4个bit来表示,那么对应的MD5的字符串长度就为:128 / 4 = 32位了!另外可能 你看到一些md5是16位的,只是将32位MD5码去掉了前八位以及后八位!不信么,我们来试试 百度一下:md5在线解密,第一个:http://www.cmd5.com/
在这里插入图片描述

2)MD5能破解吗?

答:MD5不可逆,就是说没有对应的算法,无法从生成的md5值逆向得到原始数据! 当然暴力破解除外,简单的MD5加密后可以查MD5库~

3)MD5值唯一吗?

答:不唯一,一个原始数据只对应一个MD5值,但是一个MD5值可能对应多个原始数据!

3.MD5加密实现例子:

其实网上有很多写好的MD5的例子,百度或者谷歌一搜一大堆,这里提供下小猪用的MD5加密工具类!
Md5Util.java:

/**
 * Created by Jay on 2015/9/2 0002.
 */
public class MD5 {
    public static String getMD5(String content) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(content.getBytes());
            return getHashString(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String getHashString(MessageDigest digest) {
        StringBuilder builder = new StringBuilder();
        for (byte b : digest.digest()) {
            builder.append(Integer.toHexString((b >> 4) & 0xf));
            builder.append(Integer.toHexString(b & 0xf));
        }
        return builder.toString();
    }
}


MainActivity.java直接调用getMD5这个静态方法:
Log.e(“HeHe”, MD5.getMD5(“呵呵”));

我们可以看到Logcat上打印出:
在这里插入图片描述

这就是加密过后的呵呵了,我们可以把这串密文拷贝到上面这个md5的在线解密网站:
在这里插入图片描述
嘿嘿,果然,只是这样加密一次,就直接破解了,有点不安全的样子,那就加密100次咯, 就是将加密后的字符串再加密,重复100次,我们在原先的基础上加个加密一百次的方法:
public static String getMD5x100(String content){
String s1 = content;
for(int i = 0;i < 100;i++){
s1 = getMD5(s1);
}
return s1;
}

然后调用下,发现打印这个的Log:
在这里插入图片描述
复制界面网站上:
在这里插入图片描述

4.SharedPreference工具类:

每次都要自行实例化SP相关的类,肯定很麻烦,这里贴个SP的工具类,大家可以贴到 自己的项目中,工具类来源于鸿洋大神的blog~

SPUtils.java

package com.jay.sharedpreferencedemo3;

import android.content.Context;
import android.content.SharedPreferences;

import java.util.Map;

/**
 * Created by Jay on 2015/9/2 0002.
 */
public class SPUtils {
    /**
     * 保存在手机里的SP文件名
     */
    public static final String FILE_NAME = "my_sp";

    /**
     * 保存数据
     */
    public static void put(Context context, String key, Object obj) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME, context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        if (obj instanceof Boolean) {
            editor.putBoolean(key, (Boolean) obj);
        } else if (obj instanceof Float) {
            editor.putFloat(key, (Float) obj);
        } else if (obj instanceof Integer) {
            editor.putInt(key, (Integer) obj);
        } else if (obj instanceof Long) {
            editor.putLong(key, (Long) obj);
        } else {
            editor.putString(key, (String) obj);
        }
        editor.commit();
    }


    /**
     * 获取指定数据
     */
    public static Object get(Context context, String key, Object defaultObj) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME, context.MODE_PRIVATE);
        if (defaultObj instanceof Boolean) {
            return sp.getBoolean(key, (Boolean) defaultObj);
        } else if (defaultObj instanceof Float) {
            return sp.getFloat(key, (Float) defaultObj);
        } else if (defaultObj instanceof Integer) {
            return sp.getInt(key, (Integer) defaultObj);
        } else if (defaultObj instanceof Long) {
            return sp.getLong(key, (Long) defaultObj);
        } else if (defaultObj instanceof String) {
            return sp.getString(key, (String) defaultObj);
        }
        return null;
    }

    /**
     * 删除指定数据
     */
    public static void remove(Context context, String key) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME, context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.remove(key);
        editor.commit();
    }


    /**
     * 返回所有键值对
     */
    public static Map<String, ?> getAll(Context context) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME, context.MODE_PRIVATE);
        Map<String, ?> map = sp.getAll();
        return map;
    }

    /**
     * 删除所有数据
     */
    public static void clear(Context context) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME, context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.clear();
        editor.commit();
    }

    /**
     * 检查key对应的数据是否存在
     */
    public static boolean contains(Context context, String key) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME, context.MODE_PRIVATE);
        return sp.contains(key);
    }

}

5.实现记住密码功能

上一章末尾的实战项目,登录页面下方有一个“记住密码”复选框,当时只是为了演示控件的用法,并未真正记住密码。因为用户退出后重新进入登录页面,App没有回忆起上次的登录密码。现在利用共享参数改造该项目,使之实现记住密码的功能。
改造内容主要有下列3处:
(1)声明一个共享参数对象,并在onCreate中调用getSharedPreferences方法获取共享参数的实例。
(2)登录成功时,如果用户勾选了“记住密码”,就使用共享参数保存手机号码与密码。也就是在loginSuccess方法中增加以下代码:

// 如果勾选了“记住密码”,就把手机号码和密码都保存到共享参数中
if (isRemember) {
SharedPreferences.Editor editor = mShared.edit(); // 获得编辑器的对象
editor.putString("phone", et_phone.getText().toString()); // 添加名叫phone的手
机号码
editor.putString("password", et_password.getText().toString()); // 添加名叫
password的密码
editor.commit(); // 提交编辑器中的修改
}

(3)再次打开登录页面时,App从共享参数读取手机号码与密码,并自动填入编辑框。也就是在onCreate方法中增加以下代码:

// 从share_login.xml获取共享参数对象
mShared = getSharedPreferences("share_login", MODE_PRIVATE);
// 获取共享参数保存的手机号码
String phone = mShared.getString("phone", "");
// 获取共享参数保存的密码
String password = mShared.getString("password", "");
et_phone.setText(phone); // 往手机号码编辑框填写上次保存的手机号
et_password.setText(password); // 往密码编辑框填写上次保存的密码

代码修改完毕,只要用户上次登录成功时勾选“记住密码”,下次进入登录页面后App就会自动填写上次登录的手机号码与密码。具体的效果如图6-3和图6-4所示。其中,图6-3为用户首次登录成功的界面,此时勾选了“记住密码”;图6-4为用户再次进入登录的界面,因为上次登录成功时已经记住密码,所以这次页面会自动填充保存的登录信息。
在这里插入图片描述
在这里插入图片描述

6.利用设备浏览器寻找共享参数文件

共享参数的基本用法”提到,参数文件的路径为“/data/data/应用包名/shared_prefs/* * *
.xml”,然而使用手机自带的文件管理器却找不到该路径,data下面只有空目录而已。这是因为手机厂商加了层保护,不让用户查看App的核心文件,否则万一不小心误删了,App岂不是运行报错了?当然作为开发者,只要打开了手机的USB调试功能,还是有办法拿到测试应用的数据文件。首先打开AndroidStudio,依次选择菜单Run→Run ‘***’,把测试应用比如chapter06安装到手机上。接着单击Android Studio左下角的logcat标签,找到已连接的手机设备和测试应用,如图6-5所示。
在这里插入图片描述
在这里插入图片描述

三、SQLite数据库

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

1.基本概念

1)SQLite是什么?为什么要用SQLite?SQLite有什么特点?

答:①SQLite是一个轻量级的关系型数据库,运算速度快,占用资源少,很适合在移动设备上使用, 不仅支持标准SQL语法,还遵循ACID(数据库事务)原则,无需账号,使用起来非常方便!

②前面我们学习了使用文件与SharedPreference来保存数据,但是在很多情况下, 文件并不一定是有效的,如多线程并发访问是相关的;app要处理可能变化的复杂数据结构等等! 比如银行的存钱与取钱!使用前两者就会显得很无力或者繁琐,数据库的出现可以解决这种问题, 而Android又给我们提供了这样一个轻量级的SQLite,为何不用?

③SQLite支持五种数据类型:NULL,INTEGER,REAL(浮点数),TEXT(字符串文本)和BLOB(二进制对象) 虽然只有五种,但是对于varchar,char等其他数据类型都是可以保存的;因为SQLite有个最大的特点: 你可以各种数据类型的数据保存到任何字段中而不用关心字段声明的数据类型是什么,比如你 可以在Integer类型的字段中存放字符串,当然除了声明为主键INTEGER PRIMARY KEY的字段只能够存储64位整数! 另外, SQLite 在解析CREATE TABLE 语句时, 会忽略 CREATE TABLE 语句中跟在字段名后面的数据类型信息如下面语句会忽略 name字段的类型信息: CREATE TABLE person (personid integer primary key autoincrement, name varchar(20))

小结下特点:
SQlite通过文件来保存数据库,一个文件就是一个数据库,数据库中又包含多个表格,表格里又有 多条记录,每个记录由多个字段构成,每个字段有对应的值,每个值我们可以指定类型,也可以不指定 类型(主键除外)

2)几个相关的类:

我们先来说下几个 我们在使用数据库时用到的三个类:
SQLiteOpenHelper:抽象类,我们通过继承该类,然后重写数据库创建以及更新的方法, 我们还可以通过该类的对象获得数据库实例,或者关闭数据库!
SQLiteDatabase:数据库访问类:我们可以通过该类的对象来对数据库做一些增删改查的操作
Cursor:游标,有点类似于JDBC里的resultset,结果集!可以简单理解为指向数据库中某 一个记录的指针!

3)SQL的基本语法

SQL本质上是一种编程语言,它的学名叫作“结构化查询语言”(全称为Structured Query Language,简称SQL)。不过SQL语言并非通用的编程语言,它专用于数据库的访问和处理,更像是一种操作命令,所以常说SQL语句而不说SQL代码。标准的SQL语句分为3类:数据定义、数据操纵和数据控制,但不同的数据库往往有自己的实现。

SQLite是一种小巧的嵌入式数据库,使用方便、开发简单。如同MySQL、Oracle那样,SQLite也采用SQL语句管理数据,由于它属于轻型数据库,不涉及复杂的数据控制操作,因此App开发只用到数据定义和数据操纵两类SQL。此外,SQLite的SQL语法与通用的SQL语法略有不同,接下来介绍的两类SQL语法全部基于SQLite。

1.数据定义语言

数据定义语言全称Data Definition Language,简称DDL,它描述了怎样变更数据实体的框架结构。就SQLite而言,DDL语言主要包括3种操作:创建表格、删除表格、修改表结构,分别说明如下。
(1)创建表格
表格的创建动作由create命令完成,格式为“CREATE TABLE IF NOT EXISTS 表格名称(以逗号分隔的各字段定义);”。以用户信息表为例,它的建表语句如下所示:

CREATE TABLE IF NOT EXISTS user_info (
_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name VARCHAR NOT NULL,
age INTEGER NOT NULL,
height LONG NOT NULL,
weight FLOAT NOT NULL,
married INTEGER NOT NULL,
update_time VARCHAR NOT NULL);

上面的SQL语法与其他数据库的SQL语法有所出入,相关的注意点说明见下:
①SQL语句不区分大小写,无论是create与table这类关键词,还是表格名称、字段名称,都不区分大小写。唯一区分大小写的是被单引号括起来的字符串值。
②为避免重复建表,应加上IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS 表格名称……
③SQLite支持整型INTEGER、长整型LONG、字符串VARCHAR、浮点数FLOAT,但不支持布尔类型。布尔类型的数据要使用整型保存,如果直接保存布尔数据,在入库时SQLite会自动将它转为0或1,其中0表示false,1表示true。
④建表时需要唯一标识字段,它的字段名为id。创建新表都要加上该字段定义,例如id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL。

(2)删除表格
表格的删除动作由drop命令完成,格式为“DROP TABLE IF EXISTS 表格名称;”。下面是删除用户信息表的SQL语句例子:
DROP TABLE IF EXISTS user_info;

(3)修改表结构
表格的修改动作由alter命令完成,格式为“ALTER TABLE 表格名称 修改操作;”。不过SQLite只支持增加字段,不支持修改字段,也不支持删除字段。对于字段增加操作,需要在alter之后补充add命令,具体格式如“ALTER TABLE 表格名称 ADD COLUMN 字段名称 字段类型;”。下面是给用户信息表增加手机号字段的SQL语句例子:
ALTER TABLE user_info ADD COLUMN phone VARCHAR;

注意,SQLite的ALTER语句每次只能添加一列字段,若要添加多列,就得分多次添加。

2.数据操纵语言

数据操纵语言全称Data Manipulation Language,简称DML,它描述了怎样处理数据实体的内部记录。表格记录的操作类型包括添加、删除、修改、查询4类,分别说明如下:
(1)添加记录
记录的添加动作由insert命令完成,格式为“INSERT INTO 表格名称(以逗号分隔的字段名列表)VALUES(以逗号分隔的字段值列表);”。下面是往用户信息表插入一条记录的SQL语句例子:

INSERT INTO user_info (name,age,height,weight,married,update_time)
VALUES ('张三',20,170,50,0,'20200504');

(2)删除记录
记录的删除动作由delete命令完成,格式为“DELETE FROM 表格名称 WHERE 查询条件;”,其中查询条件的表达式形如“字段名=字段值”,多个字段的条件交集通过“AND”连接,条件并集通过“OR”连接。
下面是从用户信息表删除指定记录的SQL语句例子:

DELETE FROM user_info WHERE name='张三';

(3)修改记录
记录的修改动作由update命令完成,格式为“UPDATE 表格名称 SET 字段名=字段值 WHERE 查询条件;”。下面是对用户信息表更新指定记录的SQL语句例子:

UPDATE user_info SET married=1 WHERE name='张三';

(4)查询记录
记录的查询动作由select命令完成,格式为“SELECT 以逗号分隔的字段名列表 FROM 表格名称WHERE 查询条件;”。如果字段名列表填星号“*”,则表示查询该表的所有字段。下面是从用户信息表查询指定记录的SQL语句例子:

SELECT name FROM user_info WHERE name='张三';

查询操作除了比较字段值条件之外,常常需要对查询结果排序,此时要在查询条件后面添加排序条件,对应的表达式为“ORDER BY 字段名 ASC或者DESC”,意指对查询结果按照某个字段排序,其中ASC代表升序,DESC代表降序。下面是查询记录并对结果排序的SQL语句例子:

SELECT * FROM user_info ORDER BY age ASC;

如果读者之前不熟悉SQL语法,建议下载一个SQLite管理软件,譬如SQLiteStudio,先在电脑上多加练习SQLite的常见操作语句。

2.使用SQLiteOpenHelper类创建数据库与版本管理

对于涉及数据库的app,我们不可能手动地去给他创建数据库文件,所以需要在第一次启用app 的时候就创建好数据库表;而当我们的应用进行升级需要修改数据库表的结构时,这个时候就需要 对数据库表进行更新了;对于这两个操作,安卓给我们提供了SQLiteOpenHelper的两个方法, onCreate( )与onUpgrade( )来实现

方法解析:
onCreate(database):首次使用软件时生成数据库表
onUpgrade(database,oldVersion,newVersion):在数据库的版本发生变化时会被调用, 一般在软件升级时才需改变版本号,而数据库的版本是由程序员控制的,假设数据库现在的 版本是1,由于业务的变更,修改了数据库表结构,这时候就需要升级软件,升级软件时希望 更新用户手机里的数据库表结构,为了实现这一目的,可以把原来的数据库版本设置为2 或者其他与旧版本号不同的数字即可!

代码示例:

public class MyDBOpenHelper extends SQLiteOpenHelper {
    public MyDBOpenHelper(Context context, String name, CursorFactory factory,
            int version) {super(context, "my.db", null, 1); }
    @Override
    //数据库第一次创建时被调用
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE person(personid INTEGER PRIMARY KEY AUTOINCREMENT,name VARCHAR(20))");
        
    }
    //软件版本号发生改变时调用
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("ALTER TABLE person ADD phone VARCHAR(12) NULL");
    }
}

代码解析:
上述代码第一次启动应用,我们会创建这个my.db的文件,并且会执行onCreate()里的方法, 创建一个Person的表,他又两个字段,主键personId和name字段;接着如我我们修改db的版本 号,那么下次启动就会调用onUpgrade()里的方法,往表中再插入一个字段!另外这里是插入 一个字段,所以数据不会丢失,如果是重建表的话,表中的数据会全部丢失,下一节我们会 来教大家如何解决这个问题!

流程小结:
Step 1:自定义一个类继承SQLiteOpenHelper类
Step 2:在该类的构造方法的super中设置好要创建的数据库名,版本号
Step 3:重写onCreate( )方法创建表结构
Step 4:重写onUpgrade( )方法定义版本号发生改变后执行的操作

3.如何查看我们生成的db文件

当我们调用上面的MyDBOpenhelper的对象的getWritableDatabase()就会在下述目录下创建我们的db 数据库文件:
在这里插入图片描述
我们发现数据库有两个,前者是我们创建的数据库,而后者则是为了能让数据库支持事务而产生的 临时的日志文件!一般的大小是0字节! 而在File Explorer里我们确是打不开文件的,连txt都打不开,何况是.db! 所以下面给大家两条路选:
1.先导出来,然后用SQLite的图形化工具查看
2.配置adb环境变量后,通过adb shell来查看(命令行,装比利器)!

方法1:使用SQLite图形化工具查看db文件
这类软件有很多,笔者用的是SQLite Expert Professional,当然你也可以使用其他工具 又需要的可以下载:SQLiteExpert.zip,把我们的db文件导出到电脑桌面,打开SQLiteExpert,界面如下:
在这里插入图片描述
方法2:adb shell命令行带你装逼带你飞
1.配置SDK环境变量:
右键我的电脑 ——> 高级系统设置 -> 环境变量 -> 新建系统变量 -> 把SDK的platform-tools路径拷贝下: 比如笔者的:C:\Software\Coding\android-sdks-as\platform-tools
在这里插入图片描述
确定,然后再找到Path的环境变量,编辑,然后在结尾加上:%SDK_HOME%;
在这里插入图片描述
然后打开命令行,输入adb,唰唰唰一堆东西,就说明配置成功了!

在执行后续命令行指令之前,针对你的测试的机器可能有几种: 1.原生模拟器:那行,你跳过这里,继续往下 2.Genymotion模拟器:没戏,Genymotion Shell执行不了下述命令 3.真机(已root):那么你打开File Explorer看看data/data/目录下有东西没?没么? 下面提供一个方法,就是先装个RE文件管理器,然后授予RE Root权限,接着来到根目录: 然后长按data目录,会弹出这样的对话框:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接着等他慢慢修改权限,修改完毕后,我们再次打开DDMS的File Explorer,我们可以看到:
在这里插入图片描述
好的,可以看到data/data里的东西了!

2.进入adb shell,接着键入下述指令,来到我们app的databases目录下:
在这里插入图片描述
接着依次输入下述指令:
sqlite3 my.db :打开数据库文件
.table 查看数据库中有哪些表 接着你直接输入数据库语句就可以了,比如查询:Select * from person
.schema:查看建表语句
.quit:退出数据库的编辑
.exit:退出设备控制台

…因为system/bin/sh sqlite3: not found,这个问题,后面Sqlite命令的都用不了, 要看效果图就自行查询郭大侠的书吧~而下面我们还是先导出db文件,然后用图形化的 数据库工具来查看!

4.使用Android提供的API操作SQLite

在这里插入图片描述
实现代码:
布局过于简单,就四个Button,就不贴了,直接贴MainActivity.java的代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Context mContext;
    private Button btn_insert;
    private Button btn_query;
    private Button btn_update;
    private Button btn_delete;
    private SQLiteDatabase db;
    private MyDBOpenHelper myDBHelper;
    private StringBuilder sb;
    private int i = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        myDBHelper = new MyDBOpenHelper(mContext, "my.db", null, 1);
        bindViews();
    }

    private void bindViews() {
        btn_insert = (Button) findViewById(R.id.btn_insert);
        btn_query = (Button) findViewById(R.id.btn_query);
        btn_update = (Button) findViewById(R.id.btn_update);
        btn_delete = (Button) findViewById(R.id.btn_delete);

        btn_query.setOnClickListener(this);
        btn_insert.setOnClickListener(this);
        btn_update.setOnClickListener(this);
        btn_delete.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        db = myDBHelper.getWritableDatabase();
        switch (v.getId()) {
            case R.id.btn_insert:
                ContentValues values1 = new ContentValues();
                values1.put("name", "呵呵~" + i);
                i++;
                //参数依次是:表名,强行插入null值得数据列的列名,一行记录的数据
                db.insert("person", null, values1);
                Toast.makeText(mContext, "插入完毕~", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_query:
                sb = new StringBuilder();
                //参数依次是:表名,列名,where约束条件,where中占位符提供具体的值,指定group by的列,进一步约束
                //指定查询结果的排序方式
                Cursor cursor = db.query("person", null, null, null, null, null, null);
                if (cursor.moveToFirst()) {
                    do {
                        int pid = cursor.getInt(cursor.getColumnIndex("personid"));
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        sb.append("id:" + pid + ":" + name + "\n");
                    } while (cursor.moveToNext());
                }
                cursor.close();
                Toast.makeText(mContext, sb.toString(), Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_update:
                ContentValues values2 = new ContentValues();
                values2.put("name", "嘻嘻~");
                //参数依次是表名,修改后的值,where条件,以及约束,如果不指定三四两个参数,会更改所有行
                db.update("person", values2, "name = ?", new String[]{"呵呵~2"});
                break;
            case R.id.btn_delete:
                //参数依次是表名,以及where条件与约束
                db.delete("person", "personid = ?", new String[]{"3"});
                break;
        }
    }
}

5.使用SQL语句操作数据库

当然,你可能已经学过SQL,会写相关的SQL语句,而且不想用Android提供的这些API, 你可以直接使用SQLiteDatabase给我们提供的相关方法:

execSQL(SQL,Object[]):使用带占位符的SQL语句,这个是执行修改数据库内容的sql语句用的
rawQuery(SQL,Object[]):使用带占位符的SQL查询操作 另外前面忘了介绍下Curosr这个东西以及相关属性,这里补充下: ——Cursor对象有点类似于JDBC中的ResultSet,结果集!使用差不多,提供一下方法移动查询结果的记录指针:
move(offset):指定向上或者向下移动的行数,整数表示向下移动;负数表示向上移动!
moveToFirst():指针移动到第一行,成功返回true,也说明有数据
moveToLast():指针移动到最后一样,成功返回true;
moveToNext():指针移动到下一行,成功返回true,表明还有元素!
moveToPrevious():移动到上一条记录
getCount( )获得总得数据条数
isFirst():是否为第一条记录
isLast():是否为最后一项
moveToPosition(int):移动到指定行

使用代码示例:

1.插入数据:

public void save(Person p)
{
    SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
    db.execSQL("INSERT INTO person(name,phone) values(?,?)",
                new String[]{p.getName(),p.getPhone()});
}

2.删除数据:

public void delete(Integer id)
{
    SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
    db.execSQL("DELETE FROM person WHERE personid = ?",
                new String[]{id});
}

3.修改数据:

public void update(Person p)
{
    SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
    db.execSQL("UPDATE person SET name = ?,phone = ? WHERE personid = ?",
        new String[]{p.getName(),p.getPhone(),p.getId()});
}

4.查询数据:

public Person find(Integer id)
{
    SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
    Cursor cursor =  db.rawQuery("SELECT * FROM person WHERE personid = ?",
            new String[]{id.toString()});
    //存在数据才返回true
    if(cursor.moveToFirst())
    {
        int personid = cursor.getInt(cursor.getColumnIndex("personid"));
        String name = cursor.getString(cursor.getColumnIndex("name"));
        String phone = cursor.getString(cursor.getColumnIndex("phone"));
        return new Person(personid,name,phone);
    }
    cursor.close();
    return null;
}

5.数据分页:

public List<Person> getScrollData(int offset,int maxResult)
{
    List<Person> person = new ArrayList<Person>();
    SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
    Cursor cursor =  db.rawQuery("SELECT * FROM person ORDER BY personid ASC LIMIT= ?,?",
        new String[]{String.valueOf(offset),String.valueOf(maxResult)});
    while(cursor.moveToNext())
    {
        int personid = cursor.getInt(cursor.getColumnIndex("personid"));
        String name = cursor.getString(cursor.getColumnIndex("name"));
        String phone = cursor.getString(cursor.getColumnIndex("phone"));
        person.add(new Person(personid,name,phone)) ;
    }
    cursor.close();
    return person;
}

6.查询记录数:

public long getCount()
{
    SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
    Cursor cursor =  db.rawQuery("SELECT COUNT (*) FROM person",null);
    cursor.moveToFirst();
    long result = cursor.getLong(0);
    cursor.close();
    return result;      
}   

PS:除了上面获取条数的方法外还可以使用cursor.getCount()方法获得数据的条数, 但是SQL语句要改改!比如SELECT * FROM person;

6.SQLite事务

在这里插入图片描述
写在事务里的所有数据库操作都成功,事务提交,否则,事务回滚,就是回到前面 的状态——未执行数据库操作的时候!另外,前面我们也将了,在data/data/<包名>/database/目录 下除了有我们创建的db文件外,还有一个xxx.db-journal这个文件就是用来让数据库支持事务而 产生的 临时的日志文件!

7.SQLite存储大二进制文件

当然,一般我们很少往数据库中存储大二进制文件,比如图片,音频,视频等,对于这些我们一般 是存储文件路径,但总会有些奇葩的需求,某天你突然想把这些文件存到数据库里,下面我们以 图片为例子,将图片保存到SQLite中,以及读取SQLite中的图片!
在这里插入图片描述

8.SimpleCursorAdapter绑定数据库数据

其实在讲ContentProvider我们就使用过这个东西来绑定联系人列表!这里就不写实例了, 直接上核心代码!需要的自己捣鼓捣鼓就好了,另外,现在我们一般很少自己写数据库的东西 ,一般是通过第三方的框架:ormlite,greenDao等,在进阶部分,我们会再来学习~
在这里插入图片描述

9.数据库升级

1)什么是数据库版本升级?怎么升级法?

答:假如我们开发了一款APP,里面用到了数据库,我们假定这个数据库版本为v1.0, 在这个版本,我们创建了一个x.db的数据库文件,我们通过onCreate()方法创建了第一个table, t_user,里面有两个字段:_id,user_id;后面我们想增加一个字段user_name,这个时候 我们就需要对数据库表的结构进行修改了,而我们可以把更新数据库的操作梵高onUpgrade() 方法中,我们只需要在实例化自定义SQLiteOpenHelper的时候,修改版本号,比如把1改成2 这样,就会自动调用onUpgrade()的方法了!另外,对于每个数据库版本我们都应该做好 相应的记录(文档),类似于下面这种:
在这里插入图片描述

2)一些疑问以及相关解决方案

①应用升级,数据库文件是否会删除?
答:不会!数据什么的都在!

②如果我想删除表中某个字段或者增加一个新的字段,原先的数据还在吗?
答:在的!

③你刚说的那种粗暴的更新数据库版本的方式,不保留数据的,可以贴下吗?
答:可以,这里用的是第三方的ormlite,你也可以自己写数据库创建以及删除的代码:
在这里插入图片描述
④比如是这种,假如我们已经升级到第三个版本了,我们在第二个版本增加了一个表, 然后第三个版本也增加了一个表,加入用户直接从第一个版本升级到第三个版本,这样 没经过第二个版本,就没有增加的那个表,这可怎么破?

答:很简单,我们可以在onUpgrade()里写一个switch(),结构如下:

public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource,
            int arg2, int arg3) {
    switch(arg2){
        case 1:
            db.execSQL(第一个版本的建表语句);
        case 2:
            db.execSQL(第二个版本的建表语句);
        case 3:
            db.execSQL(第三个版本的建表语句); 
    }
}

细心的你可能发现这里并没有写break,这就对了,这是为了保证跨版本升级时,每次数据库 修改都能全部执行到!这样可以保证表结构都是最新的!另外不一定是建表语句,修改表结构 也可以哦!

⑤旧表的设计太糟糕,很多字段要改,改动太多,想建一个新表,但是表名要一样 而且以前的一些数据要保存到新表中!

答:呵呵,给你跪了,当然,也有解决办法,下面说下思路:
1.将旧表改名成临时表: ALTER TABLE User RENAME TO _temp_User;
2.创建新表: CREATE TABLE User (u_id INTEGER PRIMARY KEY,u_name VARCHAR(20),u_age VARCHAR(4));
3.导入数据; INSERT INTO User SELECT u_id,u_name,“18” FROM _temp_User; //原表中没有的要自己设个默认值
4.删除临时表; DROP TABLE_temp_User;

10.数据库管理器SQLiteDatabase

SQL语句毕竟只是SQL命令,若要在Java代码中操纵SQLite,还需专门的工具类。SQLiteDatabase便是Android提供的SQLite数据库管理器,开发者可以在活动页面代码调用openOrCreateDatabase方法获取数据库实例,参考代码如下:

// 创建名为test.db的数据库。数据库如果不存在就创建它,如果存在就打开它
SQLiteDatabase db = openOrCreateDatabase(getFilesDir() + "/test.db",
Context.MODE_PRIVATE, null);
String desc = String.format("数据库%s创建%s", db.getPath(), (db!=null)?"成功":"失
败");
tv_database.setText(desc);
// deleteDatabase(getFilesDir() + "/test.db"); // 删除名为test.db数据库

首次运行测试App,调用openOrCreateDatabase方法会自动创建数据库,并返回该数据库的管理器实例,创建结果如图6-7所示。
在这里插入图片描述
获得数据库实例之后,就能对该数据库开展各项操作了。数据库管理器SQLiteDatabase提供了若干操作数据表的API,常用的方法有3类,列举如下:
1.管理类,用于数据库层面的操作
openDatabase:打开指定路径的数据库。
isOpen:判断数据库是否已打开。
close:关闭数据库。
getVersion:获取数据库的版本号。
setVersion:设置数据库的版本号。

2.事务类,用于事务层面的操作
beginTransaction:开始事务。
setTransactionSuccessful:设置事务的成功标志。
endTransaction:结束事务。执行本方法时,系统会判断之前是否调用了
setTransactionSuccessful方法,如果之前已调用该方法就提交事务,如果没有调用该方法就回滚事务。

3.数据处理类,用于数据表层面的操作
execSQL:执行拼接好的SQL控制语句。一般用于建表、删表、变更表结构。
delete:删除符合条件的记录。
update:更新符合条件的记录信息。
insert:插入一条记录。
query:执行查询操作,并返回结果集的游标。
rawQuery:执行拼接好的SQL查询语句,并返回结果集的游标。

在实际开发中,比较经常用到的是查询语句,建议先写好查询操作的select语句,再调用rawQuery方法执行查询语句。

11.数据库帮助器SQLiteOpenHelper

由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便;因此Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。SQLiteOpenHelper的具体使用步骤如下:
步骤一,新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在数据库版本升高时执行,在此可以根据新旧版本号变更表结构。
步骤二,为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接,说明如下:获取单例对象:确保在App运行过程中数据库只会打开一次,避免重复打开引起错误。
打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调
用getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。
关闭数据库连接:数据库操作完毕,调用数据库实例的close方法关闭连接。
步骤三, 提供对表记录增加、删除、修改、查询的操作方法。

能被SQLite直接使用的数据结构是ContentValues类,它类似于映射Map,也提供了put和get方法存取键值对。区别之处在于:ContentValues的键只能是字符串,不能是其他类型。ContentValues主要用于增加记录和更新记录,对应数据库的insert和update方法。

记录的查询操作用到了游标类Cursor,调用query和rawQuery方法返回的都是Cursor对象,若要获取全部的查询结果,则需根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如下:
1.游标控制类方法,用于指定游标的状态
close:关闭游标。
isClosed:判断游标是否关闭。
isFirst:判断游标是否在开头。
isLast:判断游标是否在末尾。

2.游标移动类方法,把游标移动到指定位置
moveToFirst:移动游标到开头。
moveToLast:移动游标到末尾。
moveToNext:移动游标到下一条记录。
moveToPrevious:移动游标到上一条记录。
move:往后移动游标若干条记录。
moveToPosition:移动游标到指定位置的记录。

3.获取记录类方法,可获取记录的数量、类型以及取值
getCount:获取结果记录的数量。
getInt:获取指定字段的整型值。
getLong:获取指定字段的长整型值。
getFloat:获取指定字段的浮点数值。
getString:获取指定字段的字符串值。
getType:获取指定字段的字段类型。

鉴于数据库操作的特殊性,不方便单独演示某个功能,接下来从创建数据库开始介绍,完整演示一下数据库的读写操作。用户注册信息的演示页面包括两个,分别是记录保存页面和记录读取页面,其中记录保存页面通过insert方法向数据库添加用户信息,完整代码见
chapter06\src\main\java\com\example\chapter06\SQLiteWriteActivity.java;而记录读取页面通过query方法从数据库读取用户信息,完整代码见
chapter06\src\main\java\com\example\chapter06\SQLiteReadActivity.java。

运行测试App,先打开记录保存页面,依次录入并将两个用户的注册信息保存至数据库,如图6-8和图6-9所示。再打开记录读取页面,从数据库读取用户注册信息并展示在页面上,如图6-10所示。
在这里插入图片描述
在这里插入图片描述
上述演示页面主要用到了数据库记录的添加、查询和删除操作,对应的数据库帮助器关键代码如下所示,尤其关注里面的insert、delete、update和query方法:

public class UserDBHelper extends SQLiteOpenHelper {
private static final String TAG = "UserDBHelper";
private static final String DB_NAME = "user.db"; // 数据库的名称
private static final int DB_VERSION = 1; // 数据库的版本号
private static UserDBHelper mHelper = null; // 数据库帮助器的实例
private SQLiteDatabase mDB = null; // 数据库的实例
public static final String TABLE_NAME = "user_info"; // 表的名称
private UserDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
private UserDBHelper(Context context, int version) {
super(context, DB_NAME, null, version);
}
// 利用单例模式获取数据库帮助器的唯一实例
public static UserDBHelper getInstance(Context context, int version) {
if (version > 0 && mHelper == null) {
mHelper = new UserDBHelper(context, version);
} else if (mHelper == null) {
mHelper = new UserDBHelper(context);
}
return mHelper;
}
// 打开数据库的读连接
public SQLiteDatabase openReadLink() {
if (mDB == null || !mDB.isOpen()) {
mDB = mHelper.getReadableDatabase();
}
return mDB;
}
// 打开数据库的写连接
public SQLiteDatabase openWriteLink() {
if (mDB == null || !mDB.isOpen()) {
mDB = mHelper.getWritableDatabase();
}
return mDB;
}
// 关闭数据库连接
public void closeLink() {
if (mDB != null && mDB.isOpen()) {
mDB.close();
mDB = null;
}
}
// 创建数据库,执行建表语句
public void onCreate(SQLiteDatabase db) {
Log.d(TAG, "onCreate");
String drop_sql = "DROP TABLE IF EXISTS " + TABLE_NAME + ";";
Log.d(TAG, "drop_sql:" + drop_sql);
db.execSQL(drop_sql);
String create_sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
+ "name VARCHAR NOT NULL," + "age INTEGER NOT NULL,"
+ "height INTEGER NOT NULL," + "weight FLOAT NOT NULL,"
+ "married INTEGER NOT NULL," + "update_time VARCHAR NOT NULL"
//演示数据库升级时要先把下面这行注释
+ ",phone VARCHAR" + ",password VARCHAR"
+ ");";
Log.d(TAG, "create_sql:" + create_sql);
db.execSQL(create_sql); // 执行完整的SQL语句
}
// 升级数据库,执行表结构变更语句
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.d(TAG, "onUpgrade oldVersion=" + oldVersion + ", newVersion=" +
newVersion);
if (newVersion > 1) {
//Android的ALTER命令不支持一次添加多列,只能分多次添加
String alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " +
"phone VARCHAR;";
Log.d(TAG, "alter_sql:" + alter_sql);
db.execSQL(alter_sql);
alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + "password
VARCHAR;";
Log.d(TAG, "alter_sql:" + alter_sql);
db.execSQL(alter_sql); // 执行完整的SQL语句
}
}
// 根据指定条件删除表记录
public int delete(String condition) {
// 执行删除记录动作,该语句返回删除记录的数目
return mDB.delete(TABLE_NAME, condition, null);
}
// 删除该表的所有记录
public int deleteAll() {
// 执行删除记录动作,该语句返回删除记录的数目
return mDB.delete(TABLE_NAME, "1=1", null);
}
// 往该表添加一条记录
public long insert(UserInfo info) {
List<UserInfo> infoList = new ArrayList<UserInfo>();
infoList.add(info);
return insert(infoList);
}
// 往该表添加多条记录
public long insert(List<UserInfo> infoList) {
long result = -1;
for (int i = 0; i < infoList.size(); i++) {
UserInfo info = infoList.get(i);
List<UserInfo> tempList = new ArrayList<UserInfo>();
// 如果存在同名记录,则更新记录
// 注意条件语句的等号后面要用单引号括起来
if (info.name != null && info.name.length() > 0) {
String condition = String.format("name='%s'", info.name);
tempList = query(condition);
if (tempList.size() > 0) {
update(info, condition);
result = tempList.get(0).rowid;
continue;
}
}
// 如果存在同样的手机号码,则更新记录
if (info.phone != null && info.phone.length() > 0) {
String condition = String.format("phone='%s'", info.phone);
tempList = query(condition);
if (tempList.size() > 0) {
update(info, condition);
result = tempList.get(0).rowid;
continue;
}
}
// 不存在唯一性重复的记录,则插入新记录
ContentValues cv = new ContentValues();
cv.put("name", info.name);
cv.put("age", info.age);
cv.put("height", info.height);
cv.put("weight", info.weight);
cv.put("married", info.married);
cv.put("update_time", info.update_time);
cv.put("phone", info.phone);
cv.put("password", info.password);
// 执行插入记录动作,该语句返回插入记录的行号
result = mDB.insert(TABLE_NAME, "", cv);
if (result == -1) { // 添加成功则返回行号,添加失败则返回-1
return result;
}
}
return result;
}
// 根据条件更新指定的表记录
public int update(UserInfo info, String condition) {
ContentValues cv = new ContentValues();
cv.put("name", info.name);
cv.put("age", info.age);
cv.put("height", info.height);
cv.put("weight", info.weight);
cv.put("married", info.married);
cv.put("update_time", info.update_time);
cv.put("phone", info.phone);
cv.put("password", info.password);
// 执行更新记录动作,该语句返回更新的记录数量
return mDB.update(TABLE_NAME, cv, condition, null);
}
public int update(UserInfo info) {
// 执行更新记录动作,该语句返回更新的记录数量
return update(info, "rowid=" + info.rowid);
}
// 根据指定条件查询记录,并返回结果数据列表
public List<UserInfo> query(String condition) {
String sql = String.format("select
rowid,_id,name,age,height,weight,married,update_time," +
"phone,password from %s where %s;", TABLE_NAME, condition);
Log.d(TAG, "query sql: " + sql);
List<UserInfo> infoList = new ArrayList<UserInfo>();
// 执行记录查询动作,该语句返回结果集的游标
Cursor cursor = mDB.rawQuery(sql, null);
// 循环取出游标指向的每条记录
while (cursor.moveToNext()) {
UserInfo info = new UserInfo();
info.rowid = cursor.getLong(0); // 取出长整型数
info.xuhao = cursor.getInt(1); // 取出整型数
info.name = cursor.getString(2); // 取出字符串
info.age = cursor.getInt(3); // 取出整型数
info.height = cursor.getLong(4); // 取出长整型数
info.weight = cursor.getFloat(5); // 取出浮点数
//SQLite没有布尔型,用0表示false,用1表示true
info.married = (cursor.getInt(6) == 0) ? false : true;
info.update_time = cursor.getString(7); // 取出字符串
info.phone = cursor.getString(8); // 取出字符串
info.password = cursor.getString(9); // 取出字符串
infoList.add(info);
}
cursor.close(); // 查询完毕,关闭数据库游标
return infoList;
}
}

12.优化记住密码功能

在“6.1.2 实现记住密码功能”中,虽然使用共享参数实现了记住密码功能,但是该方案只能记住一个用户的登录信息,并且手机号码跟密码没有对应关系,如果换个手机号码登录,前一个用户的登录信息就被覆盖了。真正的记住密码功能应当是这样的:先输入手机号码,然后根据手机号码匹配保存的密码,一个手机号码对应一个密码,从而实现具体手机号码的密码记忆功能。

现在运用数据库技术分条存储各用户的登录信息,并支持根据手机号查找登录信息,从而同时记住多个手机号的密码。具体的改造主要有下列3点:
(1)声明一个数据库的帮助器对象,然后在活动页面的onResume方法中打开数据库连接,在onPasue方法中关闭数据库连接,示例代码如下:

private UserDBHelper mHelper; // 声明一个用户数据库的帮助器对象
@Override
protected void onResume() {
super.onResume();
mHelper = UserDBHelper.getInstance(this, 1); // 获得用户数据库帮助器的实例
mHelper.openWriteLink(); // 恢复页面,则打开数据库连接
}
@Override
protected void onPause() {
super.onPause();
mHelper.closeLink(); // 暂停页面,则关闭数据库连接
}

**(2)登录成功时,如果用户勾选了“记住密码”,就将手机号码及其密码保存至数据库。**也就是在loginSuccess方法中增加如下代码:

// 如果勾选了“记住密码”,则把手机号码和密码保存为数据库的用户表记录
if (isRemember) {
UserInfo info = new UserInfo(); // 创建一个用户信息对象
info.phone = et_phone.getText().toString();
info.password = et_password.getText().toString();
info.update_time = DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss");
mHelper.insert(info); // 往用户数据库添加登录成功的用户信息
}

(3)再次打开登录页面,用户输入手机号再点击密码框的时候,App根据手机号到数据库查找登录信息,并将记录结果中的密码填入密码框。其中根据手机号码查找登录信息,要求在帮助器代码中添加以下方法,用于找到指定手机的登录密码:

// 根据手机号码查询指定记录
public UserInfo queryByPhone(String phone) {
UserInfo info = null;
List<UserInfo> infoList = query(String.format("phone='%s'", phone));
if (infoList.size() > 0) { // 存在该号码的登录信息
info = infoList.get(0);
}
return info;
}

此外,上面第3点的点击密码框触发查询操作,用到了编辑框的焦点变更事件,有关焦点变更监听器的详细用法参见第5章的“5.3.2 焦点变更监听器”。就本案例而言,光标切到密码框触发焦点变更事件,具体处理逻辑要求重写监听器的onFocusChange方法,重写后的方法代码如下所示:

@Override
public void onFocusChange(View v, boolean hasFocus) {
String phone = et_phone.getText().toString();
// 判断是否是密码编辑框发生焦点变化
if (v.getId() == R.id.et_password) {
// 用户已输入手机号码,且密码框获得焦点
if (phone.length() > 0 && hasFocus) {
// 根据手机号码到数据库中查询用户记录
UserInfo info = mHelper.queryByPhone(phone);
if (info != null) {
// 找到用户记录,则自动在密码框中填写该用户的密码
et_password.setText(info.password);
}
}
}
}

重新运行测试App,先打开登录页面,勾选“记住密码”,并确保本次登录成功。然后再次进入登录页面,输入手机号码后光标还停留在手机框,如图6-11所示。接着点击密码框,光标随之跳到密码框,此时密码框自动填入了该号码对应的密码串,如图6-12所示。由效果图可见,这次实现了真正意义上的记住密码功能。
在这里插入图片描述

四、存储卡的文件操作

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

1.私有存储空间与公共存储空间

为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。

<!-- 存储卡读写 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />

但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了,如图6-13所示。
在这里插入图片描述
当然图示的禁止访问只是不让访问存储卡的公共空间,App自身的私有空间依旧可以正常读写。这缘于Android把存储卡分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的专享空间。虽然Android给每个应用都分配了单独的安装目录,但是安装目录的空间很紧张,所以Android在存储卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用来保存应用自己需要处理的临时文件。这个目录只有当前应用才能够读写文件,其他应用是不允许读写的。由于私有空间本身已经加了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录自然不成问题。因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被卸载,那么对应的目录也会被删掉。

既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子

// 获取系统的公共存储路径
String publicPath =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).t
oString();
// 获取当前App的私有存储路径
String privatePath =
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
boolean isLegacy = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android10的存储空间默认采取分区方式,此处判断是传统方式还是分区方式
isLegacy = Environment.isExternalStorageLegacy();
}
String desc = "系统的公共存储路径位于" + publicPath +
"\n\n当前App的私有存储路径位于" + privatePath +
"\n\nAndroid7.0之后默认禁止访问公共存储目录" +
"\n\n当前App的存储空间采取" + (isLegacy?"传统方式":"分区方式");
tv_path.setText(desc);

该例子运行之后获得的路径信息如图6-14所示,可见应用的私有空间路径位于“存储卡根目录/Android/data/应用包名/files/Download”这个目录中。
在这里插入图片描述

2.在存储卡上读写文本文件

文本文件的读写借助于文件IO流FileOutputStream和FileInputStream。其中,FileOutputStream用于写文件,FileInputStream用于读文件,它们读写文件的代码例子如下:

// 把字符串保存到指定路径的文本文件
public static void saveText(String path, String txt) {
// 根据指定的文件路径构建文件输出流对象
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(txt.getBytes()); // 把字符串写入文件输出流
} catch (Exception e) {
e.printStackTrace();
}
}
// 从指定路径的文本文件中读取内容字符串
public static String openText(String path) {
String readStr = "";
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
byte[] b = new byte[fis.available()];
fis.read(b); // 从文件输入流读取字节数组
readStr = new String(b); // 把字节数组转换为字符串
} catch (Exception e) {
e.printStackTrace();
}
return readStr; // 返回文本文件中的文本字符串
}

接着分别创建写文件页面和读文件页面,其中写文件页面调用saveText方法保存文本,完整代码见chapter06\src\main\java\com\example\chapter06\FileWriteActivity.java;而读文件页面调用readText方法从指定路径的文件中读取文本内容,完整代码见
chapter06\src\main\java\com\example\chapter06\FileReadActivity.java。

然后运行测试App,先打开文本写入页面,录入注册信息后保存为私有目录里的文本文件,此时写入界面如图6-15所示。再打开文本读取页面,App自动在私有目录下找到文本文件列表,并展示其中一个文件的文本内容,此时读取界面如图6-16所示。
在这里插入图片描述

3.在存储卡上读写图片文件

文本文件读写可以转换为对字符串的读写,而图片文件保存的是图像数据,需要专门的位图工具Bitmap处理。位图对象依据来源不同又分成3种获取方式,分别对应位图工厂BitmapFactory的下列3种方法:
decodeResource:从指定的资源文件中获取位图数据。例如下面代码表示从资源文件huawei.png
获取位图对象:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.huawei);

decodeFile:从指定路径的文件中获取位图数据。注意从Android 10开始,该方法只适用于私有目录下的图片,不适用公共空间下的图片。
decodeStream:从指定的输入流中获取位图数据。比如使用IO流打开图片文件,此时文件输入流对象即可作为decodeStream方法的入参,相应的图片读取代码如下:

// 从指定路径的图片文件中读取位图数据
public static Bitmap openImage(String path) {
Bitmap bitmap = null; // 声明一个位图对象
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
} catch (Exception e) {
e.printStackTrace();
}
return bitmap; // 返回图片文件中的位图数据
}

得到位图对象之后,就能在图像视图上显示位图。图像视图ImageView提供了下列方法显示各种来源的图片:
setImageResource:设置图像视图的图片资源,该方法的入参为资源图片的编号,形如
“R.drawable.去掉扩展名的图片名称”。
setImageBitmap:设置图像视图的位图对象,该方法的入参为Bitmap类型。
setImageURI:设置图像视图的路径对象,该方法的入参为Uri类型。字符串格式的文件路径可通过代码“Uri.parse(file_path)”转换成路径对象。

读取图片文件的花样倒是挺多,把位图数据写入图片文件却只有一种,即通过位图对象的compress方法将位图数据压缩到文件输出流。具体的图片写入代码如下所示:

// 把位图数据保存到指定路径的图片文件
public static void saveImage(String path, Bitmap bitmap) {
// 根据指定的文件路径构建文件输出流对象
try (FileOutputStream fos = new FileOutputStream(path)) {
// 把位图数据压缩到文件输出流中
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
} catch (Exception e) {
e.printStackTrace();
}
}

接下来完整演示一遍图片文件的读写操作,首先创建图片写入页面,从某个资源图片读取位图数据,再把位图数据保存为私有目录的图片文件,相关代码示例如下:

// 获取当前App的私有下载目录
String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() +
"/";
// 从指定的资源文件中获取位图对象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.huawei);
String file_path = path + DateUtil.getNowDateTime("") + ".jpeg";
FileUtil.saveImage(file_path, bitmap); // 把位图对象保存为图片文件
tv_path.setText("图片文件的保存路径为:\n" + file_path);

然后创建图片读取页面,从私有目录找到图片文件,并挑出一张在图像视图上显示,相关代码示例如下:

// 获取当前App的私有下载目录
mPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/";
// 获得指定目录下面的所有图片文件
mFilelist = FileUtil.getFileList(mPath, new String[]{".jpeg"});
if (mFilelist.size() > 0) {
// 打开并显示选中的图片文件内容
String file_path = mFilelist.get(0).getAbsolutePath();
tv_content.setText("找到最新的图片文件,路径为"+file_path);
// 显示存储卡图片文件的第一种方式:直接调用setImageURI方法
//iv_content.setImageURI(Uri.parse(file_path)); // 设置图像视图的路径对象
// 第二种方式:先调用BitmapFactory.decodeFile获得位图,再调用setImageBitmap方法
//Bitmap bitmap = BitmapFactory.decodeFile(file_path);
//iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象
// 第三种方式:先调用FileUtil.openImage获得位图,再调用setImageBitmap方法
Bitmap bitmap = FileUtil.openImage(file_path);
iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象
}

运行测试App,先打开图片写入页面,点击保存按钮把资源图片保存到存储卡,此时写入界面如图6-17所示。再打开图片读取页面,App自动在私有目录下找到图片文件列表,并展示其中一张图片,此时读取界面如图6-18所示。
在这里插入图片描述

五、应用组件Application

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

1.Application的生命周期

Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿应用的整个生命周期。打开AndroidManifest.xml,发现activity节点的上级正是application节点,不过该节点并未指定name属性,此时App采用默认的Application实例。

注意到每个activity节点都指定了name属性,譬如常见的name属性值为.MainActivity,让人知晓该activity的入口代码是MainActivity.java。现在尝试给application节点加上name属性,看看其庐山真面目,具体步骤说明如下:
(1)打开AndroidManifest.xml,给application节点加上name属性,表示application的入口代码是MainApplication.java。修改后的application节点示例如下:

<application
android:name=".MainApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

(2)在Java代码的包名目录下创建MainApplication.java,要求该类继承Application,继承之后可供重写的方法主要有以下3个。
onCreate:在App启动时调用。
onTerminate:在App终止时调用(按字面意思)。
onConfigurationChanged:在配置改变时调用,例如从竖屏变为横屏。

光看字面意思的话,与生命周期有关的方法是onCreate和onTerminate,那么重写这两个方法,并在重写后的方法中打印日志,修改后的Java代码如下所示:

public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
}
@Override
public void onTerminate() {
super.onTerminate();
Log.d(TAG, "onTerminate");
}
}

(3)运行测试App,在logcat窗口观察应用日志。但是只在启动一开始看到MainApplication的onCreate日志(该日志先于MainActivity的onCreate日志),却始终无法看到它的onTerminate日志,无论是自行退出App还是强行杀掉App,日志都不会打印onTerminate。

无论你怎么折腾,这个onTerminate日志都不会出来。Android明明提供了这个方法,同时提供了关于该方法的解释,说明文字如下:This method is for use in emulated process environments.It will never be called on a production Android device, where processes are removed by simply killing them; no user code (including this callback) is executed when doing so。这段话的意思是:该方法供模拟环境使用,它在真机上永远不会被调用,无论是直接杀进程还是代码退出;执行该操作时,不会执行任何用户代码。

现在很明确了,onTerminate方法就是个摆设,中看不中用。如果读者想在App退出前回收系统资源,就不能指望onTerminate方法的回调了。

2.利用Application操作全局变量

C/C++有全局变量的概念,因为全局变量保存在内存中,所以操作全局变量就是操作内存,显然内存的读写速度远比读写数据库或读写文件快得多。所谓全局,指的是其他代码都可以引用该变量,因此全局变量是共享数据和消息传递的好帮手。不过Java没有全局变量的概念,与之比较接近的是类里面的静态成员变量,该变量不但能被外部直接引用,而且它在不同地方引用的值是一样的(前提是在引用期间不能改动变量值),所以借助静态成员变量也能实现类似全局变量的功能。

根据上一小节的介绍可知,Application的生命周期覆盖了App运行的全过程。不像短暂的Activity生命周期,一旦退出该页面,Activity实例就被销毁。因此,利用Application的全生命特性,能够在Application实例中保存全局变量。
适合在Application中保存的全局变量主要有下面3类数据:
(1)会频繁读取的信息,例如用户名、手机号码等。
(2)不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。
(3)容易因频繁分配内存而导致内存泄漏的对象,例如Handler处理器实例等。

要想通过Application实现全局内存的读写,得完成以下3项工作:
(1)编写一个继承自Application的新类MainApplication。该类采用单例模式,内部先声明自身类的一个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该对象的获取方法getInstance。
具体实现代码示例如下:

public class MainApplication extends Application {
private final static String TAG = "MainApplication";
private static MainApplication mApp; // 声明一个当前应用的静态实例
// 声明一个公共的信息映射对象,可当作全局变量使用
public HashMap<String, String> infoMap = new HashMap<String, String>();
// 利用单例模式获取当前应用的唯一实例
public static MainApplication getInstance() {
return mApp;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mApp = this; // 在打开应用时对静态的应用实例赋值
}
}

(2)在活动页面代码中调用MainApplication的getInstance方法,获得它的一个静态对象,再通过该对象访问MainApplication的公共变量和公共方法。

(3)不要忘了在AndroidManifest.xml中注册新定义的Application类名,也就是给application节点增加android:name属性,其值为.MainApplication。
接下来演示如何读写内存中的全局变量,首先分别创建写内存页面和读内存页面,其中写内存页面把用户的注册信息保存到全局变量infoMap,完整代码见
chapter06\src\main\java\com\example\chapter06\AppWriteActivity.java;而读内存页面从全局变量infoMap读取用户的注册信息,完整代码见
chapter06\src\main\java\com\example\chapter06\AppReadActivity.java。

然后运行测试App,先打开内存写入页面,录入注册信息后保存至全局变量,此时写入界面如图6-19所示。再打开内存读取页面,App自动从全局变量获取注册信息,并展示拼接后的信息文本,此时读取界面如图6-20所示。
在这里插入图片描述

3.利用Room简化数据库操作

虽然Android提供了数据库帮助器,但是开发者在进行数据库编程时仍有诸多不便,比如每次增加一张新表,开发者都得手工实现以下代码逻辑:
(1)重写数据库帮助器的onCreate方法,添加该表的建表语句。
(2)在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段。
(3)在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例。
(4)每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接。

上述的处理操作无疑存在不少重复劳动,数年来引得开发者叫苦连连。为此各类数据库处理框架纷纷涌现,包括GreenDao、OrmLite、Realm等,可谓百花齐放。眼见SQLite渐渐乏人问津,谷歌公司干脆整了个自己的数据库框架—Room,该框架同样基于SQLite,但它通过注解技术极大地简化了数据库操作,减少了原来相当一部分编码工作量。

由于Room并未集成到SDK中,而是作为第三方框架提供,因此要修改模块的build.gradle文件,往dependencies节点添加下面两行配置,表示导入指定版本的Room库:

implementation 'androidx.room:room-runtime:2.2.5'
annotationProcessor 'androidx.room:room-compiler:2.2.5'

导入Room库之后,还要编写若干对应的代码文件。以录入图书信息为例,此时要对图书信息表进行增删改查,则具体的编码过程分为下列5个步骤:

1.编写图书信息表对应的实体类

假设图书信息类名为BookInfo,且它的各属性与图书信息表的各字段一一对应,那么要给该类添加“@Entity”注解,表示该类是Room专用的数据类型,对应的表名称也叫BookInfo。如果BookInfo表的name字段是该表的主键,则需给BookInfo类的name属性添加“@PrimaryKey”与“@NonNull”两个注解,表示该字段是个非空的主键。下面是BookInfo类的定义代码例子:

//书籍信息
@Entity
public class BookInfo {
@PrimaryKey // 该字段是主键,不能重复
@NonNull // 主键必须是非空字段
private String name; // 书籍名称
private String author; // 作者
private String press; // 出版社
private double price; // 价格
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAuthor(String author) {
this.author = author;
}
public String getAuthor() {
return this.author;
}
public void setPress(String press) {
this.press = press;
}
public String getPress() {
return this.press;
}
public void setPrice(double price) {
this.price = price;
}
public double getPrice() {
return this.price;
}
}

2.编写图书信息表对应的持久化类

所谓持久化,指的是将数据保存到磁盘而非内存,其实等同于增删改等SQL语句。假设图书信息表的持久化类名叫作BookDao,那么该类必须添加“@Dao”注解,内部的记录查询方法必须添加“@Query”注解,记录插入方法必须添加“@Insert”注解,记录更新方法必须添加“@Update”注解,记录删除方法必须添加“@Delete”注解(带条件的删除方法除外)。对于记录查询方法,允许在@Query之后补充具体的查询语句以及查询条件;对于记录插入方法与记录更新方法,需明确出现重复记录时要采取哪种处理策略。下面是BookDao类的定义代码例子:

@Dao
public interface BookDao {
@Query("SELECT * FROM BookInfo") // 设置查询语句
List<BookInfo> queryAllBook(); // 加载所有书籍信息
@Query("SELECT * FROM BookInfo WHERE name = :name") // 设置带条件的查询语句
BookInfo queryBookByName(String name); // 根据名字加载书籍
@Insert(onConflict = OnConflictStrategy.REPLACE) // 记录重复时替换原记录
void insertOneBook(BookInfo book); // 插入一条书籍信息
@Insert
void insertBookList(List<BookInfo> bookList); // 插入多条书籍信息
@Update(onConflict = OnConflictStrategy.REPLACE)// 出现重复记录时替换原记录
int updateBook(BookInfo book); // 更新书籍信息
@Delete
void deleteBook(BookInfo book); // 删除书籍信息
@Query("DELETE FROM BookInfo WHERE 1=1") // 设置删除语句
void deleteAllBook(); // 删除所有书籍信息
}

3.编写图书信息表对应的数据库类

因为先有数据库然后才有表,所以图书信息表还得放到某个数据库里,这个默认的图书数据库要从RoomDatabase派生而来,并添加“@Database”注解。下面是数据库类BookDatabase的定义代码例子:

//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保
存路径
@Database(entities = {BookInfo.class},version = 1, exportSchema = false)
public abstract class BookDatabase extends RoomDatabase {
// 获取该数据库中某张表的持久化对象
public abstract BookDao bookDao();
}

4.在自定义的Application类中声明图书数据库的唯一实例

为了避免重复打开数据库造成的内存泄漏问题,每个数据库在App运行过程中理应只有一个实例,此时要求开发者自定义新的Application类,在该类中声明并获取图书数据库的实例,并将自定义的Application类设为单例模式,保证App运行之时有且仅有一个应用实例。下面是自定义Application类的
代码例子:

public class MainApplication extends Application {
private final static String TAG = "MainApplication";
private static MainApplication mApp; // 声明一个当前应用的静态实例
// 声明一个公共的信息映射对象,可当作全局变量使用
public HashMap<String, String> infoMap = new HashMap<String, String>();
private BookDatabase bookDatabase; // 声明一个书籍数据库对象
// 利用单例模式获取当前应用的唯一实例
public static MainApplication getInstance() {
return mApp;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mApp = this; // 在打开应用时对静态的应用实例赋值
// 构建书籍数据库的实例
bookDatabase = Room.databaseBuilder(mApp, BookDatabase.class,"BookInfo")
.addMigrations() // 允许迁移数据库(发生数据库变更时,Room默认删除原数据
库再创建新数据库。如此一来原来的记录会丢失,故而要改为迁移方式以便保存原有记录)
.allowMainThreadQueries() // 允许在主线程中操作数据库(Room默认不能在主
线程中操作数据库)
.build();
}
// 获取书籍数据库的实例
public BookDatabase getBookDB(){
return bookDatabase;
}
}

5.在操作图书信息表的地方获取数据表的持久化对象

持久化对象的获取代码很简单,只需下面一行代码就够了:

// 从App实例中获取唯一的图书持久化对象
BookDao bookDao = MainApplication.getInstance().getBookDB().bookDao();

完成以上5个编码步骤之后,接着调用持久化对象的queryXXX、insertXXX、updateXXX、deleteXXX等方法,就能实现图书信息的增删改查操作了。例程的图书信息演示页面有两个,分别是记录保存页面和记录读取页面,其中记录保存页面通过insertOneBook方法向数据库添加图书信息,完整代码见
chapter06\src\main\java\com\example\chapter06\RoomWriteActivity.java;而记录读取页面通过queryAllBook方法从数据库读取图书信息,完整代码见
chapter06\src\main\java\com\example\chapter06\RoomReadActivity.java。

运行测试App,先打开记录保存页面,依次录入两本图书信息并保存至数据库,如图6-21和图6-22所示。再打开记录读取页面,从数据库读取图书信息并展示在页面上,如图6-23所示。
在这里插入图片描述
在这里插入图片描述

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值