引言
服务,老练的程序员都不会陌生,怎么理解呢?我脑海里面最快能想到的就是那些扫地的阿姨,好吧,当然还有餐厅里面的服务员,那么计算机中的服务和这些服务员提供的服务有什么类似的地方吗?是的,我觉得是有的,都是默默无闻的,当然服务员是要拿钱的,计算机可不拿钱,总之它们是在强调一种默默的状态,在计算机里就是“后台”的概念,好了,突然发现我瞎比比的能力还是可以的。
没错,一个老练的Android程序员如果连Service都没听说过的话,那确实也太逊了。Service作为Android四大组件之一,在每一个应用程序中都扮演着非常重要的角色。它主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让Service在后台继续保持运行状态。
Service基础使用方法
如何才能正确的启动一个Service呢?好吧,其实大家都会了,使用Intent,和Activity类似,直接上代码,新建工程ServiceApp,然后新建一个ExampleService继承自Service,并重写父类的onCreate()、onStartCommand()和onDestroy()方法,如下所示:
程序只在在onCreate()、onStartCommand()和onDestroy()方法中分别打印了一句话,并没有进行其它任何的操作。
好了,构建我们要演示的UI,打开或新建activity_main.xml作为程序的主布局文件,代码如下所示:
布局文件中加入了两个按钮,一个用于启动Service,一个用于停止Service。
在主Activity里面加入启动Service和停止Service的逻辑,代码如下所示:
以上代码中,在Start Service按钮的点击事件里,我们构建出了一个Intent对象,并调用startService()方法来启动MyService。然后在Stop Serivce按钮的点击事件里,我们同样构建出了一个Intent对象,并调用stopService()方法来停止MyService,对了这个时候千万别因为少在AndroidManifest.xml文件中声明而掉链子,ok,let’s add them.
写好了这个小例子,最后它应该长这样:

点击启动服务,我们可以得到日志:
再次点击启动服务:
点击停止服务:
再次点击停止服务,什么都不会出现,此时再次点击开启,又会重走:
可以看到,这次只有onStartCommand()方法执行了,onCreate()方法并没有执行,为什么会这样呢?这是由于onCreate()方法只会在Service第一次被创建的时候调用,如果当前Service已经被创建过了,不管怎样调用startService()方法,onCreate()方法都不会再执行。因此你可以再多点击几次Start Service按钮试一次,每次都只会有onStartCommand()方法中的打印日志。
Service和Activity之间进行通信
好了,打怪升级了,Level加一,到现在启动Service之后,就可以在onCreate()或onStartCommand()方法里去执行一些具体的逻辑了。不过这样的话Service和Activity的关系并不大,只是Activity通知了Service一下:“你可以启动了。”然后Service就去忙自己的事情了。但是我现在有更多的想法,比如说在Activity中可以指定让Service去执行什么任务,这样可以吗?答案是肯定的,只要让Activity和Service建立关联就好了。
观察ExampleService中的代码,有一个叫onBind()的方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的,修改ExampleService中的代码,如下所示:
这里我们新增了一个ZombieBinder类继承自Binder类,然后在ZombieBinder中添加了一个sayHello()方法,用来打印一句话,这只是做个测试。
然后修改activity_main.xml中的代码,在布局文件中添加用于绑定Service和取消绑定Service的按钮,并且添加一个使用服务的按钮,用来调zombieBinder的代理方法:
然后我们的Activity变成了:
准备完毕,开始测试:
先点击绑定服务:
点击使用服务:
点击启动服务:
再次点击绑定服务没有任何反应,再次点击startService:
点击停止服务,没有反应,点击解绑服务:
点击开始服务:
点击停止服务:
点击解绑服务,直接crash,错误日志:
好了,试验就先做到这,其实可以玩一天。开始分析啦。
点击绑定服务,如果服务没有create会出发create生命周期,并且会走到serviceConnection的onServiceConnected(),我们在这里可以获取操作服务的代理binder,有了它,我们就能调用服务干我们想干的活啦。所以我们点击使用服务的时候,就会有相应的输出。这时候点击启动服务,得到的输出是onStartCommand() executed,很容易理解,启动服务不依赖bindService操作,如果没有启动服务,那么就create一个服务,会走create生命周期和onStartCommand(),如果不想要用代理操作服务,那么将一些功能写在onStartCommand()中去实现也可以,比如说那种一次性的,调用startService就执行一次。接下来我们点击了停止服务操作,但是发现并没有任何事情发生,原因是,如果这个服务没有接棒的话,是不能被停止的撒,好了,为了验证,我们后来又点击了解绑操作,得到其走到了onDestroy()周期,这时候没有绑定服务,生命周期也走到了尽头,此时我们可以走启动服务,可见顺利的走了onCreate和onStartCommand,这个时候由于没有绑定服务,那么点击停止,发现是可以停止的,走了onDestroy()生命周期,由于这个时候没有服务是绑定的,那么我们去执行解绑操作,会抛出没有注册的service异常,注意一个Service必须要在既没有和任何Activity关联又处理停止状态的时候才会被销毁。
到此发现好乱,一会儿绑定,一会儿启动的,那么它们之间什么关系呢,我自己的理解是,两者最好用一套就可以了,如果说并不想写复杂任务,执行一些一次性的活的话,就使用startService和stopService好了,在onStartCommand中去实现功能,但是对于复杂的长期的后台逻辑的话,我建议使用bindService和unBindService去操作我们的代理傀儡binder。
我们应该始终记得在Service的onDestroy()方法里去清理掉那些不再使用的资源,防止在Service被销毁后还会有一些不再使用的对象仍占用着内存。
Service和Thread的关系
Service和Thread都有后台,即文章开头我描述的那种默默无闻的概念,那么Service和Thread到底有什么关系呢?什么时候应该用Service,什么时候又应该用Thread?答案可能会有点让你吃惊,因为Service和Thread之间没有任何关系!
之所以有不少人会把它们联系起来,主要就是因为Service的后台概念。Thread我们大家都知道,是用于开启一个子线程,在这里去执行一些耗时操作就不会阻塞主线程的运行。而Service我们最初理解的时候,总会觉得它是用来处理一些后台任务的,一些比较耗时的操作也可以放在这里运行,这就会让人产生混淆了。但是,如果我告诉你Service其实是运行在主线程里的,你还会觉得它和Thread有什么关系吗?让我们看一下这个残酷的事实吧。
在MainActivity的onCreate()方法里加入一行打印当前线程id的语句,
同时ExampleService的onCreate()方法里也加入一行打印当前线程id的语句:
好了,执行一下,得到的结果发现两个线程的id都是12293,可以看到,它们的线程id完全是一样的,由此证实了Service确实是运行在主线程里的,也就是说如果你在Service里编写了非常耗时的代码,程序必定会出现ANR的。
你可能会惊呼,这不是坑爹么!?那我要Service又有何用呢?其实大家不要把后台和子线程联系在一起就行了,这是两个完全不同的概念。Android的后台就是指,它的运行是完全不依赖UI的。即使Activity被销毁,或者程序被关闭,只要进程还在,Service就可以继续运行。比如说一些应用程序,始终需要与服务器之间始终保持着心跳连接,就可以使用Service来实现。你可能又会问,前面不是刚刚验证过Service是运行在主线程里的么?在这里一直执行着心跳连接,难道就不会阻塞主线程的运行吗?当然会,但是我们可以在Service中再创建一个子线程,然后在这里去处理耗时逻辑就没问题了。
额,既然在Service里也要创建一个子线程,那为什么不直接在Activity里创建呢?这是因为Activity很难对Thread进行控制,当Activity被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个Activity中创建的子线程,另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。因此,使用Service来处理后台任务,Activity就可以放心地finish,完全不需要担心无法对后台任务进行控制的情况。
一个比较标准的Service就可以写成:
把Service放到前台来
Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。前台Service和普通Service最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。当然有时候你也可能不仅仅是为了防止Service被回收才使用前台Service,有些项目由于特殊的需求会要求必须使用前台Service,比如说墨迹天气,它的Service在后台更新天气数据的同时,还会在系统状态栏一直显示当前天气的信息。
ok,现在到重点了,如何才能创建一个前台Service呢?其实并不复杂,主要是结合通知栏,直接上代码,如下所示:
修改了ExampleService中onCreate()方法的代码。可以看到,这里设置了点击通知后就打开MainActivity。然后调用startForeground()方法就可以让MyService变成一个前台Service,并会将通知的图片显示出来。
现在重新运行一下程序,并点击Start Service或Bind Service按钮,MyService就会以前台Service的模式启动了,并且在系统状态栏会弹出一个通栏图标,下拉状态栏后可以看到通知的详细内容,如下图所示。
然后,运行后会看到在通知栏多了我们的服务,

