Android使用AIDL实现跨进程通讯(IPC)

前言:在还没有做任何一件事情之前,千万不要觉得这件事情很难,因为还没有开始做内心就已经对这件事情产生了恐惧,这将会阻止你的进步,也许当你动手开始做了这件事后发现其实并不是很难。

一、 AIDL概述

含义:AIDL(Android Interface Definition Language),是android接口定义语言,这种语言定义了一个客户端和服务器通讯接口的一个标准、规范。

为什么要有AIDL?

 我们都知道android中的四大组件Activity,Broadcast,Content Provider,Service,前面我们应该都接触过除了Service的其他三个组件的进程间通讯的例子,比如:一个应用可以通过显示意图启动另外一个Activity,一个应用发送一个广播,然后被其他应用所接受,一个应用对外提供一个Content Provider,然后其他应用使用ContentResolver获取它提供的数据。这些都是进程间通讯的例子(通常情况下每个应用运行在一个独立的Linux进程中),那么Service这个组件也同样也可以实现垮进程通讯,这就是本篇文章要介绍的AIDL服务。AIDL的出现除了让Service实现垮进程提供服务外,还有一个重要的原因就是:

Using AIDL is necessary only if you allow clients from different applications to access your service for IPC and want to handle multithreading in your service. If you do not need to perform concurrent IPC across different applications, you should create your interface by implementing a Binder or, if you want to perform IPC, but do not need to handle multithreading, implement your interface using a Messenger. Regardless, be sure that you understand Bound Services before implementing an AIDL

这是google官方文档对AIDL的一个提示,意思是说“只有当你允许来自不同应用通过你的service实现进程通讯,并且需要在你的service中处理多线程的情况下才用AIDL,如果你不需要实现不同应用间即时的进程通讯,那么,你应该创建一个接口实现Binder,或者,如果你想实现进程通讯但是不需要处理多线程,那么用一个Messenger实现你的接口,但是,无论如何,你都得先理解本地的服务在你实现AIDL之前“

通过上面的这句话我们就非常清楚了AIDL的作用就是让两个不同的应用间通过Service进行通信(进程通讯IPC),并且远程的Service可以处理多线程。简单来讲就是,两个应用,一个应用对外提供一个远程Service,其他的应用可以并发地访问这个Service,即:C/S模式。

二、 AIDL简单示例

实现步骤:

  1. 创建一个AIDL文件(扩展名为.aidl);
  2. 服务端实现该AIDL文件生成的Java接口(系统会自动生成对应的Java接口);
  3. 暴露一个接口给客户端(通过建立一个Service,在onBind()方法中返回一个Stub类的实例);
  4. 客户端连接绑定该远程服务。

按照这个步骤,咋们通过代码理解AIDL的使用(这里基于Android Studio这个工具,eclipse里面也类似(更简单))。

1. 创建一个AIDL文件

 首先我们需要新建一个Module,这个Module是一个服务端应用,可以直接通过as提供的直接新建AIDL文件,这样它会自动生成aidl文件夹和默认的一个AIDL文件(生成的AIDL默认的包名是应用的包名),这个改包名有点麻烦,因为客户端和服务端都必须要有相同的AIDL文件(包名也必须相同),所以,下面我们通过全手动的方式建立AIDL文件,在main文件夹下(project视图)下面建立一个aidl文件夹,然后在这个文件夹下面建一个包名,包名可以随意,但是客户端和服务器端的AIDL文件包名必须一致,接下来在这个包名下面建一个文件,这里叫IRemoteService.aidl

