封面画师:ke-ta     封面ID:56866134

数据存储技术

SharedPreference 存储

SharedPreference 是 Android 提供的,用来以最简单的方式对数据进行永久保存的方法。

SharedPreference存储的文件格式及路径:

文件格式:XML 文件

存储路径:data > data > <包名> > shared_prefs > 文件

使用SharedPreference存储数据的步骤:

  1. 获取 SharedPreferences 对象,使用 getSharedPreferences()getPreferences()

getSharedPreferences(String name, int mode):name 表示 SharedPreferences 文件名。mode 表示指定的访问权限,它有两个常用属性值,MODE_PRIVATE 被本应用读写,MODE_MULTI_PROCESS 跨应用读写。

getPreferences(int mode):mode 表示指定的访问权限,它有两个常用属性值,MODE_PRIVATE 被本应用读写,MODE_MULTI_PROCESS 跨应用读写。

  1. 获得 SharedPreferences.Editor 对象,使用 edit()方法。
  2. 向 SharedPreferences.Editor 对象中添加数据。可以使用格式为 put + 数据类型的方法来添加。比如:putBoolean()putString() putInt()
  3. 提交数据,使用 commit() 实现。

使用SharedPreference读取数据的步骤:

  1. 获取 SharedPreferences 对象,使用 getSharedPreferences()getPreferences()
  2. 使用 SharedPreferences 类提供的 getXxx()方法获取数据。可以使用格式为 get+ 数据类型的方法来获取。比如:getBoolean()getString() getInt()

模拟 QQ 自动登录

编写布局资源文件 activity_main.xml 实现登录主界面布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<LinearLayout ...
    android:orientation="vertical">
    <EditText
        android:id="@+id/username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="QQ号/手机号/邮箱"
        android:layout_marginTop="150dp"/>
    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="密码"
        android:layout_marginTop="20dp"
        android:inputType="textPassword"/>
    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="登录"
        android:textSize="18sp"
        android:background="#13A1DA"/>
</LinearLayout>

创建新布局资源文件activity_message.xml,模拟登录成功界面:

1
2
3
4
5
6
7
<RelativeLayout ...>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="模拟登录成功界面"
        android:layout_centerInParent="true"/>
</RelativeLayout>

编写MainActivity.java实现主要逻辑:

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
public class MainActivity extends AppCompatActivity {
    private String user = "admin", pwd = "12345";    //模拟后台账号密码
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final EditText usernameET = findViewById(R.id.username);
        final EditText passwordET = findViewById(R.id.password);
        Button login = findViewById(R.id.login);
        //获取SharedPreferences对象
        final SharedPreferences sp = getSharedPreferences("mypassword",MODE_PRIVATE);
        /*******************实现自动登录*******************/
        String username = sp.getString("username",null);//获取账号信息
        String password = sp.getString("password",null);//获取密码
        if (username != null && password != null){
            if (username.equals(user) && password.equals(pwd) ){
                Intent intent = new Intent(MainActivity.this,MessageActivity.class);
                startActivity(intent);
            }
        }else {
            /************实现手动登录并储存账号和密码**************/
            login.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //获取输入的账号和密码
                    String in_username = usernameET.getText().toString();
                    String in_password = passwordET.getText().toString();
                    SharedPreferences.Editor editor = sp.edit();//获取Editor对象
                    if (in_username.equals(user) && in_password.equals(pwd)){
                        //保存账号和密码
                        editor.putString("username",in_username);
                        editor.putString("password",in_password);
                        editor.commit();
                        Intent intent = new Intent(MainActivity.this,MessageActivity.class);
                        startActivity(intent);
                        Toast.makeText(MainActivity.this, "以保存账号和密码",
                                       Toast.LENGTH_SHORT).show();
                    }else {
                        Toast.makeText(MainActivity.this, "账号或密码错误",
                                       Toast.LENGTH_SHORT).show();
                    }
                }
            });
        }

    }
}

测试后,我们可以在模拟器 data—data—<包名>— shared_prefs 下找到保存了账号和密码名为 mypassword.xml 的文件。

内部存储

Android 中 文件存储:通过 Java 的 IO 流 来读取磁盘上的文件。

获取输入流:openFileInput()

获取输出流:openFileOutput()

智能手机文件的存储方式:分为内部存储和外部存储。

思考:内部存储就是和电脑自带的硬盘一样,是固定在手机里的,而外部存储就像 U 盘/移动硬盘。是可以移动的? 事实上并不是那样的

无论是内部存储还是外部存储都是在手机内部的,这与我们是否插入了可以移动的 SD 卡 无关

内部存储 不是 内存!

内部存储的文件路径:

data —— data —— <包名> —— file —— 内部存储文件。

内部存储的特点

  1. 默认只能被创建它的应用访问到;
  2. 当这个应用被卸载后,内部存储中的文件也会被删除;
  3. 一旦内部存储空间耗尽,手机也会无法使用。

使用 内部存储 实现一个简易备忘录

编写布局资源文件activity_main.xml对界面进行布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<RelativeLayout ...>
    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="50dp"
        android:gravity="top"/>
    <Button
        android:id="@+id/btn_save"
        android:layout_width="60dp"
        android:layout_height="40dp"
        android:layout_alignParentRight="true"
        android:text="保存"
        android:textSize="18sp"/>
    <Button
        android:id="@+id/btn_cancel"
        android:layout_width="60dp"
        android:layout_height="40dp"
        android:layout_toLeftOf="@+id/btn_save"
        android:text="取消"
        android:textSize="18sp"/>
</RelativeLayout>

编写 MainActivity.java 实现简易备忘录逻辑:

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
public class MainActivity extends AppCompatActivity {
    
    byte[] buffer = null;//定义保存数据的数组
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final EditText editText = findViewById(R.id.editText);
        Button save = findViewById(R.id.btn_save);
        Button cancel = findViewById(R.id.btn_cancel);

