0%

第10章: 后台默默的劳动者-探究服务

服务非常适合执行那些不需要和用户交互还要求长期运行的任务。要注意的是,服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程,当应用程序被杀掉时,所有依赖该进程的服务也会停止。不要被服务的后台概念迷惑,实际上服务并不会自动开启子线程,所有的代码都是默认运行在主线程中。因此在使用服务时要注意主线程被阻塞的情况。

服务的基本用法

新建类MyService继承Service,要求重写其唯一一个抽象方法 onBind 。一般又要重写onCreate(服务创建时调用)、onStartCommand(每次服务启动的时候调用)以及onDestroy(服务销毁时调用)方法,它们是服务中最常用的3个方法。如果我们希望服务已启动就立刻执行某个动作,就可以将逻辑写在onStartCommand方法里。另外,还需要在AndroidManifest文件中做如下声明(四大组件都得声明):

1
2
3
4
5
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"
</service

其中export属性表示是否允许除了当前程序之外的其他程序访问这个服务,enable表示是否启用这个服务。

启动和停止服务

通过以下代码可以简单地实现启动和停止服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onClick(View v){
switch(v.getId()){
case R.id.start_service:
Intent startIntent = new Intent(this,MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent startIntent = new Intent(this,MyService.class);
stopService(startIntent);
break;
}
}

构建出Intent对象,使用startService即可启动MyService,会依次执行onCreate->onStartCommand;同理,使用stopService即可停止,onDestroy就会执行。如果在service里面,那么使用stopSelf即可停止自己。由于刚才点击start的时候,onCreate和onStartCommand都执行了,也许你会疑惑,这两个方法到底有什么区别呢?其实onCreate方法在服务第一次创建时调用,而onStartCommand则在每次启动服务时调用,由于是第一次创建,所以两个方法都执行了,如果多次点击start service按钮,那就只有onStartCommand方法执行了。

活动和服务通信

上一节虽然在活动中启动和停止了服务,但是启动服务之后,活动与服务基本上就没关系了,没法控制服务。如果希望控制服务执行,比如MyService提供下载功能,则希望可以决定什么时候开始下载,以及查看下载进度等,主要的思路是创建一个专门的Binder对象来对下载功能进行管理,然后通过ServiceConnection来实现通信。简易代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//service代码
public class MyService extends Service{
private DownloadBinder mBinder = new DownloadBinder();

class DownloadBinder extends Binder{
public void startDownload(){

}

public int getProgress(){
return 0;
}
}

@Override
public IBinder onBind(Intent intent){
return mBinder;
}
}
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
//Activity代码
private ServiceConnection connection = new ServiceConnection(){
@Override
public void onServiceDisconnected(ComponentName name){//解除绑定时调用
}

@Override
public void onServiceConnected(ComponentName name,Ibinder service){//绑定时调用
downloadBinder = (MyService.DownloadBinder)service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
}
@Override
public void onClick(View v){
switch(v.getId()){
case R.id.bind_service:
Intent bindIntent = new Intent(this,MyService.class);
bindService(bindIntent,connection,BIND_AUTO_CREATE);//绑定服务
break;
case R.id.unbind_service:
unbindService(connection);//解绑服务
break;
}
}

服务生命周期

一旦在任何位置调用了Context的startService方法,相应的服务就会启动,如果服务还没创建过,onCrete先执行,在执行onStartCommand,如果已经创建了,则只会执行onStartCommand了,启动后该服务一直运行,直到调用stopService或者stopSelf为止。虽然多次启动服务onStartCommand会被多次执行,但是服务只会存在一个实例。
另外,还可以调用Context的bindService来获取一个服务的持久连接,这时会回调服务的onBind方法,类似地,如果之前没创建过该服务,就会先执行onCreate在执行onBind,之后,调用方可以获取到onBind方法里返回的IBinder对象实例,就能自由和服务进行通信。调用unbind方法,也会执行Myservice的onDestroy方法,即销毁服务。

服务的更多技巧

使用前台服务

由于服务运行在后台,因此其系统优先级比较低,当出现内存不足的情况时,容易被回收,这就考虑使用前台服务。前台服务和普通服务最大的区别在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉可以看到更详细的信息,非常类似于通知效果。其主要实现方法如下:

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

@Override
public void onCreate(){
super.onCreate();
Intent intent = new Intent(this,MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("title")
.***//其他配置省略了
.build();

//没有像普通Notification一样使用NotificationManager将通知展示出来,而是使用
//startForeground方法
**startForeground(1,notification);//与普通Notification和普通的Service区别的关键在这里**
}
}

使用IntentService

服务中的代码都是默认运行在主线程当中的,如果直接在服务里处理耗时操作,很容易出现ANR。当然,为了避免这种情况,你可以在onStartCommand方法中new 一个Thread来处理耗时逻辑,写法并不复杂,但是程序员容易忘记开启线程或者忘记调用stopSelf。为了简单地创建一个异步的、能够自动停止的服务,Android专门提供了IntentService,在继承Intentservice之后,你可以直接在它的 onHandleIntent方法中执行耗时操作(这是在新的线程里面,可以通过Thread.currentThread
().getId()方法查看线程ID,跟主线程不是同一个),在耗时操作执行完毕之后,这个IntentService会自动调用onDestroy停止。

服务的最佳实践——完整版下载示例

直接上代码吧,多说无益。用于执行异步任务的AsyncTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//这里AsyncTask传入3个参数类型,第一个泛型指定为String,表示在执行AsyncTask时需要传入一个
//字符串给后台任务,第二个指定为Integer,表示使用整型数据作为进度显示单位,第三个泛型Intege则表示使用整数型
//数据来反馈执行结果
public class DownloadTask extends AsyncTask<String,Integer,Integer>{
public static final int TYPE_SUCCESS = 1;//成功
public static final int TYPE_FAILED = 2;//失败
public static final int TYPE_PAUSED = 3;//暂停
public static final int TYPE_CANCELED = 4;//取消

private DownloadListener downloadListener = null;
private boolean isPaused = false;
private boolean isCancled = false;
private int lastProgress;

public DownloadTask(DownloadListener listener){
downloadListener = listener;
}

@Override
protected Integer doInBackground(String... params) {//用于后台执行下载逻辑
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try{
long downloadedLength = 0;//记录已经下载的文件长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//SD卡的Download目录
String directory = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).getPath();
file = new File(directory + fileName);

//如果已经存在要下载的文件了,则读取已经下载的字节数,这样就可以在后面启动端点续传功能
if(file.exists()){
downloadedLength = file.length();
}
//获取文件总长
long contentLength = getContentLength(downloadUrl);
if(contentLength == 0){
return TYPE_FAILED;
}else if(contentLength == downloadedLength){
//已下载的字节和总字节相等,说明已经下载完成
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//断点下载,指定从哪个字节开始下载
.addHeader("RANGE","bytes=" + downloadedLength + "-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if(response != null){
//以下读取服务器响应的数据,并使用Java文件流方式,不断从网络读取数据,不断写到本地,知道
//文件全部下载完成为止
is = response.body().byteStream();
savedFile = new RandomAccessFile(file,"rw");
savedFile.seek(downloadedLength);//跳过已下载的字节
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = is.read(b)) != -1){
if(isCancled){
return TYPE_CANCELED;
}else if (isPaused){
return TYPE_PAUSED;
}else {
total += len;
savedFile.write(b,0,len);
//计算已下载的百分比
int progress = (int) ((total + downloadedLength) * 100/contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if (is != null){
is.close();
}
if (savedFile != null){
savedFile.close();
}
if (isCancled && file != null){
file.delete();
}
}catch (Exception e){
e.printStackTrace();
}
}
return TYPE_FAILED;
}

@Override
protected void onProgressUpdate(Integer... values) {//用于在界面上更新当前的下载进度
int progress = values[0];
if (progress > lastProgress){
downloadListener.onProgress(progress);
lastProgress = progress;
Log.e("进度",progress+"");
}
}

@Override
protected void onPostExecute(Integer status) {//用于通知最终的下载结果
switch (status){
case TYPE_SUCCESS:
downloadListener.onSuccess();
break;
case TYPE_FAILED:
downloadListener.onFailed();
break;
case TYPE_PAUSED:
downloadListener.onPaused();
break;
case TYPE_CANCELED:
downloadListener.onCancled();
break;
default:

break;
}
}

public void pauseDownload(){
isPaused = true;
}

public void cancelDownload(){
isCancled = true;
}

//获取要下载的内容的大小
private long getContentLength(String downloadUrl) throws IOException{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if(response != null && response.isSuccessful()){
long contentLength = response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
}

用于执行后台任务的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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class DownloadService extends Service{
private DownloadTask downloadTask;
private String downloadUrl;

private DownloadListener listener = new DownloadListener(){

@Override
public void onProgress(int progress) {
getNotificationManager().notify(1,getNotification("Downloading...",progress));
}

@Override
public void onSuccess() {
downloadTask = null;
//下载成功时将前台服务通知关闭,并创建一个下载成功的通知
//这句代码将服务创建成前台服务
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Success",-1));
Toast.makeText(DownloadService.this,"Download success",Toast.LENGTH_LONG).show();

}

@Override
public void onFailed() {
downloadTask = null;
//下载失败将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this,"Download failed",Toast.LENGTH_LONG).show();

}

@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_LONG).show();

}

@Override
public void onCancled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_LONG).show();
}
};

//为了让service可以和Activity通信,创了这个binder
class DownloadBinder extends Binder{
public void startDownload(String url){
if(downloadTask == null){
downloadUrl = url;
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_LONG).show();
}
}

public void pauseDownload(){
if (downloadTask != null){
downloadTask.pauseDownload();
}
}

public void cancelDownload(){
if (downloadTask != null){
downloadTask.cancelDownload();
}else{
if (downloadUrl != null){
//取消下载时需要将文件删除,并将通知关闭
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + fileName);
if(file.exists()){//如果已经存在要下载的文件了,则读取已经下载的字节数,这样就可以在后面启动端点续传功能
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_LONG).show();
}
}
}
}

