Service的前世今生(上:单进程模型)

Posted by 阿呆 on 2019-01-14

Part 1 进程详解篇

进程优先级

在Android中,一般情况下,APP的所有组件(四大组件)都是运行在一个进程中的,所以系统杀死这个进程,APP就没了。系统杀死进程是因为系统的内存资源比较紧张,系统需要为一些急需内存资源的组件去提供足够的内存资源,所以系统就把一些进程杀死了

一般情况,言外之意,你可以指定某些组件运行在指定的进程中(通过在清单文件中配置android:process)
那么,杀死进程,杀死什么样的进程呢?这里涉及到进程的优先级问题。Android中将进程的优先级分为5个层级,主要依据对于用户的需求程度来判断的
优先级分类:从上到下递减

  1. 前台进程
  2. 可见进程
  3. 服务进程
  4. 后台进程
  5. 空进程

前台进程

情景一:该进程中有Activity的onResume被调用,此时用户正与这个Activity交互。
情景二:该进程中有一个Service,该Service绑定到当前正在与用户交互的Activity。
情景三:该进程中的通过startForegrount()启动的Service。
情景四:该进程中的Service正在执行onCreate()、onStart()、onDestory()。
情景五:该进程中有正在执行onReceiver()的BroadcastReceiver。

可见进程

情景1:该进程的service绑定到了可见或者在前台的Activity
情景2:当前进程中有Activity,但是不在前台,却可以看见(app_a打开透明主题的app_b)

服务进程

本文的重点,该进程中有通过 startService启动的服务,同时该服务的情况不属于前台进程以及可见进程。服务进程的重要性仅次于前台进程和可见进程。虽然服务进程不会对用户造成感知上的影响,但是有的时候做文件下载或者后台播放音乐,是需要在这个进程中进行的。这也是为什么这样的长时间的下载操作不会放在Activity中的原因,因为一旦App不可见,App的优先级就会变低,随时都有可能停止下载。
杀死服务进程的可能性也较低,除非内存不足以支撑前台进程、可见进程的运行

Attention
开启一个服务,并不会直接开启一个新的进程(除非你指定了新的进程),也不会开启一个新的子线程。服务也是运行在主线程中的,所以如果需要在服务中进行耗时操作,需要在服务中开启一个子线程去下载文件。

后台进程

该进程包含对用户不可见的Activity进程( 该Activity的onStop 方法被调用)
这种进程很容易被kill掉,因为它不会影响用户的直观体验,在手机中会有很多这种进程,他们都被缓存在一个列表中,这个列表就是 LRU 列表(打开App重新从引导页进入,说明它被Kill了)

空进程

没有处于活动状态的任何组件所在的进程
刚才在服务进程中讲的在Activity中开启子线程下载文件,退出APP后,该进程就变成了空进程(Actvity被销毁了?,而不是退到后台,所以没有任何actvity了,只有个Thread在运行?)。之所以保留这种进程,是为了下次快速启动该进程。这种进程经常被干掉。
$有个疑问,当Activity退出后,线程持有它的引用,那么它的实例应该就不会被销毁,那么这就不能算空进程了吧?会不会出现强制销毁Activity的情况?因为我之前看了一个View更新出现错误的情况,因为相关View已经被销毁了。
@这时候Activity还在(Thread持有它的引用,但是View被销毁了,detach,所以更新UI就会报错)

Part 2 应用篇

服务进程有个特点,就是当服务进程被系统杀死后,在内存充足的情况下会自动重新启动,适合做一些后台的操作。而后台进程喝空进程则不具备这一特点。

Service的生命周期

Service可以看成一种没有前台界面的Activity,生命周期与Activity有点类似

Service的生命周期

注意 clients 是个复数

服务有两种使用方法

方式1:调用startService创建启动服务
方式2:调用bindService创建绑定服务

无论那种方式,都应该先在清单文件中注册(这是四大组件的通性)

1
<service android:name=".RemoteService"/>

为了不让其他应用调用到我们的Service,我们
1.不要在Service中添加IntentFilter过滤器,也就是说尽量显示启动或者绑定Service
2.更安全的方式是添加 android:exported=“false”

集成自Service或者其子类

1
2
3
4
5
6
public class RemoteService extends Service{
@Override
public IBinder onBind(Intent intent){
return null;
}
}