        save.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /*************保存填写的备忘信息***********/
                FileOutputStream fileOutputStream = null; //声明文件输出流
                String text = editText.getText().toString();
                try {
                    //获得文件输出流对象
                    fileOutputStream = openFileOutput("memo", MODE_PRIVATE);
                    fileOutputStream.write(text.getBytes());
                    fileOutputStream.flush();   //清除缓存
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (fileOutputStream != null) {
                        try {
                            fileOutputStream.close();//关闭输出流
                            Toast.makeText(MainActivity.this, "保存成功",
                                           Toast.LENGTH_SHORT).show();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        /****************读取保存的备忘信息***************/
        FileInputStream fileInputStream = null;//声明文件输入流对象
        try {
            fileInputStream = openFileInput("memo");//获得文件输入流对象
            buffer = new byte[fileInputStream.available()]; //实例化字节数组
            fileInputStream.read(buffer);//从输入流中读取数据
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();//关闭输入流
                    String data = new String(buffer);//把字节数组的数据转换成字符串
                    editText.setText(data); //显示读取内容
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish(); //退出当前应用
            }
        });
    }
}

外部存储

外部存储 不是 可移动的 SD 卡! 外部存储也是存储在手机当中的。

区分内部存储和外部存储:用数据线将手机连接至电脑,能被电脑识别的部分就是外部存储。

读、写外部存储空间上的文件步骤:

  1. 获取外部存储目录:使用 Environment.getExternalStorageDirectory()
  2. 读、写外部存储空间中的文件:读 FileInputStream()、写 FileOutputStream()

使用 外部存储 实现一个简易备忘录

在本案例中使用于 内部存储 相同的布局资源文件 activity_main.xml,对 MainActivity.java 进行修改,并对修改部分进行了标注:

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
public class MainActivity extends AppCompatActivity {
    byte[] buffer = null;//定义保存数据的数组
    /***修改部分***/
    File file;  //声明文件对象,用来指定外部存储文件
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final EditText editText = findViewById(R.id.editText);
        Button save = findViewById(R.id.btn_save);
        Button cancel = findViewById(R.id.btn_cancel);
        /***修改部分***/
        file = new File(Environment.getExternalStorageDirectory(), "Text.text");//实例化文件对象

        save.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /*************保存填写的备忘信息***********/
                FileOutputStream fileOutputStream = null; //声明文件输出流
                String text = editText.getText().toString();
                try {
                    /***修改部分***/
                    fileOutputStream = new FileOutputStream(file);
                    fileOutputStream.write(text.getBytes());
                    fileOutputStream.flush();   //清除缓存
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (fileOutputStream != null) {
                        try {
                            fileOutputStream.close();//关闭输出流
                            Toast.makeText(MainActivity.this, "保存成功",
                                    Toast.LENGTH_SHORT).show();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        /****************读取保存的备忘信息***************/
        FileInputStream fileInputStream = null;//声明文件输入流对象
        try {
            /***修改部分***/
            fileInputStream = new FileInputStream(file);//获得文件输入流对象
            buffer = new byte[fileInputStream.available()]; //实例化字节数组
            fileInputStream.read(buffer);//从输入流中读取数据
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();//关闭输入流
                    String data = new String(buffer);//把字节数组的数据转换成字符串
                    editText.setText(data); //显示读取内容
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish(); //退出当前应用
            }
        });
    }
}

在本次案例中我们实现的是将备忘录信息信息保存至 SD 卡上,因此我们需要 获取读写 SD 卡的权限

对 AndroidManifest.xml 进行修改:

1
2
3
4
5
6
7
8
9
10
<manifest ...>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application ...
        android:requestLegacyExternalStorage="true">
        <activity android:name=".MainActivity">
           ...
        </activity>
    </application>
</manifest>

同时我们还需要在模拟器内部授予本案例的应用读写储存空间的权限!

如果案例测试成功,我们可以在模拟器以下目录找到存放备忘录信息的文件:storage/emulated/0/Text.text

数据库存储

本节主要讲述 sqlite3 的使用。

SQLite 数据库的特点:占用资源少、运行效率高、可移植性强、安全可靠。

操作 SQLite 数据库的方式:使用 SDK 中 sqlite3 工具(sqlite3.exe)或者使用 Java 代码。

使用命令提示符操作SQLite

使用时,先确保你的模拟器已经打开!

启动、退出 SQLite

首先,我们需要以 管理员权限 打开电脑的命令提示符。

在 SDK 目录下有:SDK/platform-tools/,我们需要在命令提示符中进入这个目录下,然后输入:

1
adb shell

在输入这条命令之前,必须先将你的模拟器打开!当然,为了便于使用,建议将该目录(SDK/platform-tools/)添加至环境变量中的 Path 下。

然后输入:

1
2
su
chmod 777 data

然后打开 SQLite,输入:

1
sqlite3

除此之外,我们也可以前往 SDK 目录下的 platform-tools 目录,找到 sqlite3.exe,双击运行也可以打开 SQLite 数据库。

如果我们想退出 SQLite 的交互模式,我们可以输入:

1
.exit

然后,我们就退出 SQLite 的交互模式,返回至 shell 模式。

创建数据库目录

相当于在你的模拟器指定的目录下创建一个文件夹。

shell 模式 下,输入:

1
mkdir /data/data/com.yang/databese

com.yang 是我的包名,你可以根据你自己的包名进行编写。创建成功后,你可以在你模拟器中查看是否有 database 这个文件夹。

创建和打开数据库

我们在创建数据库目录后,我们这可在这个目录下创建一个数据库。在SQLite数据库当中,每一个数据库保存在一个单独的文件当中。

我们需要先进入到需要创建或打开数据库的目录下,依旧是在 shell 模式 下,输入:

1
cd /data/data/com.yang/database

进入对应目录后,我们就可以创建或打开数据库了:

1
sqlite3 db_yang

使用这条语句,可以打开一个名为 db_yang 的数据库,如果没有这个数据库,就创建这个数据库。

这时,我们也会发现 命令提示行已经进入到 SQLite 的交互模式

数据表操作

SQLite 的交互模式下 输入以下命令以创建一个名为 tb_user 的数据表:

1
create table tb_user (id integer primary key autoincrement,name text not null,pwd text);

如果我们想查看当前数据库包含哪些数据表,我们可以使用:

1
.tables

如果我们想向数据库插入数据:

