Android的IPC机制--实现AIDL的最简单例子(上)

前言

对于AIDL的介绍, 将主要分为两部分:

  • 上篇 将介绍 ADIL的使用, 给出对应的demo

  • 下篇 将分析ADIL的实现原理及源码分析

一、到底什么是AIDL

AIDL是一个缩写,全称是Android Interface Defination Language, 即Android接口定义语言。它的主要作用是实现跨进程通信。通过定义我们想要的AIDL文件, 会自动在生成对应的java代码,让开发者专注于应用层的开发,提升开发效率。

二、为什么要使用AIDL来跨进程通信

Android进程中的每个进程都是在自己独立的内存上运行并存储自己的数据, 两个进程间想要通信,就需要使用一些特定的组件, 比如说我们的AIDL,比如说BroadCastReceiver、Messenger、ContentProvider等,那我们为什么要选择AIDL来进行跨进程通信呢? 两个字:高效! BroadcastReceiver使用简单, 但是占用的系统资源比较多, 耗费时间长,如果频繁的跨进程通信, 效率就太低了; Messenger跨进程通信是, 请求队列时同步的, 无法并发执行,只有前一个任务处理完了, 才能处理下一个任务; ContentProvider呢,主要作用是把数据暴露出来, 别的进程可以来读取。最最最重要的一点:以上的各种跨进程通信方式,其实依赖的都是Binder机制,都是对Binder进行的封装, 但是由于是已经封装好的系统组件, 它制定的规则已经固定,使用上也就具有一定的局限性。而AIDL是直接为了让不同进程间可相互调用而最对Binder直接的封装, 是按照我们想要的功能进行的定制化 封装, 它虽然使用起来麻烦了一点, 但却是效率最高的。

当然,看到Binder不要觉得害怕,本文不会深入Binder庞大的底层源码去分析, 只会解析它在应用层的使用及源码,让大家了解Binder在应用层常见套路。这将在下篇中体现。

三、AIDL基础知识及语法介绍

首先一定要明确,AIDL的语法很简单,大家不要怕难。它的语法和java基本上是类似的, 只是有一些细微上的不同,因为它就是用来简化Android Developer的工作的, 太难了使用成本太高,谁还愿意用。

下面主要介绍下它的基础知识点:

  • 文件类型:AIDL文件后缀为 .aidl。

  • 数据类型:AIDL默认支持一些数据类型,使用这些数据类型是不用导包的,除了这些类型以外,必须导入包, 即便是我们要使用的对象类与当前正在编写的.aidl文件在同一个包下, 这是与java的不同点,这个可以参见后面的demo。其中,默认支持的数据类型有:

    1. java中的八种基本数据类型: byte(8), short(16), int(32), long(64), float(32), double(64), boolean(1), char(16), 括号的数字表示基本数据类型的位长。
    2. CharSequence类型(包含了String)
    3. List类型:List中 的所有元素必须是AIDL支持的类型,即默认的数据类型或者是其他AIDL生成的接口,或者是定义的Parcelable。List可以使用泛型。
    4. Map类型:Map中的所有元素必须是AIDL支持的类型,即默认的数据类型或者是其他AIDL生成的接口,或者是定义的Parcelable。Map是不支持泛型。

.

  • 定向tag: AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。

    另外, Java 中的基本类型和 String ,CharSequence 的定向 tag 默认且只能是 in 。还有,请注意,请不要滥用定向 tag ,而是要根据需要选取合适的——要是不管三七二十一,全都一上来就用 inout ,等工程大了系统的开销就会大很多——因为排列整理参数的开销是很昂贵的。

  • AIDL文件大致可以分为两种:

    • 第一类是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。
    • 第二类是用来定义方法接口,以供系统使用来完成跨进程通信的。
所有的非默认支持数据类型必须通过第一类AIDL文件定义才能被使用。同时我们可以看到,我们一直在定义接口, 却没有具体的实现, 从这里可以体会一下为什么AIDL叫做接口定义语言。

举个例子, 下面分别给出两种AIDL文件的定义:

//第一类AIDL文件的例子
//定义一个 Book.aidl文件
//这个文件的作用是引入了一个序列化对象 Book 供其他的AIDL文件使用
//我们需要先定义一个Book.java, 这个类需要实现parcelable序列化接口

