关于Android Service的全面解析

今日科技快讯

据媒体报道,2017年第四季度,Google在北京亚奥板块的中海国际中心就已经租下6000平米的办公区,而这正是为谷歌AI中国中心开展工作做好准备。此前曾有消息称谷歌正在北京建立机器学习的核心团队,包括机器学习技术主管、机器学习研究员、机器学习软件工程师、云端机器学习产品经理等。

作者简介

欢迎回到星期一,新的一周也要做好防寒准备。

本篇文章来自阴月有晴_的投稿。主要介绍了Android中Service使用相关知识,希望对大家有所帮助!

阴月有晴_的博客地址:

https://www.jianshu.com/u/11893317ab1d

Service基本用法

基本用法即同进程下Activity与Service双向通信,先描述整体实现过程然后直接上代码:

新建一个继承自Service的类MyService,然后在AndroidManifest.xml里注册这个Service

Activity里面使用bindService方式启动MyService,也就是绑定了MyService(到这里实现了绑定,Activity与Service通信的话继续下面的步骤)

新建一个继承自Binder的类MyBinder

在MyService里实例化一个MyBinder对象mBinder,并在onBind回调方法里面返回这个mBinder对象

第2步bindService方法需要一个ServiceConnection类型的参数,在ServiceConnection里可以取到一个IBinder对象,就是第4步onBinder返回的mBinder对象(也就是在Activity里面拿到了Service里面的mBinder对象)

在Activity里面拿到mBinder之后就可以调用这个binder里面的方法了(也就是可以给Service发消息了),需要什么方法在MyBinder类里面定义实现就行了。如果需要Service给Activity发消息的话,通过这个binder注册一个自定义回调即可。

代码如下,关键部分给出了对应上面步骤的注释:

Activity

Service

Binder

代码很简单,首先Activity绑定Service得到一个MyBinder实例并注册MyBinder里面的OnTestListener回调监听,然后点击按钮的时候调用MyBinder里面的testMethod(String)方法将消息发出去,MyBinder持有一个MyService的实例,testMethod(String)里面调用MyService里面的方法就可以把Activity的消息传给Service了,然后testMethod(String)里面回调mListener.onTest(String)将Service的消息发给Activity。

MyBinder定义在MyService里面作为内部类也是很常见的写法,这里为了方便后面的讲解写成了普通类的形式。

至此就实现了同进程下Activity与Service的双向通信,运行代码,点击按钮后log如下:

通过代码可以看到,Activity和Service之间是通过一个binder对象来通信的。

AIDL实现跨进程通信

上面讲了Activity和Service在同进程下的通信,结论是:Activity和Service之间是通过一个binder对象来通信的,其实,这句话在多进程中同样有效,接下来就在多进程下验证这句话。到这你可能已经想到了,AIDL其实就是利用Binder实现跨进程通信的。先看一下官方文档是如何介绍AIDL的:

On Android, one process cannot normally access the memory of another process. So to talk, they need to decompose their objects into primitives that the operating system can understand, and marshall the objects across that boundary for you. The code to do that marshalling is tedious to write, so Android handles it for you with AIDL.

大概意思就是说Android进程之间不能直接通信,需要把对象转换成计算机能识别的原始语言,然后安排它跨越进程边界。但是做这些事很繁琐,于是Android提供了AIDL来做这件事。(换句话就是要实现跨进程需要编写很多复杂的代码,于是android提供了AIDL,通过编写简单的AIDL文件,编译器根据AIDL的规则生成那些复杂的代码)

总的来说,使用AIDL跨进程通信,整体过程和单进程一样,都是通过一个Binder来通信的,区别在于单进程的Binder是自己通过继承Binder类来手动实现的,而跨进程的Binder是通过AIDL自动生成的,那是一个牛逼的Binder。

对AIDL有个初步认识之后,开始实践,这里使用AndroidStudio实现AIDL,参考下面文章:

http://blog.csdn.net/lambert_a/article/details/51567773

新建一个AIDL文件

和新建类文件相似:右键 -> new -> AIDL -> AIDL File,然后输入文件名点击finish完成(这里的示例代码是IMyAidlInterface)

上面的操作不管右键哪个目录,完成之后都会在src/main目录下生成了一个aidl目录,新建的IMyAidlInterface.aidl文件就在这个目录下,注意和eclipse的不同。

打开这个文件发现就是一个接口(可能会默认生成一个basicTypes方法,这是示例方法,不用管,可以删掉),然后在里面定义一个自己的方法(需要其他的方法的话自己看着加)

代码如下:

编译项目

Build -> Make Project