1
insert into tb_user values (null,'yang','123');

查询某个表中的数据:

1
select * from tb_user;

使用代码操作SQLite

相关操作介绍:

创建数据库:openOrCreateDatabase() 或者 继承 SQLiteOpenHelper 类

操作数据库:insert() 插入 delete() 删除 update() 更新 query() 查询

使用 SQLite 实现中英文词典数据库

准备:我们需要准备一张图片按钮所用的图片资源文件,命名为 translation,放置于 res/drawable 目录下。

编写布局资源文件activity_main.xml实现主界面布局:

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
<RelativeLayout ...>
    <ImageButton
        android:id="@+id/search_translate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/translation"
        android:layout_alignParentRight="true"/>
    <EditText
        android:id="@+id/search_et"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_below="@+id/search_translate"
        android:gravity="top"
        android:hint="请输入你要翻译的内容"/>
    <ListView
        android:id="@+id/result_listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/search_et" />
    <Button
        android:id="@+id/btn_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/result_listView"
        android:layout_alignParentRight="true"
        android:background="#0B86D6"
        android:text="添加单词"
        android:textSize="18sp"/>
</RelativeLayout>

编写布局资源文件activity_add.xml实现单词添加界面的布局:

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
<LinearLayout ...
    android:orientation="vertical">
    <EditText
        android:id="@+id/add_word"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="50dp"
        android:hint="单词"/>
    <EditText
        android:id="@+id/add_interpret"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="解释"
        android:paddingTop="20dp"/>
    <LinearLayout
        android:orientation="horizontal"
        android:gravity = "right|bottom"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/btn_save"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#0B86D6"
            android:text="保存"/>
        <Button
            android:id="@+id/btn_cancel"
            android:layout_marginStart="20dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#0B86D6"
            android:text="取消"/>
    </LinearLayout>
</LinearLayout>

编写布局资源文件result_main.xml用于显示查询结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
<LinearLayout ...
    android:orientation="vertical">
    <TextView
        android:id="@+id/result_word"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="单词:"/>
    <TextView
        android:id="@+id/result_interpret"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="解释:"/>
</LinearLayout>

创建DBOpenHelper.java用于创建数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DBOpenHelper extends SQLiteOpenHelper {
    //定义创建数据表的SQL语句
    final String CREATE_TABLE_SQL = "create table tb_dict (_id integer primary key autoincrement,word,detail)";
    public DBOpenHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_SQL);   //创建单词数据表
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.i("词典","--版本更新"+oldVersion+"-->"+newVersion);
    }
}

编写MainActivity.java用于实现主界面逻辑:

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
public class MainActivity extends AppCompatActivity {
    private DBOpenHelper dbOpenHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //实例化DBOpenHelper对象,用来创建数据库
        dbOpenHelper = new DBOpenHelper(MainActivity.this, "db_dict", null, 1);

        final ListView listView = findViewById(R.id.result_listView);
        final EditText etSearch = findViewById(R.id.search_et);
        ImageButton btn_search = findViewById(R.id.search_translate);
        Button btn_add = findViewById(R.id.btn_add);

        btn_add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, AddActivity.class);
                startActivity(intent);
            }
        });

        btn_search.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String key = etSearch.getText().toString();//获取要查询的单词
                Cursor cursor = dbOpenHelper.getReadableDatabase().query("tb_dict", null,
                        "word=?", new String[]{key},
                        null, null, null);
                ArrayList<Map<String ,String >> resultList = 
                    new ArrayList<Map<String ,String >>();
                while (cursor.moveToNext()){
                    Map<String ,String > map = new HashMap<String, String>();
                    map.put("word",cursor.getString(1));
                    map.put("interpret",cursor.getString(2));
                    resultList.add(map);
                }
                if (resultList == null || resultList.size() == 0){
                    Toast.makeText(MainActivity.this, "很抱歉,没有先关记录",
                                   Toast.LENGTH_SHORT).show();
                }else {
                    SimpleAdapter simpleAdapter = 
                        new SimpleAdapter(MainActivity.this,resultList,
                            R.layout.result_mian,
                            new String[]{"word","interpret"},new int[]{
                                    R.id.result_word,R.id.result_interpret
                    });
                    listView.setAdapter(simpleAdapter);
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (dbOpenHelper!=null){
            dbOpenHelper.close();//关闭数据库连接
        }
    }
}

编写AddActivity.java用于实现单词添加界面逻辑:

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
public class AddActivity extends AppCompatActivity {
    private DBOpenHelper dbOpenHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add);
        //实例化DBOpenHelper对象,用来创建数据库
        dbOpenHelper = new DBOpenHelper(AddActivity.this, "db_dict", null, 1);

        final EditText etWord= findViewById(R.id.add_word);
        final EditText etInterpret = findViewById(R.id.add_interpret);
        Button btn_save = findViewById(R.id.btn_save);
        Button btn_cancel = findViewById(R.id.btn_cancel);

        btn_save.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String word = etWord.getText().toString();
                String interpret = etInterpret.getText().toString();
                if (word.equals("") || interpret.equals("")){
                    Toast.makeText(AddActivity.this, "填写的单词或解释为空",
                                   Toast.LENGTH_SHORT).show();
                }else {
                    //插入生词
                    insertData(dbOpenHelper.getReadableDatabase(),word,interpret);
                    Toast.makeText(AddActivity.this, "添加生词成功", Toast.LENGTH_SHORT).show();
                }
            }
        });

        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(AddActivity.this,MainActivity.class);
                startActivity(intent);
            }
        });
    }
    //插入数据
    private void insertData(SQLiteDatabase sqLiteDatabase,String word,String interpret){
        ContentValues values = new ContentValues();
        values.put("word",word);
        values.put("detail",interpret);
        sqLiteDatabase.insert("tb_dict",null,values);//执行插入操作
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (dbOpenHelper!=null){
            dbOpenHelper.close();//关闭数据库连接
        }
    }
}

编写完成后进行测试,如果测试成功,我们可以在模拟器中 data/data/<该项目包名>/databases 目录下看到名为 tb_dict 的数据库文件。

Content Provider

用于实现不同应用程序间数据共享,同时可以保证被访问数据的安全性。