//注意:下面这一行代码是包名,引入Book这个对象,这一点和java类似,
//Book.aidl与Book.java他们需要放在同一个包下
package com.example.my_chapter_2.aidl;

//注意 parcelable 是小写
parcelable Book;

//第二类AIDL文件的例子
//定义一个BookManager.aidl

//同样, 需要当前文件的包名
package com.lypeer.ipcclient;

//导入所需要使用的非默认支持数据类型的包, 和java类似
package com.example.my_chapter_2.aidl.Book;

//接口定义语言, 当然要定义接口啦, 和java接口类似
interface BookManager {

	//所有的返回值前都不需要加任何东西,不管是什么数据类型
	List<Book> getBooks();
	
	
	//传参时除了Java基本类型以及String,CharSequence之外的类型
    //都需要在前面加上定向tag,具体加什么量需而定
	void addBook(in Book book);

}

**注意:从以上介绍可以知道, 我们需要创建的AIDL文件数量至少是 n+1 个, n表示Parcelable对象的数量, 1表示定义接口方法的AIDL文件数量, 当然也有可能为数量为2,3,4。。。

四、使用AIDL跨进程通信的Demo—创建AIDL文件

介绍完AIDL的基础知识, 下面我们进入实战吧, 直接上demo, 上面提到, AIDL文件主要分为两类,第一种用来定义Parcelable对象,第二种用来定义方法接口。有的AIDL没有自定义Parcelable对象, 但是比较少,这种简单例子这里就略过了,我们按照有Parcelable对象来建立我们的Demo。我们接下来将分三步走

  • 第一步:建立我们需要的类对象
  • 第二步:建立第一种AIDL文件,定义我们的Parcelable对象
  • 第三步:建立第二种AIDL文件,定义我们的方法接口
第一步,建立我们需要的类对象

我们先定义一个Parcelable对象。首先创建一个类,简历getter和setter方法并添加一个无参构造函数:

public class Book {

	private String name;
	private int price;
	
	public Book() {
       
    }
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	}
}

接着, 我们让这个类实现序列化接口Parcelable接口,实现这个接口后,根据提示,实现它的方法, 补齐代码,下面上代码:

/**
  * 创建一个AIDL所支持的 数据类型 
  *
  */
public class Book implements Parcelable{

private String name;
private int price;

public Book() {
   
}

public Book(Parcel in) {
    name = in.readString();
    price = in.readInt();
}

public String getName() {
	return name;
}
public void setName(String name) {
	this.name = name;
}
public int getPrice() {
	return price;
}
public void setPrice(int price) {
	this.price = price;
}


@Override
public int describeContents() {
	// 默认返回0 即可
	return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
	// :默认生成的模板类的对象只支持为 in 的定向 tag 。
	//为什么呢?因为默认生成的类里面只有 writeToParcel() 方法,而如果要支持为 out 或者 inout 的定向 tag 的话,
	//还需要实现 readFromParcel() 方法
	
	//把数据写进parcel中, 后面的读值顺序应当是和writeToParcel()方法中一致的
	dest.writeString(name);
	dest.writeInt(price);
}

/**
 * 自己定义的readFromParcel方法, 这样才支持 定向tag out
 * @param dest
 */
public void readFromParcel(Parcel dest) {
	 //注意,此处的读值顺序应当是和writeToParcel()方法中一致的
	name = dest.readString();
	price = dest.readInt();
}


/**
 * 还必须要有Creator, 这是实现parcel接口 所必须的
 */
public static final Creator<Book> CREATOR = new Creator<Book>() {
	
	@Override
	public Book[] newArray(int size) {
		// 创建一个 Book数组
		return new Book[size]; //创建
	}
	
	@Override
	public Book createFromParcel(Parcel source) {
		// 根据传来的数据 创建Book
		return new Book(source);
	}
};

@Override
public String toString() {
    return String.format("[price:%s, name:%s]", price, name);
}

}

请注意,这里有一个坑:默认生成的模板类的对象只支持为 in 的定向 tag 。为什么呢?因为默认生成的类里面只有 writeToParcel() 方法,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel() 方法——而这个方法其实并没有在 Parcelable 接口里面,所以需要我们自己写一下, 按照上述代码写入即可。