完成之后会在 app/build/generated/source/debug/ 目录下生成一个和AIDL文件同名的java文件 IMyAidlInterface.java

这个类文件就是用来提供进程间通信的,需要的Binder类就在这里面。

简单来说,AIDL就是一个用来生成代码的工具,最终的目的就是得到IMyAidlInterface.java这个类。这个和数据库框架GreenDao很像,都是通过一些简单的做法生成很多复杂而有用的代码,然后拿来直接用。当然那些复杂的代码也是可以手动编写的,比如可以尝试仿照IMyAidlInterface.java或者直接把IMyAidlInterface.java复制到java目录然后删掉aidl文件实现进程间通信。

分析 IMyAidlInterface.java

AndroidStudio切换到Project工程模式在app/build/generated/source/debug/路径下找到IMyAidlInterface.java文件并打开。生成的代码格式很乱,为了方便查看,可以使用格式化代码的快捷键格式化一下。

IMyAidlInterface.java里面是一个接口,接口里面有一个内部抽象类和一个方法。这个方法就是我们在aidl文件里定义的那个方法。内部抽象类就是我们要的Binder类,类名Stub。到这里不难想象接下来的工作:(1)Service里面new一个Stub实例并在onBinder里面返回这个Stub(或者说Binder)的实例 。(2)Activity里面绑定Service的时候取到这个Binder(强转成Stub类型)。(3)调用这个Binder里面的testMethod方法实现Activity和Service的通信。大的思路是这样,不过细节上还是有很多不同的。

IMyAidlInterface.java里面的其他代码(主要是一些方法)暂时不用看,用到的时候会说。到这里只需要知道这个java文件里面有一个Stub类,有一个自定义的方法。

修改Service代码

到这AIDL相关的代码已经完成,接下来就是使用AIDL为我们生成的代码。首先修改MyService只需把MyBinder替换成Stub,但是Stub是个抽象类,需要我们自己实现,那么新建一个继承自Stub的类,类名随意,这里取名AidlBinder,然后仿照同进程下的MyBinder实现testMethod()方法,代码如下:

上面代码中没有回调相关的代码,因为跨进程的回调和同进程下是不一样的,后面会说到。另外,这里为了方便讲解,专门定义了AidlBinder类作为Stub 的实现类,另一种在Service里面直接使用匿名内部类的方式实现Stub 也是很常见的。至于AidlBinder里面的代码和同进程下很像,不解释了。

然后MyService里面使用AidlBinder即可,代码如下:

和同进程基本一样,不解释了。

总结一下:到这里为止,除去理论的分析之外,实际的操作只有两步:(1)新建一个AIDL文件用来生成一些代码。(2)实现抽象类Stub,实现类是AidlBinder。(3)Service里面使用Stub的实现类AidlBinder替换原来的MyBinder。

修改Activity代码

先来分析一下,按照同进程通信的思路就是:声明一个IMyAidlInterface.Stub类型的Binder,然后在绑定Service的时候初始化这个Binder:mBinder = (IMyAidlInterface.Stub)service; 然后使用这个Binder来跟Service通信。

意思是传过来的Binder是BinderProxy类型的不能转换成Stub类型(因为Stub不是BinderProxy的子类而是Binder的子类)。

关于BinderProxy,我也不懂,通过一些资料了解到它与C++层有关,源码中无对应的java类,编译源码后会生成BinderProxy.class类,和Binder一样实现了IBinder接口。

Activit如何使用传过来的Binder呢?AIDL生成的代码中提供了一个静态方法asInterface(IBinder),可以将IBinder转换成Aidl接口,所以可以这样做:IMyAidlInterface mService = IMyAidlInterface.Stub.asInterface(service);

艺术探索这本书中是这样介绍asInterface方法的:用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。

所以同进程下,Activity有以下3种方式使用Service传过来的Binder:

IMyAidlInterface mService = IMyAidlInterface.Stub.asInterface(service);

IMyAidlInterface.Stub mBinder = (IMyAidlInterface.Stub)service;

IMyAidlInterface.Stub mService = (IMyAidlInterface.Stub)IMyAidlInterface.Stub.asInterface(service);

而跨进程只能使用第一种方式,最终Activity的代码如下:

跨进程回调接口的实现

至此,实现了跨进程Activity给Service发送消息,接下来实现Service收到消息后回应Activity。大的方向还是和单进程一样使用回调实现,不一样的是细节。

首先,回调接口需要定义成aidl接口而不是普通接口,所以新建一个IMyCallbackListener.aidl文件,里面定义一个onRespond方法作为回调函数:

