Android多进程讲解和使用

Jianwei's blog

首页

分类

关于

归档

标签

巧用Android多进程,微信,微博等主流App都在用

发表于2017-02-26|分类于Android|0|阅读次数241

前言

对于进程的概念,来到这里的都是编程修仙之人,就不再啰嗦了,相信大家倒着、跳着、躺着、各种姿势都能背出来。

为什么要使用多进程,一个进程不就可以了吗?

相信很多同学在实际开发中,基本都不会去给app划分进程,而且,在Android中使用多进程,还可能需要编写额外的进程通讯代码,还可能带来额外的Bug,这无疑加大了开发的工作量,在很多创业公司中工期也不允许,这导致了整个app都在一个进程中。

整个app都在一个进程有什么弊端?

在Android中,虚拟机分配给各个进程的运行内存是有限制值的(这个值可以是32M,48M,64M等,根据机型而定),试想一下,如果在app中,增加了一个很常用的图片选择模块用于上传图片或者头像,加载大量Bitmap会使app的内存占用迅速增加,如果你还把查看过的图片缓存在了内存中,那么OOM的风险将会大大增加,如果此时还需要使用WebView加载一波网页,我就问你怕不怕!

微信,微博等主流app是如何解决这些问题的?

微信移动开发团队在《Android内存优化杂谈》一文中就说到:“对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。微信当前也会把它们放在单独的tools进程中”。

下面我们使用adb查看一下微信和微博的进程信息(Android 5.0以下版本可直接在“设置 -> 应用程序”相关条目中查看):

进入adb shell后,使用 “ps | grep 条目名称” 可以过滤出想要查看的进程。

可以看到,微信的确有一个tools进程,而新浪微博也有image相关的进程,而且它们当中还有好些其它的进程,比如微信的push进程,微博的remote进程等,这里可以看出,他们不单单只是把上述的WebView、图库等放到单独的进程,还有推送服务等也是运行在独立的进程中的。一个消息推送服务,为了保证稳定性,可能需要和UI进程分离,分离后即使UI进程退出、Crash或者出现内存消耗过高等情况,仍不影响消息推送服务。

可见,合理使用多进程不仅仅是有多大好处的问题,我个人认为而且是很有必要的。

所以说,我们最好还是根据自身情况,考虑一下是否需要拆分进程。这也是本文的初衷:给大家提供一个多进程的参考思路,在遇到上述问题和场景的时候,可以考虑用多进程的方法来解决问题,又或者,在面试的时候,跟面试官聊到这方面的知识时候也不至于尴尬。

为什么需要“跨进程通讯”?

Android的进程与进程之间通讯,有些不需要我们额外编写通讯代码,例如:把选择图片模块放到独立的进程,我们仍可以使用startActivityForResult方法,将选中的图片放到Bundle中,使用Intent传递即可。(看到这里,你还不打算把你项目的图片选择弄到独立进程么?)

但是对于把“消息推送Service”放到独立的进程,这个业务就稍微复杂点了,这个时候可能会发生Activity跟Service传递对象,调用Service方法等一系列复杂操作。

由于各个进程运行在相对独立的内存空间,所以它们是不能直接通讯的,因为程序里的变量、对象等初始化后都是具有内存地址的,举个简单的例子,读取一个变量的值,本质是找到变量的内存地址,取出存放的值。不同的进程,运行在相互独立的内存(其实就可以理解为两个不同的应用程序),显然不能直接得知对方变量、对象的内存地址,这样的话也自然不能访问对方的变量,对象等。此时两个进程进行交互,就需要使用跨进程通讯的方式去实现。简单说,跨进程通讯就是一种让进程与进程之间可以进行交互的技术。

跨进程通讯的方式有哪些?

四大组件间传递Bundle;

使用文件共享方式,多进程读写一个相同的文件,获取文件内容进行交互;

使用Messenger,一种轻量级的跨进程通讯方案,底层使用AIDL实现(实现比较简单,博主开始本文前也想了一下是否要说一下这个东西,最后还是觉得没有这个必要,Google一下就能解决的问题,就不啰嗦了);

使用AIDL(Android Interface Definition Language),Android接口定义语言,用于定义跨进程通讯的接口;

