1.AIDL AIDL是Android Interface Definition Language的缩写,即Android接口定义语言。所以我们知道的第一点就是:AIDL是一种语言。 设计这门语言的目的是为了实现进程间通信,尤其是在涉及多进程并发情况下的进程间通信。 每一个进程都有自己的Dalvik VM实例,都有自己的一块独立的内存,都在自己的内存上存储自己的数据,执行着自己的操作,每个进程之间都你不知我,我不知你,就像是隔江相望的两座小岛一样,都在同一个世界里,但又各自有着自己的世界。而AIDL,就是两座小岛之间沟通的桥梁,而我们就好像造物主一样,我们可以通过AIDL来制定一些规则,规定它们能进行哪些交流,比如它们可以在我们制定的规则下传输一些特定规格的数据。 总之,通过这门语言,可以愉快的在一个进程访问另一个进程的数据,甚至调用它的一些方法,当然,只能是特定的方法。 如果仅仅是要进行跨进程通信的话,其实还有其他选择,比如BroadcastReceiver , Messenger等,但是BroadcastReceiver占用的系统资源比较多,如果是频繁的跨进程通信的话显然是不可取的。Messenger进行跨进程通信时请求队列是同步进行的,无法并发执行,在有些要求多进程的情况下不适用。这种时候就需要使用AIDL了。
2.AIDL的语法 基本上AIDL的语法和Java是一样的,只是在一些细微处有些许差别——毕竟它只是被创造出来简化Android程序员工作的。这里着重说一下它和Java不一样的地方。主要有下面这些点: ①文件类型:用AIDL书写的文件的后缀是.aidl,而不是.java。 ②数据类型:AIDL默认支持一些数据类型,在使用这些数据类型的时候是不需要导包的,但是除了这些类型之外的数据类型,在使用之前必须导包,就算目标文件与当前正在编写的.aidl文件在同一个包下(在Java中,这种情况是不需要导包的)。比如,现在我们编写了两个文件,一个是Book.java ,另一个是BookManager.aidl,它们都在com.lypeer.aidldemo包下 ,现在我们需要在.aidl文件里使用Book对象,那么我们就必须在.aidl文件里面写上import com.lypeer.aidldemo.Book; 哪怕.java文件和.aidl文件就在一个包下。 默认支持的数据类型包括: (1)Java中的八种基本数据类型,包括 byte,short,int,long,float,double,boolean,char。 (2)String类型。 (3)CharSequence类型。 (4)List类型:List中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的Parcelable。List可以使用泛型。 (5)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文件:所有的AIDL文件大致可以分为两类。一类是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。一类是用来定义方法接口,以供系统使用来完成跨进程通信的。 可以看到,两类文件都是在“定义”些什么,而不涉及具体的实现,这就是为什么它叫做“Android接口定义语言”。 注:所有的非默认支持的数据类型必须通过第一类AIDL文件定义才能被使用。
下面是两个例子,对于常见的AIDL文件都有所涉及: ①AIDL用来定义Parcelable对象 // Book.aidl //第一类AIDL文件的例子,这个文件的作用是引入了一个序列化对象Book供其他的AIDL文件使用 //注意:Book.aidl与Book.java的包名应当是一样的 package com.lypeer.ipcclient;
//注意parcelable是小写 parcelable Book; ②AIDL用来定义方法接口 // BookManager.aidl //第二类AIDL文件的例子 package com.lypeer.ipcclient; //导入所需要使用的非默认支持数据类型的包 import com.lypeer.ipcclient.Book;
interface BookManager { //所有的返回值前都不需要加任何东西,不管是什么数据类型 List< Book> getBooks(); Book getBook(); int getBookCount();
//传参时除了Java基本类型以及String、CharSequence之外的类型,都需要在前面加上定向tag,具体加什么量需而定 void setBookPrice(in Book book , int price) void setBookName(in Book book , String name) void addBookIn(in Book book); void addBookOut(out Book book); void addBookInout(inout Book book); }
3.使用AIDL文件完成跨进程通信 跨进程通信的时候,根据AIDL中定义的方法里是否包含非默认支持的数据类型,我们要进行的操作是不一样的。如果不包含,那么我们只需要编写一个AIDL文件;如果包含,那么我们通常需要写n+1个AIDL文件( n 为非默认支持的数据类型的种类数)。 下面以AIDL文件中包含非默认支持的数据类型的情况为例,说明使用AIDL进程间通信的步骤: ①使数据类实现Parcelable接口 由于不同的进程有着不同的内存区域,并且它们只能访问自己的那一块内存区域,所以我们不能像平时那样,传一个句柄过去就完事了(句柄指向的是一个内存区域),现在目标进程根本不能访问源进程的内存。所以我们必须将要传输的数据转化为能够在内存之间流通的形式。这个转化的过程就叫做序列化与反序列化。比如现在我们要将一个对象的数据从客户端传到服务端去,我们就可以在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,从而还原其中包含的数据。通过这种方式,我们就达到了在一个进程中访问另一个进程的数据的目的。 通常,在我们通过AIDL进行跨进程通信的时候,选择的序列化方式是实现Parcelable接口。具体实现如下: 首先,创建一个类,实现Parcelable接口。实现Parcelable接口时,studio会报红,这时候只需要alt+enter解决即可。 最后生成的Book类如下: public class Book implements Parcelable{ 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; }
public Book(Parcel in) { name = in.readString(); price = in.readInt(); }
public static final Creator< Book> CREATOR = new Creator< Book>() { @Override public Book createFromParcel(Parcel in) { return new Book(in); }
@Override public Book[] newArray(int size) { return new Book[size]; } };
@Override public int describeContents() { return 0; }
@Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(price); } } 注意:默认生成的模板类的对象只支持为in的定向tag。这是因为默认生成的类里面只有writeToParcel(),而如果要支持为out或者inout的定向tag的话,还需要实现readFromParcel()。而这个方法其实并没有在Parcelable接口里面,所以需要我们从头写。 readFromParcel() 可以仿照writeToParcel()写: @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(price); } //参数是一个Parcel,用它来存储与传输数据 public void readFromParcel(Parcel dest) { //注意,此处的读值顺序应当是和writeToParcel()方法中一致的 name = dest.readString(); price = dest.readInt(); } 这样添加了readFromParcel()之后,Book类的对象在AIDL文件里就可以用out或者inout来作为它的定向 tag 了。
注:若AIDL文件中涉及到的所有数据类型均为默认支持的数据类型,则无此步骤。因为默认支持的那些数据类型都是可序列化的。
至此,关于AIDL中非默认支持数据类型的序列化操作就完成了。 ②书写AIDL文件 首先我们需要一个Book.aidl文件来将Book类引入,使得其他的AIDL文件可以使用Book对象。 在项目的app上点击右键,然后 new->AIDL->AIDL File,按下鼠标左键就会弹出一个框提示生成AIDL文件了。 注意:如果我们先创建了Book.java文件,然后在创建Book.aidl文件时,studio会报Interface name must be unique的错误,此时,可以改成先创建Book.aidl,然后再创建Book.java就可以了。 生成AIDL文件之后,项目目录中比以前多了一个叫做aidl的包,而且他的层级是和java包相同的,并且aidl包里默认有着和java包里相同的包结构。 如果你用的是 Eclipse 或者较低版本的studio,编译器没有这个选项怎么办呢?没关系,可以自己写。打开项目文件夹,依次进入 app->src->main,在 main 包下新建一个和java文件夹平级的aidl文件夹,然后手动在这个文件夹里面新建和java文件夹里面的默认结构一样的文件夹结构,再在最里层新建 .aidl 文件就可以了。 在这里,我们需要两个AIDL文件: (1)Book.aidl //第一类AIDL文件,这个文件的作用是引入了一个序列化对象Book供其他的AIDL文件使用 //注意:Book.aidl与Book.java的包名应当是一样的 package com.lypeer.ipcclient;
//注意parcelable是小写 parcelable Book; (2)BookManager.aidl //第二类AIDL文件,作用是定义方法接口 package com.lypeer.ipcclient; //导入所需要使用的非默认支持数据类型的包 import com.lypeer.ipcclient.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文件的。解决办法有两个:要么让系统来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文件和其中涉及到的.java文件,因此不管在哪一端写的这些东西,写完之后我们都要把这些文件复制到另一端去。 如果是用的上面两个方法中的第一个解决的找不到.java文件的问题,那么直接将aidl包复制到另一端的main目录下就可以了;如果是使用第二个方法的话,就除了把整个aidl文件夹拿过去,还要单独将.java文件放到java文件夹里去。 复制时要注意,aidl文件夹和java文件夹平级,都在app-src-main下。复制.java文件时要保证.java文件的包名和.aidl的包名一致。 复制完成后,同步并rebuild一下工程,此时在build下会生成aidl文件对应的java文件。 ④编写服务端代码 通过上面几步,我们已经完成了AIDL及其相关文件的全部内容,下面就利用这些东西来进行跨进程通信吧。 在我们写完AIDL文件并clean或者rebuild项目之后,编译器会根据AIDL文件为我们生成一个与AIDL文件同名的.java文件,这个.java文件才是与跨进程通信密切相关的东西。 基本的操作流程是:在服务端实现AIDL中定义的方法接口的具体逻辑,然后在客户端调用这些方法接口,从而达到跨进程通信的目的。 注意:因为AIDL是多线程并发,所以在方法接口的具体逻辑里都加了synchronized。
服务端代码: public class AIDLService extends Service { private List< Book> mBooks = new ArrayList<>();
//由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<>(); } }
@Override public void addBook(Book book) throws RemoteException { synchronized (this) { if (mBooks==null) { mBooks = new ArrayList<>(); } if (book == null) { Log.e(TAG, “Book is null in In”); book = new Book(); } //尝试修改book的参数,主要是为了观察其到客户端的反馈 book.setPrice(2333); if (!mBooks.contains(book)) { 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(“Android开发艺术探索”); book.setPrice(28); mBooks.add(book); }
@Override public IBinder onBind(Intent intent) { Log.e(getClass().getSimpleName(), String.format(“on bind,intent = %s”, intent.toString())); return mBookManager; } } 整体的代码结构大致可以分为三块:第一块是初始化,在 onCreate() 方法里面进行了一些数据的初始化操作。第二块是重写BookManager.Stub中的方法,在这里面提供AIDL里面定义的方法接口的具体实现逻辑。第三块是重写onBind()方法,在里面返回写好的BookManager.Stub。 最后别忘了在Manefest文件里面注册Service : < service android:name=".service.AIDLService" android:exported=“true”> < intent-filter> < action android:name=“com.lypeer.aidl”/> < category android:name=“android.intent.category.DEFAULT”/> < /intent-filter> < /service> 到这里,服务端代码就编写完毕了。 ⑤编写客户端代码 在客户端主要是调用服务端的方法。但是在那之前,我们首先要连接上服务端。 客户端代码: public class AIDLActivity extends Activity { //由AIDL文件生成的Java类 private BookManager mBookManager = null; //标志当前与服务端连接状况的布尔值,false为未连接,true为连接中 private boolean mBound = false; private List< Book> mBooks;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_aidl); }
//按钮的点击事件,点击之后调用服务端的addBook方法 public void addBook(View view) { //如果与服务端的连接处于未连接状态,则尝试连接 if (!mBound) { attemptToBindService(); Toast.makeText(this, “当前与服务端处于未连接状态,正在尝试重连,请稍后再试”, Toast.LENGTH_SHORT).show(); return; } if (mBookManager == null) return;
Book book = new Book(); book.setName(“APP研发录In”); book.setPrice(30); try { mBookManager.addBook(book); Log.e(getLocalClassName(), book.toString()); } catch (RemoteException e) { e.printStackTrace(); } }
//尝试与服务端建立连接 private void attemptToBindService() { Intent intent = new Intent(); intent.setAction(“com.lypeer.aidl”); intent.setPackage(“com.lypeer.ipcserver”); bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); }
@Override protected void onStart() { super.onStart(); if (!mBound) { attemptToBindService(); } }
@Override protected void onStop() { super.onStop(); if (mBound) { unbindService(mServiceConnection); mBound = false; } }
private ServiceConnection mServiceConnection=new ServiceConnection(){ @Override public void onServiceConnected (ComponentName name, IBinder service) { Log.e(getLocalClassName(), “service connected”); mBookManager = BookManager.Stub.asInterface(service); mBound = true;
if (mBookManager != null) { try { mBooks = mBookManager.getBooks(); Log.e(getLocalClassName(), mBooks.toString()); } catch (RemoteException e) { e.printStackTrace(); } } }
@Override public void onServiceDisconnected (ComponentName name) { Log.e(getLocalClassName(), “service disconnected”); mBound = false; } }; } 首先建立连接,然后在ServiceConnection里面获取BookManager对象,接着通过它来调用服务端的方法。 ⑥开始通信 通过上面的步骤,我们已经完成了所有的前期工作,接下来就可以通过AIDL来进行跨进程通信了!将两个app同时运行在同一台手机上,然后调用客户端的addBook()方法,我们会看到服务端的 logcat 信息是这样的: on bind,intent = Intent { act=com.lypeer.aidl pkg=com.lypeer.ipcserver } invoking getBooks() method , now the list is : [name : Android开发艺术探索 , price : 28] invoking addBooks() method , now the list is : [name : Android开发艺术探索 , price : 28, name : APP研发录In , price : 2333]
客户端的信息是这样的: service connected [name : Android开发艺术探索 , price : 28] name : APP研发录In , price : 30
所有的log信息都很正常并且符合预期,这说明我们到这里为止的步骤都是正确的,能够正确的使用AIDL来进行跨进程通信的。
4.AIDL源码分析 我们已经知道,在写完AIDL文件后,编译器会帮我们自动生成一个同名的.java文件,在我们实际编写客户端和服务端代码的过程中,真正协助我们工作的其实就是这个文件,而.aidl文件从头到尾都没有出现过。这样一来我们就很容易产生一个疑问:难道我们写AIDL文件的目的其实就是为了生成这个文件么?答案是肯定的。事实上,就算我们不写AIDL文件,直接按照它生成的.java文件那样写一个.java文件出来,在服务端和客户端中也可以照常使用这个.java类来进行跨进程通信。所以说AIDL语言只是在简化我们写这个.java文件的工作而已,而要研究AIDL是如何帮助我们进行跨进程通信的,其实就是研究这个生成的.java文件是如何工作的。 自动生成的这个.java文件的完整路径是:app->build->generated->aidl_source_output_dir->debug->out->com->lypeer->ipcclient->BookManager.java(其中 com.lypeer.ipcclient 是包名,相对应的AIDL文件为 BookManager.aidl )。
我们先从整体对这个流程整理一下: 首先从服务端开始,刨去其他与此无关的东西,从宏观上我们看看它干了些啥: private final BookManager.Stub mBookManager = new BookManager.Stub() { @Override public List< Book> getBooks() throws RemoteException { // getBooks()方法的具体实现 }
@Override public void addBook(Book book) throws RemoteException { // addBook()方法的具体实现 } };
public IBinder onBind(Intent intent) { return mBookManager; } 可以看到首先是对BookManager.Stub里面的抽象方法进行了重写。实际上,这些抽象方法正是在AIDL文件里面定义的那些。也就是说,在这里为之前定义的方法提供了具体实现。接着,在onBind()方法里我们将这个BookManager.Stub作为返回值传了过去。
接着看看客户端: private BookManager mBookManager = null;
private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected (ComponentName name, IBinder service) mBookManager = BookManager.Stub.asInterface(service); //省略 } @Override public void onServiceDisconnected (ComponentName name) { //省略 } };
public void addBook(View view) { //省略 mBookManager.addBook(book); } 简单的来说,客户端就做了这些事:获取BookManager对象,然后调用它里面的方法。
现在结合服务端与客户端做的事情,好好思考一下,我们会发现这样一个怪事情:它们配合的如此紧密,以至于它们之间的交互竟像是同一个进程中的两个类那么自然!大家可以回想下平时项目里的接口回调,基本流程与此一般无二。明明是在两个线程里面,数据不能直接互通,何以他们能交流的如此愉快呢?答案就在BookManager.java里。接下来具体分析: ①从客户端开始 点开BookManager.java,会发现BookManager是一个接口类!我们都知道,接口类里方法是没有具体实现的。但是明明在客户端里面调用了 mBookManager.addBook() !那么就说明在客户端里面用到的BookManager绝不仅仅是BookManager,而是它的一个实现类!那么就可以从这个实现类入手,看看客户端调用addBook()方法的时候,究竟 BookManager 在背后帮我们完成了哪些操作。首先看下客户端的 BookManager 对象是怎么来的:
|