为什么要实现序列化接口: 由于不同的进程有着不同的内存区域,并且它们只能访问自己的那一块内存区域,所以我们不能像平时那样,传一个对象的引用过去就完事了——引用指向的是一个内存区域,现在目标进程根本不能访问源进程的内存,那把它传过去又有什么用呢?所以我们必须将要传输的数据转化为能够在内存之间流通的形式。这个转化的过程就叫做序列化与反序列化。简单来说是这样的:比如现在我们要将一个对象的数据从客户端传到服务端去,我们就可以在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,从而还原其中包含的数据——通过这种方式,我们就达到了在一个进程中传输数据到另一个进程的数据中的目的。

,

序列化接口Parcelable和Serializable的区别, 为什么用Parcelable: Serializable接口是java中的序列化接口, 使用简单一点,但是开销大,序列化和反序列化过程都需要大量的IO操作。Parcelable接口是Android定制的序列化接口,比起java的序列化接口Serializable,性能上更有优势,在Android平台进程间通信使用, 效率较高, 是Android推荐的序列化方式,缺点是使用的时候稍微麻烦一点点, 要多写两个方法。 Parcelable主要用在 在进程间传输对象的序列化上, 通过Parcelable序列化的对象存储到本地或是网络传输也是可以的, 但是这个过程会稍微复杂一些,因此这两种情况下建议大家使用Serializable。

第二步,建立第一种AIDL文件,定义我们的Parcelable对象

上面我们创建了一个Book.java, 我们需要一个 Book.aidl 文件来将 Book 类引入使得其他的 AIDL 文件其中可以使用 Book 对象。那么第一步,在Book.java的同一个包下,新建一个后缀为.aidl的 Book.aidl文件, 这个文件如下:

//Book.aidl

//这个aidl文件的作用是为了引入一个序列化的 数据类型给 其他aidl文件使用

//注意:Book.aidl 和 Book.java的包名必须一致
package com.example.my_chapter_2.aidl;

//注意 parcelable 是小写
parcelable Book;
第三步,建立第二种AIDL文件,定义我们的方法接口

文件如下:

//作用:定义方法接口

//当前包的包名
package com.example.my_chapter_2.aidl;

//导入需要的自定义数据类
import com.example.my_chapter_2.aidl.Book;

//接口定义语言, 当然要定义接口啦
interface BookManager {

	//定义的第一个方法
	//所有的返回值前都不需要加任何东西,不管是什么数据类型
	List<Book> getBooks();
	
	//定义的第二个方法
	//传参时除了Java基本类型以及String,CharSequence之外的类型
    //都需要在前面加上定向tag,具体加什么量需而定
	void addBook(in Book book);

}

注意:这里又有一个坑! 大家可能注意到了,在 Book.aidl 文件中,我一直在强调:Book.aidl与Book.java的包名应当是一样的。这似乎理所当然的意味着这两个文件应当是在同一个包里面的——事实上,很多比较老的文章里就是这样说的,他们说最好都在 aidl 包里同一个包下,方便移植——然而在 Android Studio 里并不是这样。如果这样做的话,系统根本就找不到 Book.java 文件,从而在其他的AIDL文件里面使用 Book 对象的时候会报 Symbol not found 的错误。为什么会这样呢?因为 Gradle 。大家都知道,Android Studio 是默认使用 Gradle 来构建 Android 项目的,而 Gradle 在构建项目的时候会通过 sourceSets 来配置不同文件的访问路径,从而加快查找速度——问题就出在这里。Gradle 默认是将 java 代码的访问路径设置在 java 包下的,这样一来,如果 java 文件是放在 aidl 包下的话那么理所当然系统是找不到这个 java 文件的。那应该怎么办呢?

又要 java文件和 aidl 文件的包名是一样的,又要能找到这个 java 文件——那么仔细想一下的话,其实解决方法是很显而易见的。首先我们可以把问题转化成:如何在保证两个文件包名一样的情况下,让系统能够找到我们的 java 文件?这样一来思路就很明确了:要么让系统来 aidl 包里面来找 java 文件,要么把 java 文件放到系统能找到的地方去,也即放到 java 包里面去。接下来我详细的讲一下这两种方式具体应该怎么做:

方法1: 修改 build.gradle 文件:在 android{} 中间加上下面的内容:

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}