Content Provider 使用基于 数据模型 的简单 表格 来提供其中的数据。

URI: 在电脑术语中,统一资源标识符(Uniform Resource Identifier,URI)是一个用于标识某一互联网资源名称的字符串。 该种标识允许用户对任何(包括本地和互联网)的资源通过特定的协议进行交互操作。URI 由包括确定语法和相关协议的方案所定义。Web 上可用的每种资源 HTML 文档、图像、视频片段、程序等由一个通用资源标识符(Uniform Resource Identifier, 简称"URI")进行定位。

URL:Uniform Resource Locator,统一资源定位符。URL 是 URI 的子集。

URL与URI的区别

创建和使用 Content Provider 的步骤

  1. 继承 ContentProvider 类
  2. 声明 ContentProvider:在 AndroidManifest.xml 中使用 <provider> 进行声明。
  3. 使用 ContentProvider:
方法 含义
ContentResolver.insert() 添加
ContentResolver.delete() 删除
ContentResolver.update() 更新
ContentResolver.query() 查询

通过 ContentProvider 读取所有联系人

编写布局资源文件activity_main.xml实现联系人列表界面布局:

1
2
3
4
5
6
7
8
<RelativeLayout ...>
    <TextView
        android:id="@+id/result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:layout_margin="15dp"/>
</RelativeLayout>

编写MainActivity.java用于实现主逻辑:

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
public class MainActivity extends Activity {
    //希望获得姓名
    private String columns = ContactsContract.Contacts.DISPLAY_NAME;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.result);
        //使TextView加粗 也可以在布局文件android:textStyle="bold"
        textView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
        textView.setText(getQueryData()); //显示获取的通讯录信息
    }

    private CharSequence getQueryData(){
        //同于保存获取的联系人
        StringBuilder stringBuilder = new StringBuilder();
        ContentResolver resolver = getContentResolver();
        //查询记录
        Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI,
                                       null,null,null,null);
        //获取姓名记录的索引值
        int displayNameIndex = cursor.getColumnIndex(columns);
        for (cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()){
            String displayName = cursor.getString(displayNameIndex);
            stringBuilder.append(displayName+"\n");
        }
        cursor.close();
        return stringBuilder.toString();    //返回查询结果
    }
}

由于需要获取模拟器中联系人信息,因此需要打开读取联系人信息的权限。

在 AndroidManifest.xml 中设置权限:

1
2
3
4
5
6
7
8
9
<manifest ...>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <application ...
        android:requestLegacyExternalStorage="true">
        <activity android:name=".MainActivity" >
            ...
        </activity>
    </application>
</manifest>

同时我们还需要在模拟器内部授予本案例的应用读取联系人信息的权限!

在进行测试时,模拟器内一般没有默认联系人,因此 需要我们添加几个联系人,不然测试时就只能看到纯白界面而没有任何内容。

Handler 消息处理

Handler 消息传递机制

进程: 一个 Android应用 就是一个 进程,每个应用在各自的进程中运行,互不干扰。

线程: 比线程更小的 独立运行 单位,一个进程可以包含 多个 进程。

Android 中的线程是不安全的,不能用子线程去更改主线程。

规则:子线程不允许操作主线程中的组件。

对规则进行测试

我们可以设计案例测试一下,点击界面「按钮」,使文本内容发生改变:

编写activity_main.xml实现界面布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<RelativeLayout ...
    android:paddingTop="16dp"
    android:paddingEnd="16dp"
    android:paddingStart="16dp"
    android:paddingBottom="16dp">
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="26sp"
        android:textColor="#0086cc"
        android:text="书籍是人类进步的阶梯"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="下一条"
        android:id="@+id/button"
        android:layout_below="@+id/tv"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="34dp"/>
</RelativeLayout>

编写MainActivity.java用于实现主逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TextView textView = findViewById(R.id.tv);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //创建一个线程
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("你今天的努力,是幸运的伏笔;当下的付出,是明日的花开");
                    }
                });
                thread.start();
            }
        });
    }
}

测试: 运行案例,界面成功显示,点击「下一条」,文本内容更换,但 程序停止运行并立即闪退。这也就验证了我们上述规则的正确性。

也正是因为这个原因,因此我们 需要使用Handler(android.os.Handler) 来实现我们的案例。

那么我们应该怎么修改才能达成我们想要的目的呢?

对 MainActivity.java 进行修改:

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
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TextView textView = findViewById(R.id.tv);
        Button button = findViewById(R.id.button);
        /******修改部分******/
        final Handler handler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                if (msg.what == 0x123){
                    textView.setText("你今天的努力,是幸运的伏笔;当下的付出,是明日的花开");
                }
            }
        };
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //创建一个线程
                Thread thread = new Thread(new Runnable() {
                    /******修改部分******/
                    @Override
                    public void run() {
                        //发送空消息,参数为消息代码,可自行定义
                        handler.sendEmptyMessage(0x123);
                        //textView.setText("你今天的努力,是幸运的伏笔;当下的付出,是明日的花开");
                    }
                });
                thread.start();
            }
        });
    }
}

总结

Handler 是 Android 中提供的一个 消息处理 的机制。

作用:

  • 在任意线程中发送消息
  • 在主线程中获取并处理消息

利用 Handler 实现倒计时进度条

在布局资源activity_main.xml中实现进度条原型:

1
2
3
4
5
6
7
8
9
<RelativeLayout ...>
    <ProgressBar
        android:id="@+id/timer"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:max="60"/>
</RelativeLayout>

编写MainActivity.java用于实现倒计时主逻辑:

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
public class MainActivity extends AppCompatActivity {
    private ProgressBar timer;
    final int TIME = 60;    //定义进度条总长度为60秒
    private int mProgressStatus = 0;    //定义完成的进度
    final int TIMER_MSG = 0x001; //消息代码
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        timer = findViewById(R.id.timer);
        //2. 启动进度条
        handler.sendEmptyMessage(TIMER_MSG);
    }
    //1. 创建Handler,实现1s更新一次进度
    Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (TIME-mProgressStatus > 0){  //当前进度大于0
                mProgressStatus++;
                timer.setProgress(TIME-mProgressStatus);
                //1秒后发送消息
                handler.sendEmptyMessageDelayed(TIMER_MSG,1000);
            }else {
                Toast.makeText(MainActivity.this, "还想+1s?不存在的!",
                               Toast.LENGTH_SHORT).show();
            }
        }
    };
}

