封面画师: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完