使用ContentProvider,常用于多进程共享数据,比如系统的相册,音乐等,我们也可以通过ContentProvider访问到;

使用Socket传输数据。

接下来本文将重点介绍使用AIDL进行多进程通讯,因为AIDL是Android提供给我们的标准跨进程通讯API,非常灵活且强大(貌似面试也经常会问到,但是真正用到的也不多…)。上面所说的Messenger也是使用AIDL实现的一种跨进程方式,Messenger顾名思义,就像是一种串行的消息机制,它是一种轻量级的IPC方案,可以在不同进程中传递Message对象,我们在Message中放入需要传递的数据即可轻松实现进程间通讯。但是当我们需要调用服务端方法,或者存在并发请求,那么Messenger就不合适了。而四大组件传递Bundle,这个就不需要解释了,把需要传递的数据,用Intent封装起来传递即可,其它方式不在本文的讨论范围。

下面开始对AIDL的讲解,各位道友准备好渡劫了吗?

使用AIDL实现一个多进程消息推送

像图片选择这样的多进程需求,可能并不需要我们额外编写进程通讯的代码,使用四大组件传输Bundle就行了,但是像推送服务这种需求,进程与进程之间需要高度的交互,此时就绕不过进程通讯这一步了。

下面我们就用即时聊天软件为例,手动去实现一个多进程的推送例子,具体需求如下:

UI和消息推送的Service分两个进程;

UI进程用于展示具体的消息数据,把用户发送的消息,传递到消息Service,然后发送到远程服务器;

Service负责收发消息,并和远程服务器保持长连接,UI进程可通过Service发送消息到远程服务器,Service收到远程服务器消息通知UI进程;

即使UI进程退出了,Service仍需要保持运行,收取服务器消息。

实现思路

先来整理一下实现思路:

创建UI进程(下文统称为客户端);

创建消息Service(下文统称为服务端);

把服务端配置到独立的进程(AndroidManifest.xml中指定process标签);

客户端和服务端进行绑定(bindService);

让客户端和服务端具备交互的能力。(AIDL使用)

例子具体实现

为了阅读方便,下文中代码将省略非重点部分,可以把本文完整代码Clone到本地再看文章:

https://github.com/V1sk/AIDL

Step0. AIDL调用流程概览

开始之前,我们先来概括一下使用AIDL进行多进程调用的整个流程:

客户端使用bindService方法绑定服务端;

服务端在onBind方法返回Binder对象;

客户端拿到服务端返回的Binder对象进行跨进程方法调用;

AIDL调用过程

整个AIDL调用过程概括起来就以上3个步骤,下文中我们使用上面描述的例子,来逐步分解这些步骤,并讲述其中的细节。

Step1.客户端使用bindService方法绑定服务端

1.1 创建客户端和服务端,把服务端配置到另外的进程

创建客户端 -> MainActivity;

创建服务端 -> MessageService;

把服务端配置到另外的进程 -> android:process=”:remote”

上面描述的客户端、服务端、以及把服务端配置到另外进程,体现在AndroidManifest.xml中,如下所示:

android:name=".service.MessageService"

android:enabled="true"

android:exported="true"

android:process=":remote"/>

开启多进程的方法很简单,只需要给四大组件指定android:process标签。

1.2 绑定MessageService到MainActivity

创建MessageService

此时的MessageService就是刚创建的模样,onBind中返回了null,下一步中我们将返回一个可操作的对象给客户端。


public class MessageService extends Service{

publicMessageService(){

}

@Override

publicIBinderonBind(Intent intent){

returnnull;

}

}

客户端MainActivity调用bindService方法绑定MessageService

这一步其实是属于Service组件相关的知识,在这里就比较简单地说一下,启动服务可以通过以下两种方式:

使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);

使用startService方法 -> startService(Intent service);

bindService & startService区别:

使用bindService方式,多个Client可以同时bind一个Service,但是当所有Client unbind后,Service会退出,通常情况下,如果希望和Service交互,一般使用bindService方法,使用onServiceConnected中的IBinder对象可以和Service进行交互,不需要和Service交互的情况下,使用startService方法即可。