也就是把 java 代码的访问路径设置成了 java 包和 aidl 包,这样一来系统就会到 aidl 包里面去查找 java 文件,也就达到了我们的目的。只是有一点,这样设置后 Android Studio 中的项目目录会有一些改变,我感觉改得挺难看的。

方法2:把 java 文件放到 java 包下去

把 Book.java 放到 java 包里任意一个包下,保持其包名不变,与 Book.aidl 一致。只要它的包名不变,Book.aidl 就能找到 Book.java ,而只要 Book.java 在 java 包下,那么系统也是能找到它的。但是这样做的话也有一个问题,就是在移植相关 .aidl 文件和 .java 文件的时候没那么方便,不能直接把整个 aidl 文件夹拿过去完事儿了,还要单独将 .java 文件放到 java 文件夹里 去。
我们可以用上面两个方法之一来解决找不到 .java 文件的坑,具体用哪个就看大家怎么选了,反正都挺简单的。

到这里我们就已经将AIDL文件新建并且书写完毕了,clean 一下项目,如果没有报错,这一块就算是大功告成了。

五、使用AIDL跨进程通信的Demo–服务端代码

想要实现AIDL通信,我们需要保证在客户端和服务端的都有一样的.aidl文件和其中涉及到的java类。所以不管写在哪一端, 我们都需要把这些文件复制到另一端,并且保持包名一致,前面已经有所介绍。因为这样编译器才会给我们自动生成相同的类, 然后我们跨进程的数据就可以正常的序列化及反序列化了。

然后我们编写服务端代码, 通常就是创建一个Service, 然后在Service中实现我们在aidl中定义的接口方法。这样客户端调用接口后, 服务端就执行对应方法, 然后返回数据(如果有返回值), 下面是服务端代码:

public class AIDLService extends Service {
private String TAG = AIDLService.class.getSimpleName();

//包含Book对象的list
private List<Book> mBooks = new ArrayList<Book>();

/**
 * 根据AIDL文件 编译器为我们生成的BookManager
 */
private final BookManager.Stub mBookManager = new BookManager.Stub() {
	
	@Override
	public List<Book> getBooks() throws RemoteException {
		//客户端来获取数据时, 返回服务端的数据
		synchronized (this) {
			Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());
			if(mBooks != null) 
				return mBooks;
			
			return new ArrayList<Book>();
		}
	}
	
	@Override
	public void addBook(Book book) throws RemoteException {
		//客户端发来修改数据的请求, 服务端添加对应数据
		synchronized (this) {
			if(mBooks == null) {
				mBooks = new ArrayList<Book>();
			}
			
			if(book == null) {
				Log.e(TAG, "service add book: book is null");
				return;
			}
			mBooks.add(book);
			 //打印mBooks列表,观察客户端传过来的值
            Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());
		}
		
	}
};

@Override
public void onCreate() {
	super.onCreate();
	//启动时就去 加一本书
	Book book = new Book();
	book.setName("服务端书籍");
	book.setPrice(100);
	try {
		mBookManager.addBook(book);
	} catch (RemoteException e) {
		e.printStackTrace();
	}
}

@Override
public IBinder onBind(Intent intent) {
	Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));
	return mBookManager;
}

}

代码结构很简单, 主要有 1.创建了一个BookManager.Stub类型的 mBookManager对象; 2.onCreate()生命周期方法, 3.onBind()方法。这也是AIDL使用的代码套路,主要实现这三块即可, 其中BookManager 它是编译器根据我们的AIDL文件生成的java类,它的内部类BookManager.Stub这个类是我们用来通信的关键, 这是一个Binder对象,后面会分析。

然后需要在AndroidManifest.xml文件里面注册我们的Service,四大组件大家应该都不陌生吧:

<!-- 千万不要忘记 设置exported为true, 否则其他APP或进程无法访问 -->
<service android:name="com.example.my_chapter_2.service.AIDLService"
        android:exported="true">
        <intent-filter >
            <action android:name="com.example.my_chapter_2"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
    </service>

好了, 服务端就编写好了。

六、使用AIDL跨进程通信的Demo–客户端代码

客户端我们要完成的工作主要是调用服务端的方法,但是在那之前,我们首先要绑定上服务端,完整的客户端代码是这样的:

public class MainActivity extends Activity {

	private String TAG = "cilent";
	