Message

在 Android 中 Handler 并不是单独工作的,与它一起的还有 Looper(一个循环器,主要用于管理 MessageQueue)和 Message。

MessageQueue:一个消息队列,采用先进先出原则来管理消息,我们可以把它看成一个存储消息的管理器,被封装在 Looper 当中,所以我们并不会直接去操作它。

Message:线程之间传递的消息,可以携带少量的信息,是 Handler 接收和处理的对象。

一个线程对应一个 Looper 对象,一个 Looper 对象对应一个 MessageQueue,一个 MessageQueue 中可以存放多个 Message。

Message 可以通过它的属性来携带一些数据。

Message 的属性

arg1、arg2:存放整型数据。

obj:存放 Object 类型的任意对象

replayTo:指定 Message 发往何处

what:用户自定义的消息代码,采用 Int 类型表示,我们常用十六进制的整数

Message 的创建

创建 Message 对象我们不用 new Message()进行获取。

我们一般使用 Message.obtain() 方法或 Handler.obtainMessage() 方法,这两个方法可以避免分配新的对象,减少内存的开销。

实现轮播图片

准备:我们需要准备 4 张需要轮播的图片,分别命名为 image1~image4,然后把它们放置于 res/drawable 目录下。

编写轮播的补间动画:

slide_in_right:

1
2
3
4
5
6
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="100%p"
        android:toXDelta="0%p" />
</set>

slide_in_left:

1
2
3
4
5
6
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="0%p"
        android:toXDelta="-100%p" />
</set>

编写布局资源文件activity_main.xml设置ViewFlipper组件:

1
2
3
4
5
6
7
<RelativeLayout ...>
    <ViewFlipper
        android:id="@+id/viewFlipper"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"/>
</RelativeLayout>

编写MainActivity.java用于实现轮播主逻辑:

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
public class MainActivity extends AppCompatActivity {

    private ViewFlipper flipper;
    private int[] images = new int[]{R.drawable.image1,R.drawable.image2,R.drawable.image3,R.drawable.image4};
    private Animation[] animations = new Animation[2];
    final int FLAG_MSG = 0x001; //消息代码
    private Message message;//发送的消息对象
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /*************1. 通过ViewFlipper组件播放图片*******************/
        flipper = findViewById(R.id.viewFlipper);
        for (int index = 0; index < images.length; index++){
            ImageView imageView = new ImageView(this);
            imageView.setImageResource(images[index]);
            flipper.addView(imageView);
        }
        //初始化动画数组
        animations[0] = AnimationUtils.loadAnimation(this,R.anim.slide_in_right);
        animations[1] = AnimationUtils.loadAnimation(this,R.anim.slide_in_left);
        flipper.setInAnimation(animations[0]);
        flipper.setOutAnimation(animations[1]);
        /**********************************************************/
        /***********************3. 开启广告轮播************************/
        message = Message.obtain(); //获取Message对象
        message.what = FLAG_MSG;    //设置消息代码
        handler.sendMessage(message);   //发送消息
    }
    /*********************2. 创建Handler对象,实现3秒更新图片***************/
    Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (msg.what == FLAG_MSG){
                flipper.showPrevious(); //切换到下一张图片
                message = handler.obtainMessage(FLAG_MSG);//获取Message
                handler.sendMessageDelayed(message,3000);//延迟3秒发送消息
            }
        }
    };
}

注意:补间动画的持续时间与 Handler 延迟发送信息的时间不可太接近,否则会出现动画异常。

即:android:duration 的属性值与 Handler.sendMessageDelayed(Message msg, long delayMillis) 中的 delayMillis 不可太接近。建议参考本代码中给出的时间。

Looper

一个循环器,主要用于管理 MessageQueue,用来不断地从 MessageQueue 中获取 Message 返回给 Handler。

如果我们在主线程中创建 Handler,系统会自动创建 Looper 对象;如果我们在子线程中创建 Handler,需要手动创建 Looper 对象。

子线程中创建Looper对象的步骤:

  1. 初始化 Looper 对象:prepare()
  2. 创建 Handler 对象:new Handler()
  3. 启动 Looper:loop()

我们可以如下测试:

Looper 测试

编写布局资源文件activity_main.xml添加TextView组件:

1
2
3
4
5
6
<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="到Logcat面板查看日志"
        android:textSize="30sp"
        android:textStyle="bold"/>

创建LooperThread.java类用于实现在子线程中创建Handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LooperThread extends Thread {
    private Handler handler;//声明一个Handler对象
    @Override
    public void run() {
        super.run();
        handler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                Log.i("Looper",String.valueOf(msg.what));
            }
        };
        Message message = handler.obtainMessage();//获取Message
        message.what = 0x001;   //设置消息代码
        handler.sendMessage(message); //发送消息
    }
}

在MainActivity.java中创建并开启线程:

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LooperThread thread = new LooperThread();//创建一个线程
        thread.start(); //开启线程
    }
}

测试:运行案例,程序停止运行并立即闪退。这时候我们在 AS 中 Run 面板中可以看到运行时异常:

1
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()

这个异常告诉我们:无法在尚未调用Looper.prepare()的线程创建处理程序。这也验证了我们在「子线程中创建 Handler,需要手动创建 Looper 对象」的结论。

那么我们应该怎么做呢?

修改MainActivity.java(修改部分已经标出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LooperThread extends Thread {
    private Handler handler;//声明一个Handler对象
    @Override
    public void run() {
        super.run();
        /****修改部分*****/
        Looper.prepare();   //初始化Looper对象
        handler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                Log.i("Looper",String.valueOf(msg.what));
            }
        };
        Message message = handler.obtainMessage();//获取Message
        message.what = 0x001;   //设置消息代码
        handler.sendMessage(message); //发送消息
        /****修改部分*****/
        Looper.loop();//启动Looper
    }
}