正如上面所说,我们是要和Service交互的,所以我们需要使用bindService方法,但是我们希望unbind后Service仍保持运行,这样的情况下,可以同时调用bindService和startService(比如像本例子中的消息服务,退出UI进程,Service仍需要接收到消息),代码如下:

public class MainActivity extends AppCompatActivity{

privatestaticfinalString TAG ="MainActivity";

@Override

protectedvoidonCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

setupService();

}

/**

* unbindService

*/

@Override

protectedvoidonDestroy(){

unbindService(serviceConnection);

super.onDestroy();

}

/**

* bindService & startService

*/

privatevoidsetupService(){

Intent intent =newIntent(this, MessageService.class);

bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

startService(intent);

}

ServiceConnection serviceConnection =newServiceConnection() {

@Override

publicvoidonServiceConnected(ComponentName name, IBinder service){

Log.d(TAG,"onServiceConnected");

}

@Override

publicvoidonServiceDisconnected(ComponentName name){

Log.d(TAG,"onServiceDisconnected");

}

};

}

Stpe2.服务端在onBind方法返回Binder对象

2.1 首先,什么是Binder?

要说Binder,首先要说一下IBinder这个接口,IBinder是远程对象的基础接口,轻量级的远程过程调用机制的核心部分,该接口描述了与远程对象交互的抽象协议,而Binder实现了IBinder接口,简单说,Binder就是Android SDK中内置的一个多进程通讯实现类,在使用的时候,我们不用也不要去实现IBinder,而是继承Binder这个类即可实现多进程通讯。

2.2 其次,这个需要在onBind方法返回的Binder对象从何而来?

在这里就要引出本文中的主题了——AIDL

多进程中使用的Binder对象,一般通过我们定义好的 .adil 接口文件自动生成,当然你可以走野路子,直接手动编写这个跨进程通讯所需的Binder类,其本质无非就是一个继承了Binder的类,鉴于野路子走起来麻烦,而且都是重复步骤的工作,Google提供了 AIDL 接口来帮我们自动生成Binder这条正路,下文中我们围绕 AIDL 这条正路继续展开讨论(可不能把人给带偏了是吧🙃)

2.3 定义AIDL接口

很明显,接下来我们需要搞一波上面说的Binder,让客户端可以调用到服务端的方法,而这个Binder又是通过AIDL接口自动生成,那我们就先从AIDL搞起,搞之前先看看注意事项,以免出事故:

AIDL支持的数据类型:

Java 编程语言中的所有基本数据类型(如 int、long、char、boolean 等等)

String和CharSequence

Parcelable:实现了Parcelable接口的对象

List:其中的元素需要被AIDL支持,另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口

Map:其中的元素需要被AIDL支持,包括 key 和 value,另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口

其他注意事项:

在AIDL中传递的对象,必须实现Parcelable序列化接口;

在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类;

跟普通接口的区别:只能声明方法,不能声明变量;

所有非基础数据类型参数都需要标出数据走向的方向标记。可以是 in、out 或 inout,基础数据类型默认只能是 in,不能是其他方向。

下面继续我们的例子,开始对AIDL的讲解~

2.4 创建一个AIDL接口,接口中提供发送消息的方法(Android Studio创建AIDL:项目右键 -> New -> AIDL -> AIDL File),代码如下:


package com.example.aidl;

importcom.example.aidl.data.MessageModel;

interface MessageSender{

void sendMessage (in MessageModel messageModel);

}

一个比较尴尬的事情,看了很多文章,从来没有一篇能说清楚in、out、inout这三个参数方向的意义,后来在stackoverflow上找到比较能理解的答案(stackoverflow原文链接),我翻译一下大概意思:

被“in”标记的参数,就是接收实际数据的参数,这个跟我们普通参数传递一样的含义。在AIDL中,“out” 指定了一个仅用于输出的参数,换而言之,这个参数不关心调用方传递了什么数据过来,但是这个参数的值可以在方法被调用后填充(无论调用方传递了什么值过来,在方法执行的时候,这个参数的初始值总是空的),这就是“out”的含义,仅用于输出。而“inout”显然就是“in”和“out”的合体了,输入和输出的参数。区分“in”、“out”有什么用?这是非常重要的,因为每个参数的内容必须编组(序列化,传输,接收和反序列化)。in/out标签允许Binder跳过编组步骤以获得更好的性能。