扩展IMyAidlInterface.aidl,里面定义一个注册回调监听的方法(相当于基础篇里面的那个setOnTestListener方法)

注意aidl的语法规则,非系统的类即使在同一个包下也要import,比如上面代码的IMyCallbackListener,而系统的类String就不用import

这时编译会提示AidlBinder实现父类的抽象方法registerListener(),仿照同进程下的MyBinder里面的回调相关的代码,修改AidlBinder如下:

有同进程通信的基础,看懂这个代码很容易。然后Activity里面在合适的地方注册回调,用来接收服务端的消息:

至此,跨进程下Activity与Service的双向通信就完成了,运行代码,点击按钮log如下:

就本应用中的代码来看,代码的执行流程和单进程一样,只是一些实现的细节不同。另外,可以使用adb shell ps | grep "本应用的包名"命令查看进程信息,会看到如下两个进程:

跨进程下解注册回调

Service回应Activity消息是通过注册回调接口实现的,接下来介绍解注册,和同进程的解注册不同,多进程需要借助RemoteCallbackList来完成,所以注册回调的相关方法也要改一下,改成使用RemoteCallbackList来注册回调,AidlBinder代码修改如下:

上面代码里的unregisterListener方法像registerListener一样添加进去,并在里面实现解注册的功能。RemoteCallbackList的用法很简单,看代码就行了。

最后在Activity里面加一些测试解注册的代码即可,比如加一个按钮,点击的时候调用远程的解注册方法,下面是Activity里面的最终完整代码:

整个代码最终的功能是:启动Activity的时候绑定Service并注册一个回调,点击 send message 按钮后Activity向Service发送消息"hi, service",然后Service收到消息后log打印 "receive message from activity: hi, service",并恢复一个消息 "hi, activity",Activity收到消息后log打印 "receive message from service: hi, activity"。然后点击unregisterListener按钮解注册回调监听,再点击 send message 后就只打印log "receive message from activity: hi, service",说明解注册成功。

这里总结一下:同进程下自定义MyBinder可以轻松实现Activity与Service通信,跨进程的话需要使用AIDL生成可以跨进程的Binder。至于Activity与Service里面的代码,流程套路基本相同,不相同的只是一些很简单的细节。

通过代码可以看到,Activity和Service之间是通过一个binder对象来通信的。

Binder连接池

通过上面的介绍,不难发现,一个Service对应一个Binder,实际项目总不能把所有逻辑都写在一起的,不同业务逻辑是要分类的,难免会出现多个Binder的情况,总不能一个Binder对应一个Service,这时,就可以使用Binder连接池了。

首先,用一种简单的方式介绍一下什么是Binder连接池:Binder连接池是一种类似设计模式的代码结构。可以直接把它当作一种设计模式来看待。

然后,这种模式要解决的问题:用一个Service管理多个AIDL(或者说管理多个Binder),而不是一个AIDL对应一个Service。

再解释一下,增强理解:我们知道设计模式对于编写代码、实现功能等并不是必须的,但是它有很多优点。Binder连接池也是一样,要实现一个Service管理多个AIDL也可以不使用它。但是它可以让代码结构优雅清晰,使代码维护扩展更加容易等。

简单了解连接池之后,接下来动手实现一个例子。在动手之前先整体了解一下最终的项目的目录结构,看下图:

项目结构图

如图,这里拿动物来举例。下面一步步来实现图中的代码。

首先准备相应的类

新建一个Activity和一个Service,新建多个AIDL文件。

Activity和Service先什么都不用做,它们与要实现的Binder连接池无关,它们只是用来使用Binder连接池的。

新建多个AIDL,文件名如下:

IAnimal.aidl

IBird.aidl

IFish.aidl

IMonkey.aidl

它们的代码如下:

以上代码不难理解,每种动物包含一个它的专有方法,IAnimal接口管理其它三种动物,它里面的方法接收一个参数,这个参数代表动物种类,后面的实现会根据动物种类返回一个对应的动物的Binder。

编译项目

生成AIDL文件对应的Binder,AIDL生成的Binder是抽象类,接下来定义每个抽象Binder的实现类,类名分别为:AnimalBinder.java,BirdBinder.java,FishBinder.java,MonkeyBinder.java。代码如下:

代码很简单,不解释了。有一点要说明一下,AnimalBinder作为管理,和三种动物Binder要区分开,更好的写法是把AnimalBinder写在代表连接池的类BinderPool里面作为内部类(BinderPool类是后面要讲的),那样的话结构上更加好看合理,示例的最终代码是以内部类的方式来写的。

编写连接池代码

连接池就是一个普通的java类,类名随意取,这里取名:BinderPool.java