aidl文件有它自己的语法(aidl:接口定义语言):

  1. 每个aidl文件只能定义一个接口(单一接口);
  2. 默认Java中的基本数据类型都支持,如:int, long, char, boolean;
  3. 默认支持String和CharSequence;
  4. 默认支持List(可以选择加泛型,需要引入List所在的包),但是List中存储的数据也只能是Java基本数据类型,而且另外一边(客户端或服务端)接受的是ArrayList类型;
  5. 默认支持Map(可以选择加泛型,但泛型只能是基本数据类型或String和CharSequence,需要引入Map所在的包),但同样,Map中存储的数据也只能是Java基本数据类型,而且另外一边(客户端或服务端)接受的是HashMap类型;
  6. 所有aidl文件中的注释都会出现在自动生成的IBinder接口java文件中(除了导入包语句之前的注释);
  7. aidl接口只支持方法,不支持变量。

aidl接口文件写法和Java接口的写法非常类似,就是不要Public修饰符,所以,我们这样写:

IRemoteService.aidl

// IRemoteService.aidl
package com.lt.aidl;
// Declare any non-default types here with import statements
interface IRemoteService {
    /**
     * 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);

    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** get name by id */
    String getName(int id);
}

Ctrl + S保存后我们可以看到编译器会自动给我们生成(如果没自动生成,可以重新构建一下这个项目)一个对应的IRemoteService.java文件,这个文件在Packages视图下可以看到:

这里写图片描述

这个自动生成的java文件我们不需要看懂,所以,不管它,接下来我们进行第二步和第三步,也就是服务端实现该AIDL文件生成的Java接口(系统会自动生成对应的Java接口)。

2(3). 服务端实现该AIDL文件生成的Java接口

 打开这个AIDL生成的Java接口,我们可以发现,里面有一个内部静态抽象类Stub,这个类继承了Binder并实现了这个接口,所以,我们可以直接使用这个Stub类完成远程服务的搭建。

新建一个Sevice,在onBind方法中返回实现了AIDL Java接口的那个Binder类(new一个Stub类正好),这个类将作为这个Service代理类:

package com.lt.remoteservice.service;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;

import com.lt.aidl.IRemoteService;

/**
 * Created by lt on 2016/3/6.
 */
public class RemoteService extends Service{

    private String[] names = {"吕布","关羽","赵子龙","张飞"};

    /**
     * 返回一个RemoteService代理对象IBinder给客户端使用
     * @param intent
     * @return
     */
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub(){

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
            System.out.println("Thread: " + Thread.currentThread().getName());
            System.out.println("basicTypes aDouble: " + aDouble +" anInt: " + anInt+" aBoolean " + aBoolean+" aString " + aString);
        }

        @Override
        public int getPid() throws RemoteException {
            System.out.println("Thread: " + Thread.currentThread().getName());
            System.out.println("RemoteService getPid ");
            return android.os.Process.myPid();
        }

        @Override
        public String getName(int id) throws RemoteException {
            return names[id];
        }
    };
}

注意:不要忘了在清单文件中注册该Service,并且我们还需要提供一个包含action属性的intent-filter(客户端通常是通过隐式意图来启动该服务),这个action属性值可以任意。

4. 客户端绑定该服务

 新建另一个Module,同样需要建立aidl文件,这个文件要和服务器端的aidl文件一模一样(注意包名),这里直接复制服务器端的aidl文件夹在这个Module中。

为了直观展示通讯效果,我们做一个交互界面,activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:orientation="vertical"
                android:layout_height="match_parent"
                >

    <EditText
        android:id="@+id/editText"
        android:hint="输入查询ID"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:onClick="search"
        android:layout_width="wrap_content"
        android:text="查询"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:orientation="horizontal"
        android:layout_height="wrap_content">
        <TextView
            android:text="结果:"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_result"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</LinearLayout>

重点来了,客户端绑定远程服务,先建立一个服务连接

 private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 这里的IBinder对象service是代理对象,所以必须调用下面的方法转换成AIDL接口对象
            mRemoteService = IRemoteService.Stub.asInterface(service);
            int pid = 0;
            try {
                pid = mRemoteService.getPid();
                int currentPid = android.os.Process.myPid();
                System.out.println("currentPID: " + currentPid +"  remotePID: " + pid);
                mRemoteService.basicTypes(12, 1223, true, 12.2f, 12.3, "有梦就要去追,加油!");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            System.out.println("bind success! " + mRemoteService.toString());
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteService = null;
            System.out.println(mRemoteService.toString() +" disconnected! ");
        }
    };