经过上面两步操作,我们便创建好了一个Service

下面我们来分别讲解两种服务的创建方式

创建启动的服务 | By start service

启动服务
1
2
3
4
5
6
7
public void startService(View view)
{
Intent intent = new Intent();
intent.setClass(this,RemoteService.class);
intent.putExtra("sendData","Intent中保存的数据");
startService(intent);
}

首先为了给大家一个直观的认识,可以在设置-应用-已启动里面查看,显示为一个进程和一个服务

创建 启动的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
public class RemoteService extends Service
{
@Override
public void onCreate()
{
super.onCreate();
Log.d("onCreate------------------------------");
}

@Override
public int onStartCommand(Intent intent,int flags,int startId)
{
Log.d("onStartCommand-----Intent:"+inetnt.getStringExtra("sendData"));
return super.onStartCommnad(intent,flags,startId);
}

@Override
public void onDestroy()
{
super.onDestroy();
Log.d(TAG,"onDestroy-----------------------");
}

@Override
public IBinder onBind(Intent intent)
{
return null;
}
}

讲讲这几个回调方法

1.onCreate()
当Service被创建的时候会回调这个方法,第一次启动Service的时候,先创建(onCreate),再启动(onStartCommand)

2.onStartCommand()
这个方法默认返回 super.onStartCommand(intent,flags,startId),let’s see it

1
2
3
4
5
public int onStartCommand(Intent intent,int flags,int startId)
{
onStart(intent,startId);
return mStartCompatibility?STRAT_STICKY_COMPATIBILITY : START_STICKY;
}

默认情况下mStartCompatibility为false,返回START_STICKY,其实这个返回值有三种
还记得前面讲解Service的生命周期的时候说过,Service的生命周期很牛逼,因为它被系统杀死后,在内存充足的时候,会重新自动启动。
这里的重新启动,有三种对应的方式,上面的返回值就对应三种启动方式之一。下面,我们来看看这三个具体的返回值

START_NO_STICKY
sticky是"粘性的",这里指的非粘性启动,下面是谷歌文档的翻译

如果返回START_NOT_STICKY,表示当service运行的进程被系统强制杀掉之后,不会重新创建该service,当然如果在其被杀掉一段时间之后又调用了startService,那么该service又将被实例化

TODO ,这一块有些不理解,回头再来补充吧

入参

1.Intent
这个Intent就是刚才startService(intent)中的intent,在创建启动的服务所在的组件(这里是Activity)中如果需要传递数据给Service中的数据,可以将要传递的数据放在这个intent里面

2.flags
代表了启动Service的方式,是下面三种方式值得一种,这三种方式得值对应着上面的三种启动方式

1
2
3
4
5
6
7
0:
对应START_NOT_STICKY启动方式

START_FLAG_STICKY(0x0002)
对应START_NOT_STICKY启动方式

STRAT_FLAG_REDELIVERY(0x0001)

startId
这个代表每次创建启动Service的唯一身份ID,每次startService,这个startId均不相同,**这个用于处理多个onStartCommand请求时,关闭Service时使用。**下面会对这个使用进行详细的讲解的

3.onDestroy()
服务停止的时候会回调 onDestroy 方法

总结

1.其它组件可以通过startService(intent)创建启动服务。该服务被启动之后,服务与启动它的组件没有任何关系了。即使该组件被干掉,(这里指的手动停止某个组件,并非指被系统回收,因为系统回收的基本单位是进程,进程死了,里面的东西都没了。但是多进程App除外哈)该service依然不会被干掉。

2.这种方式的服务一旦被启动,基本不会被关闭(不h会自己关闭,只能手动关闭),除非不足以支持优先级较高的进程(前台进程、可见进程)继续运行,系统才会停止该服务,系统一旦停止该服务后,当系统内存足够的时候会再次重启(具体依赖于onStartCommand的返回值),但是如果手动关闭了该服务,该服务不会再次重启了

3.可以将要传递给service的数据放在intent里面

4.Service默认情况下不会单独启动一个进程或者线程,默认情况下与主线程所在的进程在同一个进程中。所以,不能在service中进行耗时操作

关闭服务

关闭服务有两种方法,stopService() 和 stopSelf() 以及 stopSelf(int startId)