类里面的代码主要分为几个简单的部分:

给BinderPool.java实现单例模式

绑定一个Service(绑定Service需要的Context是使用它的Activity传过来的)

提供一个queryAnimal方法,根据参数给用户提供不同的binder

以及前面说的把AnimalBinder作为BinderPool的内部类

BinderPool.java的全部代码如下:

根据划分的几个部分来看代码是很容易的,不过有一些细节需要注意:

关于单例的内存泄漏风险,代码里把context成员转换成了Application的context

AIDL是支持并发访问的,代码里在绑定Service的时候使用synchronized和CountDownLatch做了线程同步处理,所以获取BinderPool单例对象的时候不能在主线程里面。

使用Binder连接池

到这里Binder连接池的代码就完成了,主要就是一个BinderPool类,接下来在Service和Activity中使用它。

Service的代码

Activity的代码

通过Service和Activity的代码可以看到,BinderPool使用起来很简单。从使用者的角度来看,Binder连接池就是把应该在Activity里面做的事封装成了BinderPool类,比如绑定Service、客户端通过Binder调用远程服务端的方法等。

测试

通过测试代码可以知道,Service是在Activity中点击按钮的时候通过初始化BinderPool单例对象的时候绑定的(也可以在其他地方初始化BinderPool对象,随意,这里只是一种测试代码,但是不要在主线程里面),所以程序刚运行的时候只有一个Activity所在的进程,点击按钮之后才会开启Service进程。

点击按钮,可以看到打印log

Binder连接池到此结束,主要就是一个BinderPool.java类。

使用Messenger实现跨进程通信

Messenger也是用来做进程间通信的,与AIDL的区别,看官方文档的一段话:

When you need to perform IPC, using a Messenger for your interface is simpler than implementing it with AIDL, because Messenger queues all calls to the service, whereas, a pure AIDL interface sends simultaneous requests to the service, which must then handle multi-threading.

For most applications, the service doesn't need to perform multi-threading, so using a Messenger allows the service to handle one call at a time. If it's important that your service be multi-threaded, then you should use AIDL to define your interface.

意思就是Messenger比AIDL用起来简单,但是如果多个客户端同时给服务发消息的话,Messenger一次只能处理一个消息,而AIDL可以多线程处理。

Messenger本质也是用AIDL实现的,可以浏览下Messenger的源码(只有100多行),会看到一些AIDL相关的东西。

然后简单介绍一下Messenger的使用,首先列一下使用流程:

Service里面实现一个Handler用来接收消息用

使用这个Handler创建一个Messenger对象

使用这个Messenger对象创建一个Binder对象,并在onBind方法返回

Activity里面绑定Service的时候使用传过来的Binder创建一个Messenger对象

Activity里面使用这个Messenger对象给Service发消息

Service里面的Handler收到消息并处理

Activity里面实现一个Handler用来接收Service回复的消息

第5步发送消息的时候消息中携带一个Messenger对象,这个Messenger是用第7步的Handler创建的

第6步Service收到消息的时候取出消息中携带的Messenger

用第9步取出的Messenger给Activity发消息

Activity中第7步的Handler处理Service回复的消息

整个流程和单进程通信的过程很像,都是围绕Binder完成的。上面第7步以后都是Service回复消息相关的。下面直接给出完整代码,注释与上面的流程相对应。

Service代码

Activity代码

需要注意的问题

Messenger发送的消息是Message对象,组装Message消息的时候不要使用Message的obj字段,而是借用Bundle来组装数据。下面是《Android开发艺术探索》里面的一段话:

使用Messenger来传输Message,Message中能使用的载体只有what, arg1, arg2, Bundle以及replyTo。Message中的另一字段obj在同一个进程中很实用,但是在进程间通信的时候,在android2.2以前obj不支持跨进程,即便是2.2以后,也仅仅是系统提供的实现了Parcelable接口的对象才能通过它来传输。这就意味着自定义的Parcelable对象是无法通过obj字段来传输的。

在接收端的代码中,取消息的时候是先从Message里面取出Bundle,然后直接从Bundle取数据。如果数据是自定义的Parcelable对象,是不能直接从Bundle里面取的,需要在取数据之前先给Bundle设置一个ClassLoader。“取数据之前”的意思不单单是指取自定义的Parcelable对象,而是包括基本数据类型和系统提供的Parcelable对象等所有数据之前。示例代码如下:

关于这一点源码里面已经有相关注释说明了,Message类的getData方法注释如下:

总结

文中的示例源码地址为:

https://github.com/developerzjy/ServiceDemo

或者扫一扫关注我的公众号

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180115B031LZ00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券