我们知道,每个 Android 程序都是运行在独立的进程当中的,默认进程名称为该程序的包名,通过 adb shell ps
可以查看当前设备中正在运行的进程:
但同时 Android 还为我们提供了 android:process
属性,可以帮我们将同一个程序的不同组件运行在不同的进程当中,之所以要这样做,是因为 Android 系统对于每个应用所占内存是有限制的,进程占用内存越大,往往容易被系统回收,让占用内存大的组件运行在不同的进程当中,可以减少主进程所占的内存,避免被回收(反正网上是这么说的)。
这样一来,就不可避免的产生进程间通信的问题,去面试的时候,进程间通信也会经常被问到,主要有哪些方式?
大致上,Android 系统中的进程通信方法可以分为以下几类:
关于什么是 Binder,可以看这里 Android Binder机制浅析
官网上这样形容 AIDL :
您可以利用它定义发送端与接收端使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。
并且重点提示:
只有允许不同应用的发送端用 IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用 AIDL。 如果您不需要执行跨越不同应用的并发 IPC,就应该通过实现一个 Binder 创建接口;或者,如果您想执行 IPC,但根本不需要处理多线程,则使用 Messenger 类来实现接口。
说明了运用 AIDL 的两个必要条件:
那开始使用 AIDL 吧。
我们创建两个应用,分别模拟 Server 和 Client,先编辑 Server 端的 AIDL 文件
我们直接在当前 module 上右击,New
- AIDL
- AIDL File
,输入文件名:MyAIDLTest
,会发现 AndroidStudio 会自动在该 module 下创建一个 aidl
文件夹,并且会创建一个该工程包名相同的包,刚才创建的 AIDL 文件(MyAIDLTest.aidl
)就在这个包当中。
interface IMyAIDLTest {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
系统会默认创建一个方法,通过注释可以看到该方法只是为了向我们演示 AIDL 可以用作参数的数据类型,无用,删之。
我们创建自己的方法:
package com.lixyz.aidltestserver;
interface IMyAIDLTest {
String getName();
}
这里注意一下包名,先记住就行
然后我们 Rebuild 一下工程,然后我们可以发现在 generatedJava
文件夹当中自动生成了相同的和 AIDL 相同的包名,包名下有一个和 AIDL 文件名相同的 .java
文件。该文件由系统自动生成,和 R 文件一样,我们不要去编辑它。
和正常创建服务的方法大致相同,唯一的区别我们之前 onBind
方法返回的是我们自己创建的继承自 Binder
的对象,而这次返回的是实现 IMyAIDLTest.Stub
接口的对象:
public class AIDLService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new AIDLBinder();
}
class AIDLBinder extends IMyAIDLTest.Stub {
@Override
public String getName() throws RemoteException {
return "模拟返回的数据";
}
}
}
接下来,在清单文件当中注册该 Server:
<service android:name=".AIDLService" />
这样写是有问题的,但是我们先这样写,后面遇到问题时再改过来,正好说明一下常见问题
至此,接收端的工作已经完成了,开始写发送端的。
在发送端创建的 aidl 文件内容要和接收端一样。
package com.lixyz.aidlclient;
// Declare any non-default types here with import statements
interface IMyAIDLTest {
String getName();
}
然后我们在 Activity 当中绑定服务:
bind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bindService(intent, conn, BIND_AUTO_CREATE);
}
});
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IMyAIDLTest aidlTest = IMyAIDLTest.Stub.asInterface(service);
try {
Log.d(TAG, aidlTest.getName());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
然后我们运行接收端,在运行发送端,点击按钮,进行绑定,看是否会打印 Log,答案肯定不会,会抛出 IllegalArgumentException
,因为我们并没有指定 Intent 要访问的目的地。
但是我们要绑定的是其他 APP 的服务,该怎么写呢?
这样写行不行?
bind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intent.setAction("com.lixyz.aidltestserver.AIDLService");
intent.setPackage("com.lixyz.aidltestserver");
bindService(intent, conn, BIND_AUTO_CREATE);
}
});
还是绑定失败,因为:
规则一:发送端 aidl 接口文件的包名必须和接收端 aidl 文件包名相同
我们将接收端和发送端的包名修改一下,统一为 com.lixyz.aidl
,再执行,发现还是没有绑定成功,是因为:
规则二:接收端服务需要保证该服务可以对外交互,也就是说,
android:exported
属性需要为 true
规则三:Android 5.0开始,无法使用隐式 Intent 绑定服务,需要设置 Service 所在接收端的包名
这次再绑定,发现 Log 打印,绑定成功!
在文章的一开始,就说明了 AIDL 是基于 Binder 实现的,Binder 是什么这里就不展开讲了,因为要展开讲就太长了。在这里你可以将其理解为一个 虚拟设备
。
在我们绑定本地服务的时候,系统调用本地服务的 onBind
方法,该方法返回与服务进行交互的 IBinder
接口对象。要接收 IBinder
,发送端必须创建一个 ServiceConnection
实例,并将其传递给 bindService()
。ServiceConnection
包括一个回调方法,系统通过调用它来传递 IBinder
。
在 AIDL 当中,ServiceConnection 同样也需要接受 IBinder,这个 IBinder 是如何来的呢?
以我们刚刚创建的 IMyAIDLTest 为例:
可以看到在系统自动生成的同名 Java 文件当中,主要由两部分组成:
分析 Stub 类,主要由以下方法构成:
Stub 类和 Proxy 代理类都实现了我们创建的接口,在代理类 Proxy 中又实现了我们自己定义的方法。
发送端中,我们通过 IMyAIDLTest.Stub.asInterface(service)
来获取我们定义的 AIDL 接口,一步一步来,看 asInterface
方法:
/**
* Cast an IBinder object into an com.lixyz.aidl.IMyAIDLTest interface,
* generating a proxy if needed.
*/
public static com.lixyz.aidl.IMyAIDLTest asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.lixyz.aidl.IMyAIDLTest))) {
return ((com.lixyz.aidl.IMyAIDLTest) iin);
}
return new com.lixyz.aidl.IMyAIDLTest.Stub.Proxy(obj);
}
这个方法会判断我们传入的参数是否是本地接口,如果不是,则会调用代理类将其包装成一个代理类对象
接下来我们会调用我们自己添加的 getName
方法,调用的就是代理类当中的 getName 方法,最终会调用 transact
方法,系统会帮我们找到接收端的 AIDL 接口,其 onTransact()
会接收 Client 传递过来的参数,然后在 switch 方法中找到我们要调用的方法,将方法的返回值传递过来,再返回,这样就完成了从发送端到接收端,再从接收端到发送端的一个过程。
如何寻找到接收端的呢?这是 Android Binder 架构的一个知识点,我也迷迷糊糊的讲不清楚,等我搞清楚了再写
默认情况下,AIDL 支持传递的数据类型包括:
什么是 我们自己声明的可打包类型
呢?
还记得系统为我们自动生成的 java 文件吗?
在代理类的方法中,_data
和 _reply
都是 android.os.Parcel
类型的,通过查看 API 我们可以看到 Parcel 是我们通过 IBinder 传递消息的容器,支持序列化和反序列化。
官方文档中明确通过 IPC 是可以传递对象的,只需要实现按照以下步骤:
Parcelable
接口writeToParcel
方法CREATOR
的静态字段.aidl
文件前三步可以通过插件自动完成,高效还安全
但是需要注意的是,插件生成的代码有时候会不包含readFromParcel
方法,需要我们手动添加,否则会编译不通过
在手动添加时,一定要保证 writeToParcel 和 readFromParcel 方法读写的顺序是一一对应的
所以我们只需要以下几步,就可以在通过 AIDL 传递对象了:
Parcelable
接口writeToParcel
,它会获取对象的当前状态并将其写入 Parcel
CREATOR
的静态字段,这个字段是一个实现 Parcelable.Creator
接口的对象。.aidl
文件例如:
自定义 Person
类,并实现 Parcelable
接口,添加 writeToParcel
和 readFromParcel
方法,添加 CREATOR
字段。
public class Person implements Parcelable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.age);
}
public void readFromParcel(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
}
public Person() {
}
protected Person(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
}
public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>() {
@Override
public Person createFromParcel(Parcel source) {
return new Person(source);
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
}
创建 Person 的 aidl
文件,这个文件的作用是声明 Person
是 parcelable
的,可以用于 AIDL 传输。
package com.lixyz.bean;
parcelable Person;
修改之前的 AIDL 接口文件:
package com.lixyz.aidl;
// 我们自定义的对象,需要使用 import 在此声明,否则会编译不通过
import com.lixyz.bean.Person;
interface IMyAIDLTest {
Person getPerson();
}
同样修改 Service 方法:
public class AIDLService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
@Override
public void onCreate() {
super.onCreate();
}
class MyBinder extends AIDLTest.Stub {
@Override
public Person getPerson() throws RemoteException {
return new Person("张三", 20);
}
}
}
发送端方面依葫芦画瓢,调用就可以了。
需要注意的是,我们创建的自定义类,和声明该类为 parcelable
的 aidl 接口,包的名称必须相同,否则编译会不通过,可以看到我在上面都是新建了一个 com.lixyz.bean
包。
我们接着往下走,之前 Server 端提供的 getPerson 方法,我们新增一个 addPerson 方法,参数为一个 Person 对象:
interface AIDLTest {
Person getPerson();
void addPerson(Person person);
}
然后你就会发现,编译出错,为什么呢?官网中这样说:
所有非原语参数都需要指示数据走向的方向标记。可以是 in、out 或 inout
也就是说,所有我们自定义的对象作为参数,都必须标记数据的走向:
Java 自带的数据类型,只能是 in
但是切记,in、out、inout 只是 对象参数的流向
关于in、out 和 inout 的解释,是 copy 自这里:AIDL中的in,out,inout
官网中说,您应该将方向限定为真正需要的方向,因为编组参数的开销极大
。具体因为啥,我也不知道,我猜是因为对对象的序列化和反序列化这个过程本身对于内存或者处理器的消耗大吧。
我们知道 AIDL 是用来进行进程之间的通信的,也就是说,你无法预料和你通信的进程是本地进程还是远程进程,事实上,不同的进程连接,也会存在差异,还记得我们分析工具为我们自动生成的那个文件吗?在那个文件里是不是有一个判断,如果是本地线程,直接调用接口,如果是远程,就返回代理类。
官方文档是这样说的:
所以我们之前写的代码实际上都是不规范的,因为我们并没有考虑到线程安全问题。那么,我们还需要对接收端的相关代码进行一下修改,上面说了,如果是其他线程,那么那个线程就是在服务中执行相关代码的线程(这样也就产生了多线程问题)
所以我们需要保证相关代码的线程安全问题:
class MyBinder extends AIDLTest.Stub {
@Override
public Person getPerson() throws RemoteException {
synchronized (this) {
return new Person("张三", 10);
}
}
@Override
public Person editPerson(Person person) throws RemoteException {
synchronized (this) {
Log.d("TTT", "服务端收到的" + person.getName() + " ||| " + person.getAge());
person.setName("王五");
person.setAge(30);
Log.d("TTT", "服务端修改后的: " + person.getName() + " ||| " + person.getAge());
return person;
}
}
}
默认情况下,整个调用过程是同步进行的,所以当接收端需要进行大量耗时操作的时候,假如我们在 Activity 的主线程当中发起调用,则会很容易引发 ANR,所以如果我们确定整个调用过程在几毫秒内无法完成,那么尽量把调用放在子线程当中进行。
假如接收端的 Service 定义了某项权限,也就是说,添加了 android:permission
属性,那么发送端也必须拥有该权限,否则将会出现异常。
接收端:
<permission
android:name="com.lixyz.aidl.permission"
android:protectionLevel="normal" />
<uses-permission android:name="com.lixyz.aidl.permission" />
<service
android:name=".AIDLService"
android:exported="true"
android:permission="com.lixyz.aidl.permission">
<intent-filter>
<action android:name="com.lixyz.aidltestserver.MyService" />
</intent-filter>
</service>
发送端:
<uses-permission android:name="com.lixyz.aidl.permission" />