上述的MessageModel为消息的实体类,该类在AIDL中传递,实现了Parcelable序列化接口,代码如下:

public class MessageModel implements Parcelable{

privateString from;

privateString to;

privateString content;

...

Setter & Getter

...

@Override

publicintdescribeContents(){

return0;

}

//...

序列化相关代码

//...

}

手动实现Parcelable接口比较麻烦,安利一款AS自动生成插件android-parcelable-intellij-plugin

创建完MessageModel这个实体类,别忘了还有一件事要做:”在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类“。代码如下:

package com.example.aidl.data;

parcelable MessageModel;

对于没有接触过aidl的同学,光说就能让人懵逼,来看看此时的项目结构压压惊:

项目结构

我们刚刚新增的3个文件:

MessageSender.aidl -> 定义了发送消息的方法,会自动生成名为MessageSender.Stub的Binder类,在服务端实现,返回给客户端调用

MessageModel.java -> 消息实体类,由客户端传递到服务端,实现了Parcelable序列化

MessageModel.aidl -> 声明了MessageModel可在AIDL中传递,放在跟MessageModel.java相同的包路径下

OK,相信此时懵逼已解除~

2.5 在服务端创建MessageSender.aidl这个AIDL接口自动生成的Binder对象,并返回给客户端调用,服务端MessageService代码如下:

public class MessageService extends Service{

private static final String TAG ="MessageService";

public MessageService(){

}

IBinder messageSender =newMessageSender.Stub() {

@Override

public void sendMessage(MessageModel messageModel)throwsRemoteException{

Log.d(TAG,"messageModel: "+ messageModel.toString());

}

};

@Override

public IBinder onBind(Intent intent){

return messageSender;

}

}

MessageSender.Stub是Android Studio根据我们MessageSender.aidl文件自动生成的Binder对象(至于是怎样生成的,下文会有答案),我们需要把这个Binder对象返回给客户端。

2.6 客户端拿到Binder对象后调用远程方法

调用步骤如下:

在客户端的onServiceConnected方法中,拿到服务端返回的Binder对象;

使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl对应的操作接口;

取得MessageSender对象后,像普通接口一样调用方法即可。

此时客户端代码如下:

public class MainActivity extends AppCompatActivity{

privateMessageSender messageSender;

@Override

protected voido nCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

setupService();

}

//...

private voids etupService(){

Intent intent =newIntent(this, MessageService.class);

bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

startService(intent);

}

ServiceConnection serviceConnection =newServiceConnection() {

@Override

public void onServiceConnected(ComponentName name, IBinder service){

//使用asInterface方法取得AIDL对应的操作接口

messageSender = MessageSender.Stub.asInterface(service);

//生成消息实体对象

MessageModel messageModel =newMessageModel();

messageModel.setFrom("client user id");

messageModel.setTo("receiver user id");

messageModel.setContent("This is message content");

//调用远程Service的sendMessage方法,并传递消息实体对象

try{

messageSender.sendMessage(messageModel);

}catch(RemoteException e) {

e.printStackTrace();

}

}

@Override

public void onServiceDisconnected(ComponentName name){

}

};

}

在客户端中我们调用了MessageSender的sendMessage方法,向服务端发送了一条消息,并把生成的MessageModel对象作为参数传递到了服务端,最终服务端打印的结果如下:

这里有两点要说:

服务端已经接收到客户端发送过来的消息,并正确打印;

服务端和客户端区分两个进程,PID不一样,进程名也不一样;

到这里,我们已经完成了最基本的使用AIDL进行跨进程方法调用,也是Step.0的整个细化过程,可以再回顾一下Step.0,既然已经学会使用了,接下来…全剧终。。。

如果写到这里全剧终,那跟咸鱼有什么区别…

知其然,知其所以然。

我们通过上述的调用流程,看看从客户端到服务端,都经历了些什么事,看看Binder的上层是如何工作的,至于Binder的底层,这是一个非常复杂的话题,本文不深究。(如果看到这里你又想问什么是Binder的话,请手动倒带往上看…)

