【前言】
花了周末两天的时间,整理了一下作为Android四大组件之一的Service的基础知识,通过这篇文章,应该可以明白:对Service的理解、在什么地方使用、怎么使用、要注意哪些问题等。
【本文主要内容】
一、Service的基本概念(四大组件之一)
二、定义(启动)一个Service
- 1、如何定义(启动)一个Service:
- 2、停止一个started服务有两种方法
- 3、onStartCommand方法的返回值
三、IntentService
- 1、IntentService的引入
- 2、IntentService的作用
- 3、IntentService的用法
- 4、Service和Thread的关系
四、使用Bind Service完成Service和Activity之间的通信
- 1、Bind Service的介绍
- 2、实现Service和Activity之间通信步骤
- 3、started服务与bind服务的区别
- 4、Service的生命周期
五、使用Bind Service完成IPC进程间通信:(在同一个APP内模拟)
- 1、Bind Service的介绍
- 2、在客户端绑定一个服务的步骤
- 3、IPC(Inter-Process Communication)进程间通信机制
- 4、AIDL(Android Interface Definition Language)Android接口定义语言
- 5、IPC(进程间通讯)具体的步骤如下
- 6、让Activity与一个远程Service建立关联的步骤:(在同一个APP内模拟)
- 7、AIDL支持的自定义数据类型
六、使用Bind Service完成IPC进程间通信:(两个APP之间)
七、Messenger的使用
【正文】
一、Service的基本概念(四大组件之一)
Service是Android中实现程序后台运行的解决方案,非常适合用于去执行哪些不需要和用户交互而且还要求长期运行的任务。不能运行在一个独立的进程当中,而是依赖与创建服务时所在的应用程序进程。只能在后台运行,并且可以和其他组件进行交互。
Service可以在很多场合使用,比如播放多媒体的时候用户启动了其他Activity,此时要在后台继续播放;比如检测SD卡上文件的变化;比如在后台记录你的地理信息位置的改变等等,总之服务是藏在后台的。
服务不会自动开启线程,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务。关于多线程的知识:可以参考另外一篇文章:Android多线程----异步消息处理机制之Handler详解
二、定义(启动)一个Service:
1、如何定义(启动)一个Service:
核心步骤如下:
- 创建一个类继承android.app.Service类,实现抽象方法onBind(),重写onCreate()、onStartCommand()、onDestry();
- 在清单文件中配置Service。
新建一个Android项目ServiceTest,具体步骤如下:
(1)新建一个MyService类,继承自Service,并重写父类的onCreate()、onStartCommand()和onDestroy()方法,代码如下:
可以看到,我们只是在onCreate()、onStartCommand()和onDestroy()方法中分别打印了一句话,并没有进行其它任何的操作,注意代码注释中这三个方法的作用。
onBind()方法是Service中唯一的一个抽象方法,所以必须要在子类里实现。我们知道,Service可以有两种启动方式:一种是startService(),另一种是bindService()。第二种启动方式才会用到onBind()方法。我们这先用第一种方式启动Service,所以暂时忽略onBind()方法。
(2)在清单文件中声明:(和Activity标签并列)
(3)修改activity_main.xml代码,如下:
我们在布局文件中加入了两个按钮,一个用于启动Service,一个用于停止Service。
(4)在MainActivity作为程序的主Activity,在里面加入启动Service和停止Service的逻辑,代码如下:
核心代码:31行至32行、35行至36行。
可以看到,在Start Service按钮的点击事件里,我们构建出了一个Intent对象,并调用startService()方法来启动MyService。然后在Stop Serivce按钮的点击事件里,我们同样构建出了一个Intent对象,并调用stopService()方法来停止MyService。代码的逻辑非常简单。
这样的话,一个简单的带有Service功能的程序就写好了。
启动和停止服务:
定义好服务之后,接下来看一下如何启动和停止一个服务,这主要是借助Intent来实现的。注意startService()和stopService()方法都是定义在Context类当中的,所以可以在MainActivity中直接调用这两个方法。
运行上面的程序,点击button1_start_service按钮,启动服务,后台打印日志如下:
说明服务启动成功。
那么如果我再连续点三次button1_start_service按钮,后台增加的日志如下:
事实上,onCreate()方法只会在Service第一次被创建的时候调用,而onStartCommand()方法在每次启动服务的时候都会调用。
我们还可以在正在“设置--应用---运行”中找到这个服务,如下图所示:
点开上图中的红框部分,可以看到:
如果我们再点击button2_stop_service按钮或者点击上图中的“Stop”,MyService服务就停止掉了:
需要注意的是:
- 服务对象同时只会有一个
- 默认情况下,一个started的Service与启动他的组件在同一个线程中。上面的实例中,服务就是在主线程中运行的,如果是在服务中完成耗时操作的话,容易造成主线程阻塞。
2、停止一个started服务有两种方法:
(1)在外部使用stopService()
(2)在服务内部(onStartCommand方法内部)使用stopSelf()方法。
3、onStartCommand方法的返回值:
onStartCommand方法执行时,返回的是一个int型。这个整型可以有三个返回值:START_NOT_STICKY、START_STICKY、START_REDELIVER_INTENT
- START_NOT_STICKY:“非粘性的”。使用这个返回值时,如果在执行完onStartCommand方法后,服务被异常kill掉,系统不会自动重启该服务。
- START_STICKY:如果Service进程被kill掉,保留Service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试重新创建Service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。如果在此期间没有任何启动命令被传递到Service,那么参数Intent将为null。
- START_REDELIVER_INTENT:重传Intent。使用这个返回值时,系统会自动重启该服务,并将Intent的值传入。
三、IntentService
1、IntentService的引入:
我们在第一段中就已经说了,服务中的代码默认运行在主线程中,如果直接在服务里执行一些耗时操作,容易造成ANR(Application Not Responding)异常,所以就需要用到多线程的知识了。
因此一个比较标准的服务可以这样写:
核心代码:14至19行,在子线程中处理具体的逻辑。
需要注意的是,如果没有第17行的stopSelf(),服务一旦启动后,就会一直处于运行状态,必须调用stopService()或者stopSelf()方法才能让服务停止下来;所以我们添加了17行的stopSelf(),服务执行完毕后会自动停止。
虽说上面的这种写法并不复杂,但总会有一些程序猿忘记开启线程,或者忘记调用stopSelf()方法。为了可以简单地创建一个异步的、会自动停止的服务,Android专门提供了一个IntentService类,这个类就很好的解决了上面所提到的两种尴尬。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent()回调方法中执行,并且每次只会执行一个工作线程,执行完第一个后,再执行第二个,以此类推。
2、IntentService的作用:
当我们需要这样一次性完成的任务时,就可以使用IntentService来完成。
3、IntentService的用法:
我们在上面的项目ServiceTest基础上进行修改,步骤如下:
(1)新建一个MyIntentService类,继承自IntentService,并重写父类的onHandleIntent()方法,代码如下:
这里首先要提供一个无参的构造方法,并且必须在其内部调用父类的有参构造方法(9至12行),我们在第10行手动将服务的名字改为“MyIntentService”。
然后在子类中实现onHandleIntent()这个抽象方法,可以在这个方法里去处理一些具体的逻辑,我们就用三次for循环,打印当前线程的id,每次延时1秒。
因为这个服务在运行结束后会自动停止,所以我们在onDestroy()方法中打印日志验证一下。
(2)在清单文件中对服务进行注册服务:
(3)在activity_main.xml中添加一个按钮button3_stop_intentservice,用于启动MyIntentService服务,代码略。
(4)在MainActivity里面加入启动IntentService的逻辑,核心代码如下:
我们在第02行中,打印主线程的id。
运行程序,点击按钮button3_stop_intentservice,显示如下:
由此可见,启动一个IntentService和启动一个普通的Service,步骤是一样的。
4、Service和Thread的关系:
不少Android初学者都可能会有这样的疑惑,Service和Thread到底有什么关系呢?什么时候应该用Service,什么时候又应该用Thread?答案可能会有点让你吃惊,因为Service和Thread之间没有任何关系!
之所以有不少人会把它们联系起来,主要就是因为Service的后台概念。Thread我们大家都知道,是用于开启一个子线程,在这里去执行一些耗时操作就不会阻塞主线程的运行。而Service我们最初理解的时候,总会觉得它是用来处理一些后台任务的,一些比较耗时的操作也可以放在这里运行,这就会让人产生混淆了。但是,如果我告诉你Service其实是运行在主线程里的,你还会觉得它和Thread有什么关系吗?
其实,后台和子线程是两个完全不同的概念:
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,就可以写成本段中第1节的样子。
四、使用Bind Service完成Service和Activity之间的通信
有没有什么办法能让它们俩的关联更多一些呢?比如说在Activity中指挥Service去干什么,Service就去干什么。当然可以,只需要让Activity和Service建立关联就好了。
1、Bind Service的介绍:
应用程序组件(客户端)通过调用bindService()方法能够绑定服务,然后Android系统会调用服务的onBind()回调方法,则个方法会返回一个跟服务器端交互的Binder对象。
这个绑定是异步的,bindService()方法立即返回,并且不给客户端返回IBinder对象。要接收IBinder对象,客户端必须创建一个ServiceConnection类的实例,并且把这个实例传递给bindService()方法。ServiceConnection对象包含了一个系统调用的传递IBinder对象的回调方法。
注意:只有Activity、Service、Content Provider能够绑定服务;BroadcastReceiver广播接收器不能绑定服务。
2、实现Service和Activity之间通信步骤:
我们依然在第二段中的项目ServiceTest基础上进行修改。
观察上面第二段中MyService中的代码,你会发现一直有一个onBind()方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的,修改MyService中的代码,如下所示:
38至50行:新建一个MyBinder类,继承Binder:让里面的方法执行下载任务,并获取下载进度。当然,这里只是两个模拟方法,并没有实现真正的功能,我们通过打印日志的形式来体现。
接着创建MyBinder的实例(13行),然后在onBind()方法里返回这个实例(35行)。
核心代码是35行,返回这个mBinder,是一个IBinder类型,就可以把这个IBinder类型传递到MainActivity中,从而调用Service里面的方法。下面就要看一看,在MainActivity是如何调用Service里面的两个方法的。
(2)检查清单文件,是否已经对Service进行注册:
(3)在activity_main.xml中继续添加两个按钮button3_bind_service和button4_unbind_service,用于绑定服务和取消绑定服务。最终,activity_main.xml的完整代码如下:
(4)接下来再修改MainActivity中的代码,让MainActivity和MyService之间建立关联,代码如下所示:
可以看到,这里我们首先创建了一个ServiceConnection的匿名类(24行),在里面重写了onServiceConnected()方法和onServiceDisconnected()方法,如果当前Activity与服务连接成功后,服务会回调onServiceConnected()方法,
在onServiceConnected()方法中,我们又通过向下转型得到了MyBinder的实例(34行),有了这个实例,Activity和Service之间的关系就变得非常紧密了。现在我们可以在Activity中根据具体的场景来调用MyBinder中的任何public方法(36、37行),即实现了Activity指挥Service干什么Service就去干什么的功能。
当然,现在Activity和Service其实还没关联起来了呢,这个功能是在Bind Service按钮的点击事件里完成的。可以看到,这里我们仍然是构建出了一个Intent对象,然后调用bindService()方法将Activity和Service进行绑定。bindService()方法接收三个参数,第一个参数就是刚刚构建出的Intent对象,第二个参数是前面创建出的ServiceConnection的实例,第三个参数是一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service建立关联后会自动创建Service(即使之前没有创建Service也没有关系),这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。
然后如何我们想解除Activity和Service之间的关联怎么办呢?调用一下unbindService()方法就可以了,这也是Unbind Service按钮的点击事件里实现的逻辑。
现在让我们重新运行一下程序吧,在MainActivity中点击一下Bind Service按钮,LogCat里的打印日志如下图所示:
可以看到,只点击了Bind Service按钮,但是oncreate()方法得到了执行,而onStartCommand()方法不会执行。
另外需要注意,任何一个Service在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity建立关联,还可以和任何一个Activity建立关联,而且在建立关联时它们都可以获取到相同的MyBinder实例。
如何销毁Service:
根据上面第一段的知识,我们介绍了销毁Service最简单的一种情况:现在卸载程序,重新运行程序,点击Start Service按钮启动Service,再点击Stop Service按钮停止Service,这样MyService就被销毁了:
现在回到本段内容。卸载程序,重新开始。那么如果我们只点击的Bind Service按钮呢?由于在绑定Service的时候指定的标志位是BIND_AUTO_CREATE,说明点击Bind Service按钮的时候Service也会被创建,这时应该怎么销毁Service呢?其实也很简单,点击一下Unbind Service按钮,将Activity和Service的关联解除就可以了:
以上这两种销毁的方式都很好理解。那么如果我们既点击了Start Service按钮,又点击了Bind Service按钮会怎么样呢?这个时候你会发现,不管你是单独点击Stop Service按钮还是Unbind Service按钮,Service都不会被销毁,必要将Unbind Service按钮和Stop Service按钮都点击一下(没有先后顺序),Service才会被销毁。也就是说,点击Stop Service按钮只会让Service停止,点击Unbind Service按钮只会让Service和Activity解除关联,一个Service必须要在既没有和任何Activity关联又处理停止状态的时候才会被销毁。
点击Unbind Service按钮后,再次点击Unbind Service按钮按钮引发的问题:
假设现在Service和Activity已经相关联了,点击Unbind Service按钮能够解除绑定,如果继续点击Unbind Service按钮,程序会异常退出,这说明代码不够完善,我们需要在代码中加一个判断是否绑定的标记mBound。在改MainActivity中增加一部分代码,最终改MainActivity的完整代码如下:(加粗字体是添加的内容)
添加的代码是第21行、29行、72行至74行。
这样的话,连续点击Unbind Service按钮,就不会使程序出现异常。
3、started服务与bind服务的区别:
区别一:生命周期
- 通过started方式的服务会一直运行在后台,需要由组件本身或外部组件来停止服务才会以结束运行
- bind方式的服务,生命周期就要依赖绑定的组件
区别二:参数传递
- started服务可以给启动的服务对象传递参数,但无法获取服务中方法的返回值
- bind服务可以给启动的服务对象传递参数,也可以通过绑定的业务对象获取返回结果
实际开发中的技巧;
- 第一次先使用started方式来启动一个服务
- 之后可以使用bind的方式绑定服务,从而可以直接调用业务方法获取返回值
4、Service的生命周期:
一旦在项目的任何位置调用了Context的startService()方法,相应的服务就会启动起来,并回调onstartCommand()方法。如果这个服务之前还没有创建过,onCreate()方法会先于onstartCommand()方法执行。服务启动过后,会一直保持运行状态,直到stopService()或stopself()方法被调用。注意虽然每次调用一次startService()方法,onstartCommand()方法就会以执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService()或stopself()方法,服务就会停止。
另外,还可以调用Context的bindService()来获取一个服务的持久连接,这时就会回调服务中的onBind()方法。类似地,如果这个服务之前还没有创建过,onCreate()方法会先于onBind()方法执行。之后调用方可以获取到onBind()方法里返回的IBinder对象的实例,这样,就能自由地和服务进行通信了。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。
五、使用Bind Service完成IPC进程间通信:(在同一个APP内模拟)
既然是在在同一个APP内模拟进程间通信,其实就是完成进程内通信,但是原理都是一样的嘛。
也就是说,要实现:让Activity与一个远程Service建立关联,这就要使用AIDL来进行跨进程通信了(IPC)。这里把Bind Service及其他的概念再重复一下:
1、Bind Service的介绍:
应用程序组件(客户端)通过调用bindService()方法能够绑定服务,然后Android系统会调用服务的onBind()回调方法,则个方法会返回一个跟服务器端交互的Binder对象。
这个绑定是异步的,bindService()方法立即返回,并且不给客户端返回IBinder对象。要接收IBinder对象,客户端必须创建一个ServiceConnection类的实例,并且把这个实例传递给bindService()方法。ServiceConnection对象包含了一个系统调用的传递IBinder对象的回调方法。
注意:只有Activity、Service、Content Provider能够绑定服务;BroadcastReceiver广播接收器不能绑定服务。
2、在客户端绑定一个服务的步骤:
(1)实现ServiceConnection抽象类。实现过程中,必须重写一下两个回调方法:
- onServiceConnected() 和服务绑定成功后,系统会调用这个方法来发送由服务的onBind()方法返回的IBinder对象
- onServiceDisconnected() 当服务异常终止时会调用(如服务崩溃或被杀死时)。注意,在客户端解除绑定时不会调用该方法。
(2)调用bindService()方法来传递ServiceConnection类的实现;
(3)当系统调用你的onServiceConnected()回调方法时,你就可以开始使用接口中定义的方法来调用服务了
(4)调用unbindService()方法断开与服务的链接。
注:bindService()和unbindService()方法都是Context类中的方法。
3、IPC(Inter-Process Communication)进程间通信机制:
在同一进程中,各个组件进行通信是十分方便的,普通的函数调用就可以解决;但是对于不同的进程中的组件来说,要进行通信,就需要用到Android的IPC机制了。
对应用开发者来说,Android的IBinder/Binder框架实现了Android的IPC通信。当然,IBinder/Binder框架也可以用来实现进程内通信(本地通信),也可以实现进程间通信(远程通信)
从Android SDK中对IBinder/Binder的解释可知,IBinder/Binder是Android远程对象的基本接口,它是Android用于提供高性能IPC通信而设计的一套轻量级远程调用机制的核心部分。该接口描述了与一个远程对象进行通信的抽象协议。
4、AIDL(Android Interface Definition Language)Android接口定义语言:
AIDL它可以用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。
AIDL支持的类型:八大基本数据类型、String类型、CharSequence、List、Map、自定义。
来看下面的这张原理图:
上图中,如果A应用程序想访问B应用程序中的业务对象,可以先让A绑定B应用中的Service,然后通过Service去访问B中的业务对象。我们可以用AIDL来描述需要被别人调用的接口(即B中的业务对象)。
5、IPC(进程间通讯)具体的步骤如下:
- 使用AIDL定义业务接口,通过ADT工具来生成一个java类,此类实现了进程间远程通讯的代理
- 编写自己的业务类(继承生成的类中的Stub)来实现业务接口功能
- 再通过绑定Service的方式来暴露此业务对象,给其它组件提供功能
- 调用者组件通过bindService方法绑定服务,从而获取绑定成功后的远程业务对象或本地业务对象,然后就可以调用相关功能。
- 注意:一般在使用完绑定服务后,需要解除绑定。
下面就通过代码来实现。
6、让Activity与一个远程Service建立关联的步骤:(在同一个APP内模拟)
新建一个全新的Android工程ServiceTest02。
(1)新建IPerson.aidl文件,代码如下所示:
这个文件里,添加我们需要的业务方法。第01行是包名。注意不要写public等修饰符。(如果这个文件写错了,程序会报错,后面的Java文件也不会自从生成)
文件保存之后,ADT会在gen目录下自动生成一个对应的Java文件,如下图所示:
之后,程序运行的时候使用的是这个Java文件,与aidl文件就没有关系了。
我们来大致分析一下这个自动生成的Java文件。完整版代码如下:
分析:
这个Java文件实际上是一个接口,同时生成了在aidl文件中定义的四个方法,并抛出了远程调用的异常。我们按住Ctrl键,点开上图中蓝框部分的IInterface,查看一下源代码:
可以看到,在IInterface接口里,定义了一个接口IBinder,这是IPC机制的核心接口。
再回来看IPerson.java文件的第9行定义了这样一个抽象类:
上图中的Stub类可以比作存根。Stub类继承了Binder类,同时实现了IPerson接口(没有实现IPerson里的方法)。所以进一步理解为:Stub既是IPerson里的内部类,也是一个IPerson。
(2)新建PersonImpl类,继承IPerson.Stub类,重写父类里的方法。代码如下:(也就是说,根据上面的java类,生成业务对象,即原理图中B应用的业务对象)
(3)新建类MyService,代码如下:
核心代码:12行和35行。
因为PersonImpl类继承了IPerson.Stub,而Stub继承了Binder,Binder又实现了IBinder。所以,PersonImpl可以理解为一个IBinder。于是可以在第35行返回PersonImpl的实例。
(4)在清单文件中添加权限:
现在,B应用的业务对象和服务建立好了。B应用的业务对象通过与Service绑定,让Service把业务对象暴露给了A应用或者其他的应用。也就是说,Service最终并没有实现业务功能。
如果要让A应用来访问,该怎么做呢?
(5)在activity_main.xml中添加两个按钮button_bind_service和button_unbind_service,用于绑定远程服务和取消绑定。activity_main.xml的代码如下:
(6)MainActivity中的代码,如下所示:
核心代码是第38行:可以看到,这里首先使用了MyAIDLService.Stub.asInterface()方法将传入的IBinder对象传换成了IPerson对象,接下来就可以调用在IPerson.aidl文件中定义的所有接口了(41至44行)。调用之后,我们在后台打印输出。
运行程序,点击按钮,效果如下:
由此可见,我们确实已经成功实现跨进程通信了,在一个进程中访问到了另外一个进程中的方法。 注意,这个Service是运行在主线程当中的,毕竟我们是在本地模拟的嘛。
另外注意蓝色箭头处,可以看出,这个person其实就是personImpl,因为是在本地调用。所以说,目前的跨进程通信其实并没有什么实质上的作用,因为这只是在一个Activity里调用了同一个应用程序的Service里的方法。而跨进程通信的真正意义是为了让一个应用程序去访问另一个应用程序中的Service,以实现共享Service的功能。那么下面我们自然要学习一下,如何才能在其它的应用程序中调用到MyService里的方法。
继续回顾第(1)步中自动生成的IPerson.java文件,截取第22至32行,摘抄如下:
代码解释:
上方第06行的iin代表的是,查询本地对象返回的结果。
07行:如果iin不为空,并且iin为IPerson,那就将iin强制转换为IPerson(08行)。很显然,这里是进程内通信(本地通信)要用到的。也就是本段中的例子。
10行:代表的是进程间通信(远程通信),此时第07行的if语句不成立,于是返回第10行的代理对象Proxy(obj)。
实现进程间通信的两个办法:
如果要实现进程间通信,就必须让MainActivity和Service相互独立。有两个办法:
(1)办法一:将本地的Service设置为远程。只需要在清单文件中注册Service的时候将它的android:process属性指定成:remote就可以了,代码如下所示:
后台打印日志如下:
上图的红框部分显示,Service和Activity并非在同一个线程内,连包名都不一样。而IPeron也并非是本地的IPeron。
如果将这种方法应用到上面的第四段中,情形是这样的:
点击button1_start_service,服务启动。既然已经将Service的android:process属性指定成:remote,此时Service和Activity不在同一个线程内,那么即使在Service的onStartCommand()方法中执行耗时操作而不重新开启子线程,程序也不会阻塞。
但是,如果点击button3_bind_service按钮绑定服务,程序会崩溃的。这是因为,目前MyService已经是一个远程Service了,Activity和Service运行在两个不同的进程当中,这时就不能再使用传统的建立关联的方式,程序也就崩溃了。
现在我们总结一下:
第四段中使用的是传统的方式和Service建立关联,默认MainActivity和MyService在同一个线程内,如果将Service的android:process属性指定成:remote,此时MainActivity和MyService将在不同的线程内,但是无法绑定服务。
本段中(第五段)使用的是IPC跨进程通信,MainActivity和MyService在不同的进程中,可以绑定远程服务。
(2)办法二:新建另外一个工程,真正实现远程通信。这就是我们下一段(第六段)要讲的内容。
我们还是先回过头来再巩固一下本段中AIDL的知识吧。
7、AIDL支持的自定义数据类型:
我们在本段中的第4小结讲到,AIDL支持的类型:八大基本数据类型、String类型、CharSequence、List、Map、自定义,那我们就来详细说下这个自定义数据类型。
由于这是在不同的进程之间传递数据,Android对这类数据的格式支持是非常有限的,基本上只能传递Java的基本数据类型、字符串、List或Map等。那么如果我想传递一个自定义的类该怎么办呢?这就必须要让这个类去实现Parcelable接口,并且要给这个类也定义一个同名的AIDL文件进行声明。这部分内容并不复杂,而且和Service关系不大。具体操作如下:
重新建一个工程ServiceTest03。步骤如下:
(1)新建一个Student类去实现Parcelable接口。Student类是作为传递的自定义类:
我们在这个类中放入了name和age这两个参数,并实现了Parcelable接口。注意第44行至55行代码的修改。
接着,新建一个和类同名的aidl文件,即新建Student.aidl,代码如下:
注意这个parcelable的第一个字母是小写。
继续,新建IStudent.aidl,作为需要远程传递的业务方法。代码如下:
核心代码是第03行,虽然Student类文件和本文件是在同一个包下,但是依然要导包,否则将无法识别Student类。然后在第06行代码中,就可以把Student这个类传递出去了。注意了,第06行返回的是Student类型,这不就是ADIL所支持的自定义类型嘛。
文件结构如下:
综上所述,传递自定义类,有三个步骤:
- 自定义类实现Parcelable接口
- 新建同名的aidl文件,声明这个Parcelable类型的自定义类
- 在需要远程传递的aidl文件中导包,引用进来
那么接下来的步骤就和本段中的第6小节一样了,就不再多解释了,这里只贴代码:
(2)新建StudentImpl类,继承IStudent.Stub类。代码如下:(也就是说,根据步骤(1)中的java类,生成业务对象,即原理图中B应用的业务对象)
(3)新建Service类,代码如下:
核心代码:12行、35行、36行。
(4)在清单文件中添加权限:
现在,B应用的业务对象和服务建立好了。B应用的业务对象通过与Service绑定,让Service把业务对象暴露给了A应用或者其他的应用。也就是说,Service最终并没有实现业务功能。
如果要让A应用来访问,该怎么做呢?
(5)在activity_main.xml中添加两个按钮button1_setStudent和button2_getStudent。activity_main.xml的代码如下:
注:布局文件里不再添加绑定服务和取消绑定的按钮,我们稍后在Activity的生命周期里完成这件事。
(6)MainActivity中的代码,如下所示:
核心代码是第44行。
我们在第75行、86至87行使用到了IStudent中的业务方法。
运行程序,点击第一个按钮,然后点击第二个按钮,效果如下:
这样,Acitivity就成功调用了远程Service的自定义类。
六、使用Bind Service完成IPC进程间通信:(两个APP之间)
上一段中的跨进程通信其实并没有什么实质上的作用,因为这只是在一个Activity里调用了同一个应用程序的Service里的方法。而跨进程通信的真正意义是为了让一个应用程序去访问另一个应用程序中的Service,以实现共享Service的功能。那么下面我们自然要学习一下,如何才能在其它的应用程序中调用到MyService里的方法。
在第四段中我们已经知道,如果想要让Activity与Service之间建立关联,需要调用bindService()方法,并将Intent作为参数传递进去,在Intent里指定好要绑定的Service,核心代码如下:
这里在构建Intent的时候是使用MyService.class来指定要绑定哪一个Service的,但是在另一个应用程序中去绑定Service的时候并没有MyService这个类,这时就必须使用到隐式Intent了。
具体步骤如下:
我们在第六段中的MyService02这个工程文件中进行修改。代码实现如下:
(1)现在修改AndroidManifest.xml中的代码,给MyService加上一个action,如下所示:
这就说明,MyService可以响应带有com.example.servicetest02.MyService这个action的Intent。
现在重新运行一下MyService02这个程序,这样就把远程Service端的工作全部完成了。
然后新建一个新的工程,起名为ClientTest,我们就尝试在这个程序中远程调用MyService中的方法。
ClientTest中的Activity如果想要和MyService建立关联其实也不难,首先需要将IPerson.aidl文件从ServiceTest02项目中拷贝过来,注意要将原有的包路径一起拷贝过来,完成后项目的结构如下图所示:
(2)在activity_main.xml中添加两个按钮button_bind_service和button_unbind_service,用于绑定远程服务和取消绑定。activity_main.xml的代码如下:
(3)在MainActivity中加入和远程的MyService建立关联的代码,如下所示:
这部分代码大家一定会非常眼熟吧?没错,这和在ServiceTest02的MainActivity中的代码几乎是完全相同的,只是在让Activity和Service建立关联的时候我们使用了隐式Intent,将Intent的action指定成了com.example.servicetest02.MyAIDLService(63行)。
在当前Activity和MyService建立关联之后,我们仍然是调用了setName、setAge、setSex、getPerson()这几个方法,远程的MyService会对传入的参数进行处理并返回结果,然后将结果打印出来。
这样的话,ClientTest中的代码也就全部完成了,现在运行一下这个项目,然后点击Bind Service按钮,此时就会去和远程的MyService建立关联,观察LogCat中的打印信息如下所示:
注意红框部分,包名是不一样的哦。由此可见,我们确实已经成功实现跨进程通信了,在一个程序中访问到了另外一个程序中的方法。
七、Messenger的使用:
介绍:Messenger实现了IPC通信,底层也是使用了AIDL方式。和AIDL方式不同的是,Messenger方式是利用Handler形式处理,因此,它是线程安全的,这也表示它不支持并发处理;而AIDL方式是非线程安全的,支持并发处理,因此,我们使用AIDL方式时,需要保证代码的线程安全。大部分情况下,应用中不需要并发处理,因此我们通常只需要使用Messenger方式。
过程:在进程A中创建一个Message,将这个Message对象通过Messenger.send(message)方法传递到进程B的消息队列里,然后交给Handler去处理。
当然,Message对象本身是无法被传递到进程B的,send(message)方法会使用一个Pacel对象对Message对象编集,再将Pacel对象传递到进程B中,然后解编集,得到一个和进程A中的Message对象内容一样的对象。
关于多线程的Handler机制,如果不清楚的话,可以参考本人另外一篇博客:
使用Messenger来实现IPC的步骤:
- 在Service中创建一个Messenger对象并绑定一个Handler
- 在onBind方法中通过Messenger.getIbinder方法返回一个IBinder对象。
- 在调用的组件中的ServiceConnection的onServiceConnected事件方法中根据iBinder对象来创建一个Messenger对象。这样,两个Messenger就同时绑定到一个IBinder上,从而实现通信。
- 在调用的组件中使用Messenger的send方法来发送消息到Service的Messenger对象中。
那我们通过代码来实现以下吧。新建一个全新的工程MessengerTest。步骤如下:
(1)新建一个MessengerService类,继承Service类,代码如下:
核心代码:16至28行、31行、37行。
37行中,将IBinder类型返回之后,就已经和Messenger进行绑定了。
(2)在清单文件中注册服务:(和Activity标签并列)
(3)修改activity_main.xml代码,添加一个按钮,用于发送Message,代码如下:
(4)在MainActivity作为程序的主Activity,在里面加入发送Message消息和建立Service连接的逻辑,代码如下:
我们在上一步的MessengerService类新建了一个Messenger,在这里又新建另一个Messenger(54行)。两个Messenger绑定了同一个服务,Activity就可以和Service实现通讯了。
点击按钮(72行),发送消息,让MessengerService类里的Messenger去接收,然后交给handler去处理,从而执行handleMessage()里方法,也就是说,执行了Service里面的方法。
运行程序,点击按钮,显示效果如下:
后台打印日志如下:
说明这个MessengerService和普通Service一样,也是运行在主线程当中的。
当然了,这里的Messenger的实现比较简单,如果以后需要实现复杂IPC访问,还是需要自己去写AIDL才更加直接有效,可控性强。