然后我们可以前往 Logcat 面板搜索与 Looper 有关的日志,我们可以看到如下日志:

1
2020-05-03 21:06:15.438 27574-27609/com.yang.loopertest I/Looper: 1

其中,com.yang.loopertest 是我们当前项目的包,1(十进制)就是消息代码。

Service 应用

什么是 Service

定义:能够在 后台 长时间运行,并且 没有 用户界面的应用程序组件。

Activity 可以比作看皮影戏时看到的画面,但是那些画面怎么动起来的我们并不知道。而在幕布后面,我们知道是由艺人操作让画面动起来的,艺人就可以比作是 Service。Activity 是可以看到的,而 Service 是看不到的。

Service 的应用:后台下载文件、后台获取 GPS 定位信息、在后台播放音乐等。

Service 的分类

Started Service:调用 startService() 方法启动的 Service。通过这个方法,运行应用的时候 Service 并没有启动,而是当应用程序组件(比如:Activity)调用 startService() 通知服务启动,这时候 Service 才处于启动状态。一旦启动将无限期运行下去,除非停止它。

Bound Service:调用 bindService() 方法启动的 Service。通过这个方法,当应用运行的时候 Service 与 Activity 就绑定在一起了,一旦 Activity 停止,Service 也会随着停止。

Service 的基本用法

创建和配置 Service

在 Android Studio 中,提供了 创建与配置Service的向导

右击要创建 Service 的包 > New > Service,然后进行选择并创建 Service。有两种可以创建的 Service,一种是 IntentService,另一种是普通的 Service。选择 IntentService 可以创建继承自 IntentService 的 Service。

我们选择普通的 Service,然后我们可以看到有两个复选框,一个是 Exported,另一个是 Enabled,我们可以用鼠标轻触这两个复选框查看它们表示什么含义(一般默认都勾选)。

我们输入创建 Service 的名字后并完成创建,Android Studio 会自动在 AndroidManifest.xml 中帮我们配置 Service。

我们创建名为MyService的Service,并在其中进行代码编写:

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
public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {//在Service创建时调用
        Log.i("Service","Service已创建");
        super.onCreate();
    }

    //在每次启动Service时调用
    @Override
    public int onStartCommand(final Intent intent, int flags, int startId) {
        Log.i("Service","Service已启动");
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (isRunning()){    //如果Service正在运行
                    //输出变量i
                    Log.i("Service",String.valueOf(++i));
                    try {
                        Thread.sleep(1000); //线程休眠1秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {//在Service销毁时调用
        Log.i("Service","Service已停止");
        super.onDestroy();
    }

    //判断Service是否正在运行
    public boolean isRunning() {
        ActivityManager activityManager = 
            (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        //获取所有正在运行的Service
        ArrayList<ActivityManager.RunningServiceInfo> runningService =
            (ArrayList<ActivityManager.RunningServiceInfo>) 
            activityManager.getRunningServices(10);
       for (int i = 0;i<runningService.size();i++){
           if (runningService.get(i).service.getClassName().
               toString().equals("com.yang.useservice.MyService")){
               return true;
           }
       }
       return false;
    }
}

启动和停止 Service

启动 Service:startService();

停止 Service:Service 自身调用 stopSelf() 或者 其他组件调用 stopService()


编写布局资源文件activity_main.xml,设置控制Service的启动或停止的按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<RelativeLayout ...>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true">
        <Button
            android:id="@+id/startService"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="启动Service"/>
        <Button
            android:id="@+id/stopService"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="30dp"
            android:text="停止Service"/>
    </LinearLayout>
</RelativeLayout>

编写 MainActivity.java,实现控制 Service 启动或关闭的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button start = findViewById(R.id.startService);
        Button stop = findViewById(R.id.stopService);
        final Intent intent = new Intent(MainActivity.this,MyService.class);
        start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startService(intent);   //启动Service
            }
        });

        stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                stopService(intent);//停止Service
            }
        });
    }
}

测试: 启动程序、打开虚拟机,进入程序主入口界面,点击「启动 Service」按钮,我们可以前往 Logcat 面板搜索与 Service 有关的日志,我们可以看到如下日志:

1
2
3
4
5
6
7
8
2020-05-03 22:23:03.927 28465-28465/com.yang.useservice I/Service: Service已创建
2020-05-03 22:23:03.927 28465-28465/com.yang.useservice I/Service: Service已启动
2020-05-03 22:23:03.931 28465-28501/com.yang.useservice I/Service: 1
2020-05-03 22:23:05.281 28465-28501/com.yang.useservice I/Service: 2
2020-05-03 22:23:06.284 28465-28501/com.yang.useservice I/Service: 3
2020-05-03 22:23:07.287 28465-28501/com.yang.useservice I/Service: 4
2020-05-03 22:23:08.290 28465-28501/com.yang.useservice I/Service: 5
2020-05-03 22:23:09.292 28465-28501/com.yang.useservice I/Service: 6

并且随着时间每过 1 秒,末尾的数字会加 1,并生成新的日志。

当我们点击「停止 Service」按钮,会看到以下日志:

1
2020-05-03 22:23:10.727 28465-28465/com.yang.useservice I/Service: Service已停止

同时末尾的数字不会会加 1,更不会生成新的日志。

这也是 Service的生命周期

Started Service 的生命周期

Started Service 的生命周期

控制背景音乐的播放

准备:我们需要准备两张图片按钮资源,表示音乐播放和音乐暂停,分别命名为 music_play和music_stop,将它们放置于 res/drawable 目录下。然后我们还需要准备一段音频文件,命名为 music,在 res 目录下新建 raw 目录,将音频文件放置于这个目录下。

编写activity_main.xml布局资源文件:

1
2
3
4
5
6
7
8
9
10
<RelativeLayout ...>
    <ImageButton
        android:id="@+id/btn_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        android:src="@drawable/music_play" />
</RelativeLayout>