好了,现在我们已经把关于Service的很多重要知识点都梳理了一遍,基本上可以开始用起来了,但是服务很重要的特性在于其多进程适用,接下来开始捋多进程方面。
多进程
很高兴各位看到这啦,我们可以在这里开始更深层次的探索啦,多进程! Let’s go~
来干一件傻逼的事情
先来问一下,想让小绿机器人说我没反应了是多少秒?1?2?4?8?好了,别猜了,多了多了,只要5秒。
只要5秒,安卓就会抛出ANR,从前面我们知道,Service其实是运行在主线程里的,如果直接在Service中处理一些耗时的逻辑,就会导致程序ANR。
让我们来做个实验验证一下吧,修改前面的代码,在ExampleService的onCreate()方法中让线程睡眠10秒,如下所示:
好了,我的手机跪了,图就不贴了,截屏真的很累的,上面的图我都搞够了,但是我们等一会儿,选择等待后,还是又活过来了。
之前我们提到过,应该在Service中开启线程去执行耗时任务,这样就可以有效地避免ANR的出现。
如果将MyService转换成一个远程Service,还会不会有ANR的情况呢?让我们来动手尝试一下吧。将一个普通的Service转换成远程Service其实非常简单,只需要在注册Service的时候将它的android:process属性指定成:remote就可以了,代码如下所示:
现在重新运行程序,并点击一下Start Service按钮,你会看到控制台立刻打印了onCreate() executed的信息,而且主界面并没有阻塞住,也不会出现ANR。大概过了一分钟后,又会看到onStartCommand() executed打印了出来。
为什么将ExampleService转换成远程Service后就不会导致程序ANR了呢?这是由于,使用了远程Service后,ExampleService已经在另外一个进程当中运行了,所以只会阻塞该进程中的主线程,并不会影响到当前的应用程序。
为了证实一下ExampleService现在确实已经运行在另外一个进程当中了,我们分别在MainActivity的onCreate()方法和ExampleService的onCreate()方法里加入一行日志,打印出各自所在的进程id,结果得到分别是31029和32009。
可以看到,不仅仅是进程id不同了,就连应用程序包名也不一样了,ExampleService中打印的那条日志,包名后面还跟上了:remote标识。
那既然远程Service这么好用,干脆以后我们把所有的Service都转换成远程Service吧,还省得再开启线程了。其实不然,远程Service非但不好用,甚至可以称得上是较为难用。一般情况下如果可以不使用远程Service,就尽量不要使用它。
下面就来看一下它的弊端吧,首先将ExampleService的onCreate()方法中让线程睡眠的代码去除掉,然后重新运行程序,并点击一下绑定服务按钮,你会发现程序崩溃了!为什么点击启动服务按钮程序就不会崩溃,而点击绑定服务按钮就会崩溃呢?这是由于在绑定服务按钮的点击事件里面我们会让MainActivity和ExampleService建立关联,但是目前ExampleService已经是一个远程Service了,Activity和Service运行在两个不同的进程当中,这时就不能再使用传统的建立关联的方式,程序也就崩溃了。
那么如何才能让Activity与一个远程Service建立关联呢?这就要使用AIDL来进行跨进程通信了(IPC)。
AIDL(Android Interface Definition Language)是Android接口定义语言的意思,它可以用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。
下面我们就来一步步地看一下AIDL的用法到底是怎样的。首先需要新建一个AIDL文件,在这个文件中定义好Activity需要与Service进行通信的方法。新建ExampleAIDLService.aidl文件,代码如下所示:
保存并build一下,发现我们的gen/R文件目录下多了一个类,ExampleAIDLService.java,修改一下我们的ExampleService,代码如下:
Activity的代码:
注册到manifest文件:
点击测试,得到如下结果:

Oh,shit,我似乎成功了,没错,我就是成功了,不过,这好像没什么吊的嘛,自己App调自己App的方法,有什么骄傲的。把脸凑过来,我们现在是跨进程哎,那就意味着,我们可以在其他的App中调用我们的服务了。
接下里我们要写一个ClientApp来测试,既然要和我通信,那么请按照我的规则来,那什么是规则呢,好吧,就是我们定义的AIDL文件,当然你可以牛逼的去手写,但是如果你不是很执着于这种当疼的事情的话,我还是建议拷贝吧。
ClientApp和主App通信
既然要和另外一个App去沟通服务,那么总得有别人的唯一的标识吧,是的,我们需要在主App的服务声明中去添加一个filter,代码如下:
adb shell dumpsys activity services -> 查看应用产生的服务
adb shell service list -> 查看系统服务
好了,我们在ClientApp中也去构造同样的服务,布局什么的都一样,最后如下:
在Android 5.0以下的机器上可以正常的看到结果,但是5.0以上不能够使用隐式Intent,不能远程调用,发现onServiceConnected()回调接口并没有触发,怎样去解决这个问题,我正在查找方案,如果你知道,请告诉我。
好了,就是这么简单,如果我们调用adb shell dumpsys activity services能够看到响应的服务,那么我们就可以玩转啦,好了,写到这里基本上大家对service有一定的了解了吧。
总结
不过还有一点需要说明的是,由于这是在不同的进程之间传递数据,Android对这类数据的格式支持是非常有限的,基本上只能传递Java的基本数据类型、字符串、List或Map等。那么如果我想传递一个自定义的类该怎么办呢?这就必须要让这个类去实现Parcelable接口,并且要给这个类也定义一个同名的AIDL文件。这部分内容并不复杂,而且和Service关系不大。
不想敲代码就到我的git来拉一下吧~https://github.com/gordon-rawe/understandService