注意:

  1. 连接的两个方法,一个是服务连接成功后回调,一个是服务端开连接时回调;
  2. 连接成功后在onServiceConnected方法中返回了一个IBinder对象service,这个Service对象必须通过IRemoteService.Stub.asInterface(service);转换成AIDL对象,这个对象将作为远程服务的代理对象。

通过隐式意图绑定远程服务

// 连接绑定远程服务
Intent intent = new Intent();
// action值为远程服务的action,即上面我们在服务端应用清单文件的action
intent.setAction("lt.test.aidl");
intent.setPackage("com.lt.remoteservice");
isConnSuccess = bindService(intent, conn, Context.BIND_AUTO_CREATE);

注意:

  1. android 5.0 中对service隐式启动做了限制,必须通过设置action和package;
  2. package是指要启动的那个服务所在的包名,这里即服务器端的那个应用的包名;
  3. 绑定服务有可能会失败(如客户端和服务器端的AIDL文件不一致)。

界面交互,响应button事件:

public void search(View view){
    if(isConnSuccess){
        // 连接成功
        int id = Integer.valueOf(mEditText.getText().toString());
        try {
            String name = mRemoteService.getName(id);
            mTv_result.setText(name);
        }catch (RemoteException ex) {
            ex.printStackTrace();
        }
    }else{
        System.out.println("连接失败!");
    }
}

客户端activity完整代码:

package com.lt.remoteclient;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

import com.lt.aidl.IRemoteService;

public class MainActivity extends Activity {

    private IRemoteService mRemoteService;
    private TextView mTv_result;
    private EditText mEditText;
    private boolean isConnSuccess;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mEditText = (EditText) findViewById(R.id.editText);
        mTv_result = (TextView) findViewById(R.id.tv_result);

        // 连接绑定远程服务
        Intent intent = new Intent();
        intent.setAction("lt.test.aidl");
        intent.setPackage("com.lt.remoteservice");
        isConnSuccess = bindService(intent, conn, Context.BIND_AUTO_CREATE);
    }

    public void search(View view){
        if(isConnSuccess){
            // 连接成功
            int id = Integer.valueOf(mEditText.getText().toString());
            try {
                String name = mRemoteService.getName(id);
                mTv_result.setText(name);
            }catch (RemoteException ex) {
                ex.printStackTrace();
            }
        }else{
            System.out.println("连接失败!");
        }
    }

    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 这里的IBinder对象service是代理对象,所以必须调用下面的方法转换成AIDL接口对象
            mRemoteService = IRemoteService.Stub.asInterface(service);
            int pid = 0;
            try {
                pid = mRemoteService.getPid();
                int currentPid = android.os.Process.myPid();
                System.out.println("currentPID: " + currentPid +"  remotePID: " + pid);
                mRemoteService.basicTypes(12, 1223, true, 12.2f, 12.3, "有梦就要去追,加油!");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            System.out.println("bind success! " + mRemoteService.toString());
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteService = null;
            System.out.println(mRemoteService.toString() +" disconnected! ");
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(conn);
    }
}

运行服务端应用,然后在运行客户端应用,测试结果:

这里写图片描述

观察后台日志打印:

这里写图片描述

可以看到,服务端和客户端应用处在两个不同的进程中,并且客户端可以像传递基本类型的数据,同时客户端也可以从远程服务端应用取得数据(查询结果),这就通过远程Service完成了两个应用间的通讯(垮进程通讯IPC)。但是,出现了一个问题,上面我们只是传递了基本类型的数据,而没有传递对象这种非基本类型的数据,难道不能传递非基本类型数据吗?答案是当然可以,下面我们实现垮进程传递一个对象。