我们先来回顾一下从客户端发起的调用流程:

MessageSender messageSender = MessageSender.Stub.asInterface(service);

messageSender.sendMessage(messageModel);

抛开其它无关代码,客户端调跨进程方法就这两个步骤,而这两个步骤都封装在 MessageSender.aidl 最终生成的 MessageSender.java 源码(具体路径为:build目录下某个子目录,自己找,不爽你来打我啊 😋 )

请看下方代码和注释,前方高能预警…

public interface MessageSender extends android.os.IInterface{

public static abstract class Stub extends android.os.Binderimplementscom.example.aidl.MessageSender{

private static final java.lang.String DESCRIPTOR ="com.example.aidl.MessageSender";

/**

* 把IBinder对象转换为 com.example.aidl.MessageSender 接口

* 判断IBinder是否处于相同进程,相同进程返回Stub实现的com.example.aidl.MessageSender接口

* 不同进程,则返回Stub.Proxy实现的com.example.aidl.MessageSender接口

*/

public static com.example.aidl.MessageSenderasInterface(android.os.IBinder obj){

if((obj ==null)) {

return null;

}

android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);

if(((iin !=null) && (iininstanceofcom.example.aidl.MessageSender))) {

return((com.example.aidl.MessageSender) iin);

}

return new com.example.aidl.MessageSender.Stub.Proxy(obj);

}

/**

* 同一进程时,不会触发

*

* 不同进程时,asInterface会返回Stub.Proxy,客户端调用 messageSender.sendMessage(messageModel)

* 实质是调用了 Stub.Proxy 的 sendMessage 方法,从而触发跨进程数据传递,

* 最终Binder底层将处理好的数据回调到此方法,并调用我们真正的sendMessage方法

*/

@Override

public boolean onTransact(intcode, android.os.Parcel data, android.os.Parcel reply,intflags)throwsandroid.os.RemoteException{

switch(code) {

caseINTERFACE_TRANSACTION: {

reply.writeString(DESCRIPTOR);

returntrue;

}

caseTRANSACTION_sendMessage: {

data.enforceInterface(DESCRIPTOR);

com.example.aidl.data.MessageModel _arg0;

if((0!= data.readInt())) {

_arg0 = com.example.aidl.data.MessageModel.CREATOR.createFromParcel(data);

}else{

_arg0 =null;

}

this.sendMessage(_arg0);

reply.writeNoException();

return true;

}

}

return super.onTransact(code, data, reply, flags);

}

private static class Proxy implements com.example.aidl.MessageSender{

private android.os.IBinder mRemote;

Proxy(android.os.IBinder remote) {

mRemote = remote;

}

/**

* Proxy中的sendMessage方法,并不是直接调用我们定义的sendMessage方法,而是经过一顿的Parcel读写,

* 然后调用mRemote.transact方法,把数据交给Binder处理,transact处理完毕后会调用上方的onTransact方法,

* onTransact拿到最终得到的参数数据,调用由我们真正的sendMessage方法

*/

@Override

public void sendMessage(com.example.aidl.data.MessageModel messageModel)throwsandroid.os.RemoteException{

android.os.Parcel _data = android.os.Parcel.obtain();

android.os.Parcel _reply = android.os.Parcel.obtain();

try{

_data.writeInterfaceToken(DESCRIPTOR);

if((messageModel !=null)) {

_data.writeInt(1);

messageModel.writeToParcel(_data,0);

}else{

_data.writeInt(0);

}

//调用Binder的transact方法进行多进程数据传输,处理完毕后调用上方的onTransact方法

mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply,0);

_reply.readException();

}finally{

_reply.recycle();

_data.recycle();

}

}

}

staticfinalintTRANSACTION_sendMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION +0);

}

public void sendMessage(com.example.aidl.data.MessageModel messageModel)throwsandroid.os.RemoteException;

}

只看代码的话,可能会有点懵逼,相信结合代码再看下方的流程图会更好理解:

从客户端的sendMessage开始,整个AIDL的调用过程如上图所示,asInterface方法,将会判断onBind方法返回的Binder是否存处于同一进程,在同一进程中,则进行常规的方法调用,若处于不同进程,整个数据传递的过程则需要通过Binder底层去进行编组(序列化,传输,接收和反序列化),得到最终的数据后再进行常规的方法调用。

敲黑板:对象跨进程传输的本质就是 序列化,传输,接收和反序列化 这样一个过程,这也是为什么跨进程传输的对象必须实现Parcelable接口

跨进程的回调接口

在上面我们已经实现了从客户端发送消息到跨进程服务端的功能,接下来我们还需要将服务端接收到的远程服务器消息,传递到客户端。有同学估计会说:“这不就是一个回调接口的事情嘛”,设置回调接口思路是对的,但是在这里使用的回调接口有点不一样,在AIDL中传递的接口,不能是普通的接口,只能是AIDL接口,所以我们需要新建一个AIDL接口传到服务端,作为回调接口。

新建消息收取的AIDL接口MessageReceiver.aidl:

package com.example.aidl;

import com.example.aidl.data.MessageModel;

interface MessageReceiver{

voido nMessageReceived(in MessageModel receivedMessage);

}

接下来我们把回调接口注册到服务端去,修改我们的MessageSender.aidl:

package com.example.aidl;

import com.example.aidl.data.MessageModel;

import com.example.aidl.MessageReceiver;

interface MessageSender{

void sendMessage(in MessageModel messageModel);

void registerReceiveListener(MessageReceiver messageReceiver);

void unregisterReceiveListener(MessageReceiver messageReceiver);

}

以上就是我们最终修改好的aidl接口,接下来我们需要做出对应的变更:

在服务端中增加MessageSender的注册和反注册接口的方法;

在客户端中实现MessageReceiver接口,并通过MessageSender注册到服务端。

客户端变更,修改MainActivity:

public class MainActivity extends AppCompatActivity{

private MessageSender messageSender;

@Override

protected voido nCreate(Bundle savedInstanceState){

//...

}

/**

* 1.unregisterListener

* 2.unbindService

*/

@Override

protectedvoidonDestroy(){

//解除消息监听接口

if(messageSender !=null&& messageSender.asBinder().isBinderAlive()) {

try{

messageSender.unregisterReceiveListener(messageReceiver);

}catch(RemoteException e) {

e.printStackTrace();

}

}

unbindService(serviceConnection);

super.onDestroy();

}

//消息监听回调接口

private MessageReceiver messageReceiver =newMessageReceiver.Stub() {

@Override

public voido nMessageReceived(MessageModel receivedMessage)throwsRemoteException{

Log.d(TAG,"onMessageReceived: "+ receivedMessage.toString());

}

};

ServiceConnection serviceConnection =newServiceConnection() {

@Override

public void onServiceConnected(ComponentName name, IBinder service){

//使用asInterface方法取得AIDL对应的操作接口

messageSender = MessageSender.Stub.asInterface(service);

//生成消息实体对象

MessageModel messageModel =newMessageModel();

//...

try{

//把接收消息的回调接口注册到服务端

messageSender.registerReceiveListener(messageReceiver);

//调用远程Service的sendMessage方法,并传递消息实体对象

messageSender.sendMessage(messageModel);

}catch(RemoteException e) {

e.printStackTrace();

}

}

@Override

public voido nServiceDisconnected(ComponentName name){

}

};

}

客户端主要有3个变更:

增加了messageReceiver对象,用于监听服务端的消息通知;

onServiceConnected方法中,把messageReceiver注册到Service中去;

onDestroy时候解除messageReceiver的注册。

下面对服务端MessageServie进行变更:

public classMessageService extends Service{

private static final String TAG ="MessageService";

private AtomicBoolean serviceStop =newAtomicBoolean(false);

//RemoteCallbackList专门用来管理多进程回调接口

private RemoteCallbackList listenerList =newRemoteCallbackList<>();

public MessageService(){

}

IBinder messageSender =newMessageSender.Stub() {

@Override

public void sendMessage(MessageModel messageModel)throwsRemoteException{

Log.e(TAG,"messageModel: "+ messageModel.toString());

}

@Override

public void registerReceiveListener(MessageReceiver messageReceiver)throwsRemoteException{

listenerList.register(messageReceiver);

}

@Override

public void unregisterReceiveListener(MessageReceiver messageReceiver)throwsRemoteException{

listenerList.unregister(messageReceiver);

}

};

@Override

publicI Binder onBind(Intent intent){

return messageSender;

}

@Override

public voido nCreate(){

super.onCreate();

newThread(newFakeTCPTask()).start();

}

@Override

public void onDestroy(){

serviceStop.set(true);

super.onDestroy();

}

//模拟长连接,通知客户端有新消息到达

private class FakeTCPTask implements Runnable{

@Override

public void run(){

while(!serviceStop.get()) {

try{

Thread.sleep(5000);

}catch(InterruptedException e) {

e.printStackTrace();

}

MessageModel messageModel =newMessageModel();

messageModel.setFrom("Service");

messageModel.setTo("Client");

messageModel.setContent(String.valueOf(System.currentTimeMillis()));

/**

* RemoteCallbackList的遍历方式

* beginBroadcast和finishBroadcast一定要配对使用

*/

final intlistenerCount = listenerList.beginBroadcast();

Log.d(TAG,"listenerCount == "+ listenerCount);

for(inti =0; i < listenerCount; i++) {

MessageReceiver messageReceiver = listenerList.getBroadcastItem(i);

if(messageReceiver !=null) {

try{

messageReceiver.onMessageReceived(messageModel);

}catch(RemoteException e) {

e.printStackTrace();

}

}

}

listenerList.finishBroadcast();

}

}

}

}

服务端主要变更:

MessageSender.Stub实现了注册和反注册回调接口的方法;

增加了RemoteCallbackList来管理AIDL远程接口;

FakeTCPTask模拟了长连接通知客户端有新消息到达。(这里的长连接可以是XMPP,Mina,Mars,Netty等,这里弄个假的意思意思,有时间的话咱开个帖子聊聊XMPP)

这里还有一个需要讲一下的,就是RemoteCallbackList,为什么要用RemoteCallbackList,普通ArrayList不行吗?当然不行,不然干嘛又整一个RemoteCallbackList 🙃,registerReceiveListener 和 unregisterReceiveListener在客户端传输过来的对象,经过Binder处理,在服务端接收到的时候其实是一个新的对象,这样导致在 unregisterReceiveListener 的时候,普通的ArrayList是无法找到在 registerReceiveListener 时候添加到List的那个对象的,但是它们底层使用的Binder对象是同一个,RemoteCallbackList利用这个特性做到了可以找到同一个对象,这样我们就可以顺利反注册客户端传递过来的接口对象了。RemoteCallbackList在客户端进程终止后,它能自动移除客户端所注册的listener,它内部还实现了线程同步,所以我们在注册和反注册都不需要考虑线程同步,的确是个666的类。(至于使用ArrayList的幺蛾子现象,大家可以自己试试,篇幅问题,这里就不演示了)

到此,服务端通知客户端相关的代码也写完了,运行结果无非就是正确打印🙃就不贴图了,可以自己Run一下,打印的时候注意去选择不同的进程,不然瞪坏屏幕也没有日志。

DeathRecipient

你以为这样就完了?too young too simple…

不知道你有没有感觉到,两个进程交互总是觉得缺乏那么一点安全感…比如说服务端进程Crash了,而客户端进程想要调用服务端方法,这样就调用不到了。此时我们可以给Binder设置一个DeathRecipient对象,当Binder意外挂了的时候,我们可以在DeathRecipient接口的回调方法中收到通知,并作出相应的操作,比如重连服务等等。

DeathRecipient的使用如下:

声明DeathRecipient对象,实现其binderDied方法,当binder死亡时,会回调binderDied方法;

给Binder对象设置DeathRecipient对象。

在客户端MainActivity声明DeathRecipient:


/**

* Binder可能会意外死忙(比如Service Crash),Client监听到Binder死忙后可以进行重连服务等操作

*/