StopService

1
2
3
4
5
6
public void stopService(View view)
{
Intent intent = new Intent();
intent.setClass(this,RemoteService.class);
stopService(intent);
}

这种方法适用于第三方组件关闭该服务

stopSelf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int onStartCommand(Intent intent,int flags,int startId)
{
Log.d("Dan","--intent:"+intent.getStringExtra("sendData")+"flags="
+flags+"----startId="+startId);
new Thread(){
@Override
public void run(){
super.run();
try{
sleep(6000);
stopSelf(); // 干掉自己
}catch(InterruptedException e){
e.printStackTrace();
}
}
}.start();
return super.onStartCommand(intent,flags,startId);
}

在上面的例子中,在onStartCommand中开启一个子线程,6s(模拟下载)后调用stopSelf方法干掉自己,停止服务停止服务。那么问题来了,如果我们需要下载多次呢,不能每次下载完就关闭一次,然后新的下载又得重新创建启动service,这是不合理的。

下载完所有图片后则需要关闭服务,那么我们可能需要这样一个东西,在某个下载线程结束的时候,它记录着目前还有没有其余的正在下载的任务。

Google就为我们提供了这么一个方法, stopSelf(),我们来看一下代码

1
2
3
4
5
6
....As above
try{
sleep(1000*startId);
stopSelf(startId);
}
....

废话少说,我们来看一下源码
startId

分析:
每次一个 onStartCommand请求的时候,都会传过来唯一的startId,这个startId被线程携带着,当代码执行到 stopSelf(startId)的时候,进行 r.getLastStartId!=startId(最近启动的service的id跟startId是否相等)的判断,如果不等,就return掉,从而不会停止服务,如果相等,则说明在自己线程运行的过程中,没有新的 onStartCommand 请求过来,这时就可以停止掉服务了。

小结创建启动的 Service

1.清单文件注册,继承service
2.纯启动service生命周期 onCreate->onStartCommand->onDestroy
3.在onStartCommand方法中开启子线程进行估计耗时的操作
4.子线程结束后stopSelf关闭service
5.也可以通过其它组件stopService(intent)来关闭指定的service

创建 绑定的Service

我们知道创建了启动的Service后,Service与启动它的组件就不会有任何关系了(当然,你也可以通过广播来使二者建立起联系),这时的Service就像一个断了线的风筝一样,不会受我们控制了

那么如果有如下需求:开发一个音乐播放器,主页面有上一首、下一首、播放、暂停等基本按钮


分析:

首先,把播放音乐的代码放在MainActivity里的子线程里不行,因为播放器退到后台后,还是要继续播放音乐的,MainActivity退出的时候,仅仅有一个线程的App就变成了空进程,很容易被杀掉,所以播放音乐的代码不能放在MainActivity的子线程中去

所以我们需要用到service

但是,如果我们在service的startCommand方法中play Music是没有问题的,但是暂停、播放以及replay呢?这些我们无法控制,只能让一首歌一直播放直到停止。

那我们就要用到创建绑定的服务了。

接下来,我们分绑定服务解绑服务两个部分来讲解

绑定Service

一共有三步,打开冰箱门、把大象塞进去、关不上冰箱门

这里先从总体上介绍一下,我们是如何控制绑定的service的,很明显,我们需要拿到绑定的service的实例,这里是通过回调的方式,在 onConnected()方法中,会传回来一个Binder对象,我们在Service内部有一个Binder实例,在onBind方法回调的时候,我们会返回这个Binder对象(Service实例),经过几次辗转。这个Binder对象,会最终到达ServiceConnection对象,在onServiceConnected方法中实例化到本地(保存在Activity中),然后我们就可以为所欲为辣!!!但是,注意,实例化service并调用其public方法只是我们初步的方案,在单一进程内,这样做看似没有什么问题,可是即便是这样,还是会带来很多问题。

下面是详细步骤:

1.新建一个继承自Service的类MyService,然后在AndroidManifest.xml里注册这个Service
2.Activity里面使用bindService方式启动MyService,也就是绑定了MyService
(到这里实现了绑定,Activity与Service通信的话继续下面的步骤)
3.新建一个继承自Binder的类MyBinder
4.在MyService里实例化一个MyBinder对象mBinder,并在onBind回调方法里面返回这个mBinder对象
5.第2步bindService方法需要一个ServiceConnection类型的参数,在ServiceConnection里可以取到一个IBinder对象,就是第4步onBinder返回的mBinder对象(也就是在Activity里面拿到了Service里面的mBinder对象)
6.在Activity里面拿到mBinder之后就可以调用这个binder里面的方法了(也就是可以给Service发消息了),需要什么方法在MyBinder类里面定义实现就行了。如果需要Service给Activity发消息的话,通过这个binder注册一个自定义回调即可。

下面我们先来分析一下这种方案详细的代码实现:

RemoteService.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
public class RemoteService extends Service 
{
// myBinder持有Service的引用,在Activity内我们会得到myBinder的实例,
//也就可以通过Binder的getRemoteService方法得到RemoteService的实例
private final MyBinder myBinder = new MyBinder();

@Override public void onCreate() {//Service被创建时回调
super.onCreate();
Log.i("Dan", "------onCreate-----");
}

/**
* @param intent 传递过来的包装的intent数据
* @param flags
* @param startId 启动的Service的唯一标记
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, final int startId) //Service被启动时回调
{
Log.i("hui", "-onStartCommand-intent:"+ intent.getStringExtra("sendData")+ "flags="+flags+"---startId="+startId);
return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() //Service被销毁时回调
{
super.onDestroy();
Log.i("hui", "------onDestroy-----");
}

/**
* 绑定的Service必须实现的方法,返回非null
* @param intent
* @return
*/
@Override
public IBinder onBind(Intent intent) {
Log.i("hui", "-onBind-intent:"+ intent.getStringExtra("sendData"));
return myBinder;
}

public void playMusic()
{
Log.i("hui", "--------playMusic------");
}
public void pauseMusic()
{
Log.i("hui", "--------pauseMusic------");
}

/**
* Binder是实现了IBinder接口的对象
*/
public class MyBinder extends Binder{
/**
* 拿到Service的对象
* @return
*/
public RemoteService getRemoteService(){
return RemoteService.this;
}
}
}

再来看看

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

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

public void bindService(View view){
Intent intent = new Intent();
intent.putExtra("sendData", "通过bindService的intent保存MainActivity组件中要传递的数据");
intent.setClass(this, RemoteService.class);
/**
* Intent service, ServiceConnection conn, int flags
** @param service 包装了要启动的Service及要携带的信息
* @param conn
* @param flags 启动Service的发送,一般为BIND_AUTO_CREATE,表示:如果服务不存在会自动创建 */
bindService(intent, serviceConnection, BIND_AUTO_CREATE);
}
//解绑service也非常简单: unbindService(serviceConnection)

//播放
public void play(View view){
if(remoteService != null){
remoteService.playMusic();
}
}

//暂停
public void pause(View view){
if(remoteService != null){
remoteService.pauseMusic();
}
}

/**
* 服务连接对象
*/
private ServiceConnection serviceConnection = new ServiceConnection() {
/**
* 当创建绑定Service与组件成功连接后,此方法会
* 被调用。
* @param name 组件名称
* @param service Service中返回的IBinder对象
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("hui", "---------onServiceConnected-----------");
//将拿到的Service中onBInder返回的对象也就是MyBInder对象进行强制转换
RemoteService.MyBinder myBInder = (RemoteService.MyBinder) service;
//调用MyBinder的方法,拿到RemoteService对象
remoteService = myBInder.getRemoteService();
}
/**
* 当创建绑定Service与组件意外中断后(unbindservice时这个方法并不会回调),此方法会被调用。
* @param name
*/
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d("hui", "---------onServiceConnected-----------");
}
};
}
注意几个地方

1.bindService是一个异步方法,所以不要在bindService之后同步调用service的方法,会报null错误。这里有一篇文章详细地分析了这个过程 bindService的异步绑定,实际上,startService也是一个异步过程。

2.bindService调用多次,而onBind()则只会回调一次,类似于start方式的onCreate().

3.onServiceDisconnect正常情况下不会被回调,只有在意外终止的情况下会被回调

3.当unbindService之后,不要再次unbind,避免程序crash

Service的绑定

这样调用service的实例是非常不推荐的!!!