创建MusicService.java,实现音乐加载、播放:

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
public class MusicService extends Service {
    static boolean isPlay; //记录当前播放状态
    MediaPlayer player; //MediaPlayer对象
    public MusicService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        //实例化MediaPlayer对象,并加载播放的音频文件
        player = MediaPlayer.create(this,R.raw.music);
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (!player.isPlaying()){
            player.start(); //播放音乐
            isPlay = player.isPlaying();    //设置当前状态为正在播放音乐
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        player.stop();  //停止播放音乐
        isPlay = player.isPlaying();//设置当前状态为停止音乐
        player.release();//释放资源
        super.onDestroy();
    }
}

编写MainActivity.java实现音乐播放、暂停的逻辑:

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
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.tv);
        textView.setSelected(true);
        final Intent intent = new Intent(MainActivity.this,MusicService.class);
        ImageButton btn_play = findViewById(R.id.btn_play);
        btn_play.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               //启动和停止 MusicService
                if (MusicService.isPlay == false){
                    startService(intent); //启动Service
                    ((ImageButton)v).setImageDrawable(getResources().
                                                      getDrawable(R.drawable.music_play,null));
                }else {
                    stopService(intent);
                    ((ImageButton)v).setImageDrawable(getResources().
                                                      getDrawable(R.drawable.music_stop,null));
                }
            }
        });
    }

    @Override
    protected void onStart() {
        startService(new Intent(MainActivity.this,MusicService.class));//启动Service
        super.onStart();
    }
}

测试: 经过测试后,我们发现程序存在可以优化的地方。当我们进入应用后,音乐会开始播放,然后我们点击图片按钮,音乐播放暂停,但当我们在此点击图片按钮时,音乐会从头开始播放,这不是我们想要的,一次本案例存在可以进行优化的地方!

Bound Service

在 Started Service 中启动 Service 的组件与 Service 之间没有太大的联系,我们无法进行通信或交换数据;如果 Service 和启动它的组件需要进行方法调用或交换数据,我们就需要使用 Bound Service。

Bound Service 的生命周期:

BoundService 的生命周期

实现 Bound Service 的基本步骤:

image-20200504110950734

模拟双色球随机选号

由于显示号码的文本框组件是圆形,因此我们需要创建这样的一个资源文件。

在res/drawable目录下创建名为circle_text的资源文件:

1
2
3
4
5
6
7
8
9
10
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval"
    android:useLevel="false">
    <solid android:color="#EA0D0D" />
    <stroke android:width="0.5dip" android:color="#000000" />
    <size
        android:width="15dp"
        android:height="15dp" />
    <corners android:radius="180dp" />
</shape>

编写activity_main.xml布局资源文件实现主界面显示:

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
<RelativeLayout ...>
    <LinearLayout
        android:id="@+id/linearLayout_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_centerInParent="true">
        <TextView
            android:id="@+id/text_num1"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:textSize="18sp"
            android:layout_marginEnd="10dp"
            android:gravity="center"
            android:background="@drawable/circle_text"/>
        <TextView
            android:id="@+id/text_num2"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:textSize="18sp"
            android:layout_marginEnd="10dp"
            android:gravity="center"
            android:background="@drawable/circle_text"/>
        <TextView
            android:id="@+id/text_num3"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:textSize="18sp"
            android:layout_marginEnd="10dp"
            android:gravity="center"
            android:background="@drawable/circle_text"/>
        <TextView
            android:id="@+id/text_num4"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginEnd="10dp"
            android:textSize="18sp"
            android:gravity="center"
            android:background="@drawable/circle_text"/>
        <TextView
            android:id="@+id/text_num5"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:textSize="18sp"
            android:layout_marginEnd="10dp"
            android:gravity="center"
            android:background="@drawable/circle_text"/>
        <TextView
            android:id="@+id/text_num6"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginEnd="10dp"
            android:textSize="18sp"
            android:gravity="center"
            android:background="@drawable/circle_text"/>
        <TextView
            android:id="@+id/text_num7"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:textSize="18sp"
            android:gravity="center"
            android:background="@drawable/circle_text"
            android:backgroundTint="#0788F8"/>
    </LinearLayout>
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="随机选号"
        android:textSize="18dp"
        android:layout_below="@id/linearLayout_1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="15dp"
        android:background="#CA0505"/>
</RelativeLayout>

创建名为BinderService的Service:

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
public class BinderService extends Service {
    public BinderService() {
    }

    //1. 创建MyBinder内部类
    public class MyBinder extends Binder{
        public BinderService getService(){//创建、获取Service的方法
            return BinderService.this;  //返货当前Service类
        }
    }
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return new MyBinder();  //2. 返回MyBinder对象
    }

    //3. 生成随机数
    public List getRandomNum(){
        List resArr = new ArrayList();
        int number;
        String strNumber = "";//用于保存生成的随机数的字符串
        for (int i = 0; i < 7;i++){
            //生成指定范围的随机整数
            if (i == 6){
                number = new Random().nextInt(16) +1;
            }else {
                number = new Random().nextInt(33) +1;
            }
            if (number < 10){
                strNumber = "0"+String.valueOf(number);
            }else {
                strNumber = String.valueOf(number);
            }
            resArr.add(strNumber);//转换后的字符串添加到LIst
        }
        return resArr;
    }

    @Override
    public void onDestroy() {//4. 销毁Service
        super.onDestroy();
    }
}

编写MainActivity.java实现调用Service及单击响应逻辑:

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
public class MainActivity extends AppCompatActivity {
    BinderService binderService;
    //文本框组件ID
    int[] tvids = {R.id.text_num1,R.id.text_num2,R.id.text_num3,
                   R.id.text_num4,R.id.text_num5,R.id.text_num6,R.id.text_num7};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_random = findViewById(R.id.btn);
        btn_random.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //2. 单击事件
                List num = binderService.getRandomNum();
                for (int i = 0; i< num.size();i++){
                    TextView tv = findViewById(tvids[i]);
                    tv.setText(num.get(i).toString());
                }
            }
        });
    }
    //1. 创建ServiceConnection对象
    private ServiceConnection connection = new ServiceConnection() {
        //绑定Service的组件与Service连接成功时回调
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //获取后台Service
            binderService = ((BinderService.MyBinder)service).getService();
        }
        //绑定Service的组件与Service断开连接时回调
        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    //3.
    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(MainActivity.this,BinderService.class);
        bindService(intent,connection,BIND_AUTO_CREATE);
    }

    //4.
    @Override
    protected void onStop() {
        super.onStop();
        unbindService(connection);
    }
}