private DownloadBinder mBinder = new DownloadBinder();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}


private NotificationManager getNotificationManager(){
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}

private Notification getNotification(String title,int progress){
Intent intent = new Intent(this,MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);

NotificationCompat.Builder builder;

//TODO 注意这段代码,在8.0以后通知要求设置 NotificationChannel,否则会报错
//TODO 在书本中的代码没有这一段
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ){
NotificationChannel channel = new NotificationChannel("im_channel_id","System", NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if(manager != null){
**manager.createNotificationChannel(channel);
}
}

//书上是 builder = new NotificationCompat.Builder(this); ,但是这个方法现在已经废弃了
builder = new NotificationCompat.Builder(this,"im_channel_id");
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if (progress > 0){
//当progress大于或者等于0才需显示下载进度
builder.setContentText(progress + "%");
builder.setProgress(100,progress,false);
}
return builder.build();
}
}

Activity中的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private DownloadService.DownloadBinder downloadBinder;
//为了能够控制service的行为
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;

}

@Override
public void onServiceDisconnected(ComponentName name) {

}
};

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

findViewById(R.id.start).setOnClickListener(this);
findViewById(R.id.pause).setOnClickListener(this);
findViewById(R.id.cancel).setOnClickListener(this);

Intent intent = new Intent(this,DownloadService.class);
startService(intent);
bindService(intent,connection,BIND_AUTO_CREATE);

//6.0及以上要求动态申请权限,有权限才能使用这个下载功能
if(ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]
{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);

}
}

@Override
public void onClick(View v) {
//首先判断downloadBinder是否有效
if (downloadBinder == null){
return;
}
switch (v.getId()){
case R.id.start:
String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
downloadBinder.startDownload(url);
break;
case R.id.pause:
downloadBinder.pauseDownload();
break;
case R.id.cancel:
downloadBinder.cancelDownload();
break;
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){
Toast.makeText(MainActivity.this,"拒绝权限无法使用程序",Toast.LENGTH_SHORT).show();
finish();
}
break;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}

当然,还有要注意的是,要在AndroidManifest.xml中注册Service:

service android:name=”.DownloadService”

还有,在AndroidManifest.xml中需要标明网络权限和存储权限:

uses-permission android:name=”android.permission.INTERNET”
uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”

最后,针对本例子,例子中使用了okhttp,需要在buildgradle中添加依赖:

implementation ‘com.squareup.okhttp3:okhttp:3.9.0’

谢谢你的鼓励