IBinder.DeathRecipient deathRecipient =newIBinder.DeathRecipient() {

@Override

public void binderDied(){

Log.d(TAG,"binderDied");

if(messageSender !=null) {

messageSender.asBinder().unlinkToDeath(this,0);

messageSender =null;

}

////TODO:2017/2/28 重连服务或其他操作

setupService();

}

};

ServiceConnection serviceConnection =newServiceConnection() {

@Override

publicvoidonServiceConnected(ComponentName name, IBinder service){

//...

try{

//设置Binder死亡监听

messageSender.asBinder().linkToDeath(deathRecipient,0);

}catch(RemoteException e) {

e.printStackTrace();

}

}

//...

};

Binder中两个重要方法:

linkToDeath -> 设置死亡代理 DeathRecipient 对象;

unlinkToDeath -> Binder死亡的情况下,解除该代理。

此外,Binder中的isBinderAlive也可以判断Binder是否死亡。

权限验证

就算是公交车,上车也得嘀卡对不,如果希望我们的服务进程不想像公交车一样谁想上就上,那么我们可以加入权限验证。

介绍两种常用验证方法:

在服务端的onBind中校验自定义permission,如果通过了我们的校验,正常返回Binder对象,校验不通过返回null,返回null的情况下客户端无法绑定到我们的服务;

在服务端的onTransact方法校验客户端包名,不通过校验直接return false,校验通过执行正常的流程。

自定义permission,在Androidmanifest.xml中增加自定义的权限:

android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION"

android:protectionLevel="normal"/>

服务端检查权限的方法:


IBinder messageSender =newMessageSender.Stub() {

//...

@Override

public boolean onTransact(intcode, Parcel data, Parcel reply,intflags)throwsRemoteException{

/**

* 包名验证方式

*/

String packageName =null;

String[] packages = getPackageManager().getPackagesForUid(getCallingUid());

if(packages !=null&& packages.length >0) {

packageName = packages[0];

}

if(packageName ==null|| !packageName.startsWith("com.example.aidl")) {

Log.d("onTransact","拒绝调用:"+ packageName);

return false;

}

return super.onTransact(code, data, reply, flags);

}

};

@Override

public IBinder onBind(Intent intent){

//自定义permission方式检查权限

if(checkCallingOrSelfPermission("com.example.aidl.permission.REMOTE_SERVICE_PERMISSION") == PackageManager.PERMISSION_DENIED) {

return null;

}

returnmessageSender;

}

根据不同进程,做不同的初始化工作

相信前一两年很多朋友还在使用Android-Universal-Image-Loader来加载图片,它是需要在Application类进行初始化的。打个比如,我们用它来加载图片,而且还有一个图片选择进程,那么我们希望分配更多的缓存给图片选择进程,又或者是一些其他的初始化工作,不需要在图片选择进程初始化怎么办?

这里提供一个简单粗暴的方法,博主也是这么干的…直接拿到进程名判断,作出相应操作即可:

public class MyApp extends Application{

@Override

public voido nCreate(){

super.onCreate();

Log.d("process name", getProcessName());

}

//取得进程名

privateStringgetProcessName(){

ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);

List runningApps = am.getRunningAppProcesses();

if(runningApps ==null) {

returnnull;

}

for(ActivityManager.RunningAppProcessInfo procInfo : runningApps) {

if(procInfo.pid == Process.myPid()) {

return procInfo.processName;

}

}

return null;

}

}

每个进程创建,都会调用Application的onCreate方法,这是一个需要注意的地方,我们也可以根据当前进程的pid,拿到当前进程的名字去做判断,然后做一些我们需要的逻辑,我们这个例子,拿到的两个进程名分别是:

客户端进程:com.example.aidl

服务端进程:com.example.aidl:remote

总结

多进程app可以在系统中申请多份内存,但应合理使用,建议把一些高消耗但不常用的模块放到独立的进程,不使用的进程可及时手动关闭;

实现多进程的方式有多种:四大组件传递Bundle、Messenger、AIDL等,选择适合自己的使用场景;

Android中实现多进程通讯,建议使用系统提供的Binder类,该类已经实现了多进程通讯而不需要我们做底层工作;

多进程应用,Application将会被创建多次;

注,原文地址:http://cjw-blog.net/2017/02/26/AIDL/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容