	//由AIDL文件生成的Java类
    private IBookManager mBookManager = null;

    //标志当前与服务端连接状况的布尔值,false为未连接,true为连接中
    private boolean mBound = false;

    //包含Book对象的list
    private List<Book> mBooks;
    
private static int mPrice = 10;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}
	
	@Override
	protected void onStart() {
		super.onStart();
		if (!mBound) {
            attemptToBindService();
        }
	}
	
	@Override
	protected void onStop() {
		// TODO Auto-generated method stub
		super.onStop();
		if(mBound) {//如果绑定了, 才能解绑
			unbindService(mServiceConnection);//一定要记得给service解绑
		}
	}
	
	/**
	 * 按钮的点击事件
	 * @param view
	 */
	public void addBook(View view) {
		if(!mBound) {
			//没绑定则去绑定服务端
			attemptToBindService();
			return;
		}
		
		Book book = new Book();
		book.setName("android 开发艺术探索 " + System.currentTimeMillis());
		book.setPrice(mPrice++);
		try {
			mBookManager.addBook(book);
		} catch (RemoteException e) {
			Log.e(TAG, " addBook RemoteException");
			e.printStackTrace();
		}
		
	}
	
	public void getBooks(View view) {
		if(!mBound) {
			//没绑定则去绑定服务端
			attemptToBindService();
			return;
		}
		
		try {
			List<Book> books = mBookManager.getBookList();
			Log.e(TAG, " getBooks: "+books.toString());
		} catch (RemoteException e) {
			Log.e(TAG, " addBook RemoteException");
			e.printStackTrace();
		}
	}
	
	
	/**
	 * 尝试与服务端进行连接
	 */
	private void attemptToBindService() {
		Intent intent = new Intent();
		intent.setAction("com.example.my_chapter_2_mine"); //设置企图
		intent.setPackage("com.example.my_chapter_2.service");//设置包名
		
		bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
		
	}
	
	ServiceConnection mServiceConnection = new ServiceConnection() {
		
		@Override
		public void onServiceDisconnected(ComponentName name) {
			//一般不会走到这个回调, 除非是 service异常终止, 才会回调到这来
			Log.e(TAG, "service disconnected");
			mBound = false;
			
		}
		
		@Override
		public void onServiceConnected(ComponentName name, IBinder service) {
			// 绑定成功后给的回调
			Log.e(TAG, "service connected");
			mBookManager = BookManagerImpl.asInterface(service); //初始化mBookManager
			mBound = true;
			
			if(mBookManager != null) {
				try {
					mBooks = mBookManager.getBookList();
					 Log.e(TAG, mBooks.toString());
				} catch (RemoteException e) {
					Log.e(TAG, "RemoteException");
					e.printStackTrace();
				}
			}
			
		}
	};

}

代码结构也很清晰, 就是我们绑定Service的常规步骤,然后用绑定后得到的本地代理mBookManager去操作服务端数据。

七、测试通信

客户端服务端都建立好了, 我们开始通信吧, 将两个测试demo都装到手机上,启动服务端demo, 然后在客户端demo进行操作

调用addBook()方法,服务端打印日志:

//服务端的 log 信息,我把无用的信息头去掉了,然后给它编了个号
on bind,intent = Intent { act=com.example.my_chapter_2 pkg=com.example.my_chapter_2.service }
invoking addBooks() method , now the list is : [name : 服务端数据 , price : 100,name : Android开发艺术探索 , price : 10]

客户端日志:

//客户端的 log 信息
service connected
[name : Android开发艺术探索 , price : 10]
八 、结语

对于AIDL的使用流程到这里就讲完了, 我们再次总结一下, 主要分为一下步骤:

  • 1、定义我们需要的Parcelable对象,即实现Parcelable接口的java类
  • 2、定义第一类AIDL文件, 即Parcelable对象对应的AIDL文件
  • 3、定义第二类AIDL文件, 即我们的接口方法 文件
  • 4、把AIDL文件拷贝到服务端/客户端 相同的包名下
  • 5、Service相关代码:服务端注册Service、客户端绑定Service
  • 6、启动服务端客户端,开始通信

下一篇, 我们将介绍AIDL实现原理及对应的应用层源码分析

谢谢大家, 喜欢的朋友麻烦点个赞吧~

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值