Intent Service 的使用

IntentService 是 Service 的子类。

对于 普通的Service,它 不能自动开启线程也不能自动停止服务;但是对于 IntentService,它 可以自动开启线程也可以自动停止服务

我们接下来分别 对普通的Service和IntentService进行测试~

普通的Service

编写布局资源文件activity_main.xml用于设置启动Service或IntentService的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<RelativeLayout ...
    android:gravity="center">
    <Button
        android:id="@+id/service"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动Service"
        android:textSize="18sp"
        android:textAllCaps="false"/>
    <Button
        android:id="@+id/intentService"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/service"
        android:layout_marginStart="20dp"
        android:text="启动IntentService"
        android:textSize="18sp"
        android:textAllCaps="false"/>
</RelativeLayout>

创建名为MyService的Service,模拟耗时任务并输出打印日志:

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
public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        Log.i("Service","Service已创建");
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("Service","Service已启动");
        long endTime = System.currentTimeMillis()+10*1000;//结束时间
        while (System.currentTimeMillis()<endTime){
            synchronized (this){
                try {
                    wait(endTime-System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        Log.i("Service","Service已停止");
        super.onDestroy();
    }
}

编写MainActivity.java用于控制按钮启动Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_service = findViewById(R.id.service);
        btn_service.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startService(new Intent(MainActivity.this,MyService.class));
            }
        });
    }
}

测试: 启动程序,打开虚拟机,点击「启动 Service」,等待耗时任务完成,我们可以在 Logcat 面板上看到:

1
2
3
2020-05-04 12:40:44.105 30626-30626/com.yang.intentservice I/Service: Service已创建
2020-05-04 12:40:44.106 30626-30626/com.yang.intentservice I/Service: Service已启动
2020-05-04 12:40:54.108 30626-30626/com.yang.intentservice I/Choreographer: Skipped 600 frames!  The application may be doing too much work on its main thread.

Service 创建和启动都完成了,但是完成耗时任务后,Logcat 面板打印出「The application may be doing too much work on its main thread.」,这句话表示应用在主线程中做了耗时任务,并且在模拟器上可能出现「应用无响应。要将其关闭吗?」的对话框(ANR 错误)。这个对话框可以说并不是错误,如果我们点击了「等待」,过一段时间后应用会恢复正常,但这样对用户的体验是不好的,我们应该在设计的时候避免 ANR 错误的出现。

那么我们应该怎么做才可以呢?我们需要在模拟耗时任务时创建一个新的线程!

对MyService中的onStartCommand()方法进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("Service","Service已启动");
        new Thread(new Runnable() {
            @Override
            public void run() {
                long endTime = System.currentTimeMillis()+10*1000;//结束时间
                while (System.currentTimeMillis()<endTime){
                    synchronized (this){
                        try {
                            wait(endTime-System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start(); //开启线程
        return super.onStartCommand(intent, flags, startId);
    }

这时,我们在进行测试,我们发现 Logcat 面板中没有出现「The application may be doing too much work on its main thread.」,模拟器上也没有出现 ANR 错误的对话框。

1
2
2020-05-04 12:55:12.193 30763-30763/com.yang.intentservice I/Service: Service已创建
2020-05-04 12:55:12.194 30763-30763/com.yang.intentservice I/Service: Service已启动

但我们仔细观察后会发现,耗时任务结束后,Service 没有关闭,难道普通的 Service 不能自动关闭吗?

我们再修改MyService中的onStartCommand()方法进行手动关闭Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("Service","Service已启动");
        new Thread(new Runnable() {
            @Override
            public void run() {
                long endTime = System.currentTimeMillis()+10*1000;//结束时间
                while (System.currentTimeMillis()<endTime){
                    synchronized (this){
                        try {
                            wait(endTime-System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                stopSelf();
            }
        }).start(); //开启线程
        return super.onStartCommand(intent, flags, startId);
    }

查看 Logcat 面板:

1
2
3
2020-05-04 13:00:59.248 30841-30841/com.yang.intentservice I/Service: Service已创建
2020-05-04 13:00:59.259 30841-30841/com.yang.intentservice I/Service: Service已启动
2020-05-04 13:01:09.265 30841-30841/com.yang.intentservice I/Service: Service已停止

这时我们发现打印出了 Service 已停止的日志,也证实了普通的 Service 不能自动关闭。

最后我们可以得出结论:普通的 Service,它不能自动开启线程,也不能自动停止服务。

IntentService

创建名为MyIntentService的IntentService,模拟耗时任务并输出打印日志:

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
public class MyIntentService extends IntentService {
    /**
     * Creates an IntentService.  Invoked by your subclass's constructor.
     *
     * @param name Used to name the worker thread, important only for debugging.
     */
    public MyIntentService(String name) {
        super(name);
    }

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        Log.i("IntentService","Service已启动");
        long endTime = System.currentTimeMillis()+10*1000;//结束时间
        while (System.currentTimeMillis()<endTime){
            synchronized (this){
                try {
                    wait(endTime-System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void onDestroy() {
        Log.i("IntentService","Service已停止");
        super.onDestroy();
    }
}

在 MainActivity.java 中为启动 IntentService 的按钮编写逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //普通Service的启动...

        //IntentService的启动
        Button btn_intentService = findViewById(R.id.intentService);
        btn_intentService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startService(new Intent(MainActivity.this,MyIntentService.class));
            }
        });
    }
}

测试: 启动程序,打开虚拟机,点击「启动 IntentService」,等待耗时任务完成,我们可以在 Logcat 面板上看到:

1
2
2020-05-04 14:48:14.806 31247-31303/com.yang.intentservice I/IntentService: Service已启动
2020-05-04 14:48:24.810 31247-31247/com.yang.intentservice I/IntentService: Service已停止

同时在模拟器上,也不会出现 ANR 错误。

因此我们不难得出结论:IntentService 可以自动开启线程执行耗时任务,耗时任务执行完还可以自动关闭。


Android 数据存储、Handler 与 Service 完