问题分析:当我们的Activity退出后,音乐还能继续播放么?

被绑定的Service的生命周期

对于上面的那个问题,在正常情况下,当被绑定的service的所有与其绑定的组件都销毁过后,该service也将被销毁,这在实际情况中是很不灵活的,比如我们播放音乐的service,不可能因为你返回上一个页面就停止播放音乐。

那么有什么好的解决方案么?

混合启动方案

创建启动的Service+创建绑定的Service的混合启动方案(bindService+startService)
也就是说,我们在MainActivity上绑定Service后,再次启动Service,这样,对于创建启动的Service而言不会因为组件的销毁而销毁

我们先来看看官方的一段话

Note that if a stopped service still has ServiceConnection objects bound to it with the BIND_AUTO_CREATE set, it will not be destroyed until all of these bindings are removed.

对于混合启动的service来说,必须同时满足没有组件与其绑定的情况下调用stopService(或者stopSelf())才可以将其销毁。

start和bind的先后顺序没有什么关系

给大家分享一下官方的一张图,可能更直观一些:

Service究竟该不该被我们的Activity本地化实例

在调试的过程中,我发现,即便 unBindService(serviceConnection)调用了,依然可以使用play以及pause等public方法。我的直观感觉是,这肯定是不合理的,因为从原则上来讲,unbind的目的就是解除该组件与service的绑定,但是转念一想,在代码上来说,是不存在问题的,因为Activity已经持有了service的引用(本地的service实例,在 onServiceConnected里面拿到的),而且,在unbindservice之后,虽然public方法可以继续调用,但是再次调用unbindservice程序就会crash,当然从代码上来说,这也没有问题,虽然service已经本地实例化,但是service和该组件的绑定已经解除了,所以存在的是一个被解除绑定的service,这实际上相当于内存已经泄漏了,你再调用unbindService肯定就会导致程序crash,为了使该service在解除绑定的情况下能够被gc回收,我在unbindService之后调用了 service = null,手动置空了service,这样一来,已经Destroy的service就可以被gc回收了(前提是它只有当前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
78
79
80
81
public class BindServiceActivity extends AppCompatActivity implements View.OnClickListener {
private final String TAG = "Dan";
private ServiceConnection serviceConnection;
private MMusicPlayService mMusicPlayService=null;

private Button bindBtn;
private Button playBtn;
private Button stopBtn;
private Button unbindBtn;
private Intent intent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bind_service);

intent = new Intent(this,MMusicPlayService.class);
intent.putExtra("send_data","bindData");

serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
mMusicPlayService = ((MMusicPlayService.MBinder)iBinder).getMMusicPlayService();
}

@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG, "onServiceDisconnected:------------------- ");
mMusicPlayService = null;
}
};

bindBtn = findViewById(R.id.bind_btn);
playBtn = findViewById(R.id.play_btn);
stopBtn = findViewById(R.id.stop_btn);
unbindBtn = findViewById(R.id.unbind_btn);

bindBtn.setOnClickListener(this);
playBtn.setOnClickListener(this);
stopBtn.setOnClickListener(this);
unbindBtn.setOnClickListener(this);
}

@Override
public void onClick(View v)
{

switch (v.getId())
{
case R.id.bind_btn:
bindService(intent,serviceConnection,BIND_AUTO_CREATE);
break;
case R.id.play_btn:
if(mMusicPlayService!=null){
Log.d(TAG, "onClick: service is not null "+mMusicPlayService);
mMusicPlayService.play();
}
else{
Toast.makeText(this,"请先启动服务",Toast.LENGTH_SHORT)
.show();
}
break;
case R.id.stop_btn:
if(mMusicPlayService!=null)
mMusicPlayService.stop();
else{
Toast.makeText(this,"请先启动服务",Toast.LENGTH_SHORT)
.show();
}
break;
case R.id.unbind_btn:
if(mMusicPlayService!=null){
Log.d(TAG, "onClick: unbindservice----------------");
unbindService(serviceConnection);
//将mMusicPlayService手动置空,因为 onServiceDisconnected正常情况下不会被回调
mMusicPlayService = null;
}
break;

}
}
}

1

2

3

4

5

那么接下来,就让我们来看看 进程间通信 的相关知识吧!