三、 AIDL垮进程传递对象

 要通过AIDL垮进程传递非基本数据类型对象,那么这个对象需要实现Parcelable接口( android系统内部会将这个对象类型分解成基本数据类型,然后在进程间传递)。

实现步骤(新建一个类):

  1. 实现Parcelable接口;
  2. 覆盖writeTowriteToParcel(Parcel dest, int flags)Parcel方法,这个方法携带了当前对象的状态和将它写入Parcel中;
  3. 添加一个静态成员变量CREATOR,这个变量是一个实现了Parcelable.Creator interface.接口的对象;
  4. 建立一个这个类对应的aidl文件,即文件名和类名一样,扩展名为.aidl

1(2)(3). 新建一个类实现Parcelable接口

Person.java

package com.lt.aidl;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * Created by lt on 2016/3/8.
 */
public class Person implements Parcelable{

    private String name;
    private int age;

    private Person(Parcel in)
    {
        readFromParcel(in);
    }

    private void readFromParcel(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 在想要进行序列号传递的实体类内部一定要声明该常量。常量名只能是CREATOR,类型也必须是
     * Parcelable.Creator<T>  T:就是当前对象类型
     */
    public static final Creator<Person> CREATOR = new Creator<Person>() {

        /***
         * 根据序列化的Parcel对象,反序列化为原本的实体对象
         * 读出顺序要和writeToParcel的写入顺序相同
         */
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in.readString(),in.readInt());
        }

        /**
         * 创建一个要序列化的实体类的数组,数组中存储的都设置为null
         */
        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 将对象写入到Parcel(序列化)
     * @param dest:就是对象即将写入的目的对象
     * @param flags: 有关对象序列号的方式的标识
     * 这里要注意,写入的顺序要和在createFromParcel方法中读出的顺序完全相同。例如这里先写入的为name,
     * 那么在createFromParcel就要先读name
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

4. 建立该类对应的aidl文件

在Person类所在的包建立一个对应的aidl文件Person.aidl,文件内容:

package com.lt.aidl;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Person;

这里现在的项目结构为:

这里写图片描述

OK,将新建的Person.javaPerson.aidl往客户端相同的地方复制一份,然后重新构建整个项目,如果直接这样Android Studio在构建的时候会报导入包出错(AIDL自定义类型导入失败),这个问题折腾了我几个小时(说多了都是泪…),解决办法:

在项目grade文件中添加如下代码将AIDL也作为源文件夹:

sourceSets {
    main {
        manifest.srcFile 'src/main/AndroidManifest.xml'
        java.srcDirs = ['src/main/java', 'src/main/aidl']
        resources.srcDirs = ['src/main/java', 'src/main/aidl']
        aidl.srcDirs = ['src/main/aidl']
        res.srcDirs = ['src/main/res']
        assets.srcDirs = ['src/main/assets']
    }
}

加入这段代码后整个grade文件内容为:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.lt.remoteclient"
        minSdkVersion 11
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java', 'src/main/aidl']
            resources.srcDirs = ['src/main/java', 'src/main/aidl']
            aidl.srcDirs = ['src/main/aidl']
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
}

然后重新构建一下项目,OK,完美解决(激动…),分别运行两个应用后的测试结果:

这里写图片描述

看到这个结果显示应该知道在java代码中做了哪些改变,只是个测试传递对象数据的问题,所以这里就不帖出代码了,在文章结尾将会贴上源码。

到这里AIDL的使用也就介绍完了

总结:

 AIDL作为一个接口定义语言,它有它自己的语法,其语法和定义java接口类似,所以有人也通过建立一个接口,然后更改一下后缀名为aidl来创建AIDL接口文件,需要注意的是AIDL只有当不同的应用需要即使通讯,并且需要处理多线程的情况下才会使用,不然可以使用其他进程通信的方式来实现,还有,AIDL默认支持大部分数据类型,但如果要传递对象数据,那么需要采取一些措施才行。

demo下载:http://download.csdn.net/detail/ydxlt/9455949

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值