Android Studio中AIDL的使用

什么是AIDL

AIDL的全称为:Android Interface Definition Language,翻译过来就为安卓接口定义语言,对理解AIDL没什么实际意义,那么它到底是干什么的呢?简单来说,它是通过一种定义接口的方式,可以让我们快速的进行进程间通讯。关于进程,我想都应该明白它的含义,在Android中,可以直接理解为我们不同的程序,所以如果我们一个APP想要和另外一个APP进行通讯或者数据上的交换,就可以使用AIDL。在之前Eclipse中使用AIDL的文章很多,这篇文章我们来看看在Android Studio中如何使用AIDL。

服务端Demo的功能介绍

在做进程间通讯时,会存在一个服务端和一个客户端,所以使用AIDL也一样,我们两个Demo项目中AIDLServer作为服务端

AIDLServer

它的功能为:提供一个输入框,可以输入数字,然后点击下面的按钮,会启动一个Service(如果已经启动,不会再启动),并将输入框的数字通过Intent传到Service中。

先看Activity中的代码:

package com.jucongyuan.aidlserver.activity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.jucongyuan.aidlserver.R;
import com.jucongyuan.aidlserver.service.ServerService;

public class ServerActivity extends Activity {
    private EditText et;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_server);
        et = (EditText) findViewById(R.id.et);
        findViewById(R.id.btnStart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String str = et.getText().toString();
                if (TextUtils.isEmpty(str))
                    Toast.makeText(ServerActivity.this, "请随意输入一些数字", Toast.LENGTH_SHORT).show();
                else {
                    Intent intent = new Intent(ServerActivity.this, ServerService.class);
                    intent.putExtra("str", str);
                    startService(intent);
                }
            }
        });
    }
}

布局文件的代码就不展示了,现在我们要开始定义aidl文件了,在Android Studio中,我们要创建aidl文件,需要在main文件夹下,也就是和java、res文件夹同一层中创建一个aidl文件夹,如上面第一个图中结构那样。接下来,我们需要在aidl文件中新增一个包,这里为com.jucongyuan,然后就要开始新建一个aidl文件了,具体步骤为在com.jucongyuan包上右键,然后new->AIDL->AIDL File,接着输入文件名,例如这个例子中为IConnect

文件生生成后,会默认有一个方法,我们不用管,直接修改或者添加自己想要的方法就可以了。IConnect.aidl的内容为:

// IConnect.aidl
package com.jucongyuan;

// Declare any non-default types here with import statements

interface IConnect {
    String getStr();
}

里面除了被注释的代码,还有包名和一个接口类,接口类中有一个方法,就这么简单。到目前为止,是不是我们就可以开始使用这个aidl文件了呢?如果你现在就使用,你会发现,我们是找不到IConnect这个类的,还需要关键的一步,就是Make Project

为什么需要这一步呢,因为AIDL之所以叫定义语言,就是在进程间通讯时,并不会使用这个IConnect.aidl文件,我们定义好这个aidl文件后,需要利用编译器生成一个真正在进程间通讯会使用到的类,这个类在哪里呢?他在我们的build文件夹下,具体位置如下:

默认情况下,这个类的代码是没有缩进的,我们需要格式化一下代码,方便我们查看,这个类就是通过IConnect.aidl生成的,他也是我们进程间通讯的关键类,这里我们就不过多解释里面的内容了,后面会专门介绍进程间通讯的过程和原理。接下来我们才可以开始(也才能)使用这个定义语言生成的类进行进程间通讯。它的使用在ServerService.java中:

package com.jucongyuan.aidlserver.service;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;

import com.jucongyuan.IConnect;

public class ServerService extends Service {

    private String str;

    IConnect.Stub stub = new IConnect.Stub() {
        @Override
        public String getStr() throws RemoteException {
            return str;
        }
    };

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        str = intent.getStringExtra("str");
        return Service.START_NOT_STICKY;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }
}

在这个Service中,我们是实现了一个刚才生成那个类中的一个内部类IConnect.Stub,然后在实现方法中返回了一个字符串,这个字符串是哪里来的呢?就是我们之前那个ServerActivity中输入的,这一点不作过多解释。另外注意,在ServerService中的onBind是返回了我们实现的IConnect.Stub这个类的实例,这点非常重要。还有一个非常重要的地方就是在Manifest文件中,我们在注册这个ServerService服务时,需要指定一个action:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.jucongyuan.aidlserver">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity
            android:name=".activity.ServerActivity"
            android:label="@string/app_name"
            android:launchMode="standard">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".service.ServerService">
            <intent-filter>
                <action android:name="com.jucongyuan.IConnect" />"
            </intent-filter>
        </service>

    </application>

</manifest>

这里action的名字为:com.jucongyuan.IConnect,这个name可以随意命名,但是我们必须记住,因为在待会客户端连接时,会使用到它。

客户端Demo的功能介绍

接下来我们来看看客户端AIDLClient的项目怎么编写,首选来看看项目结构:

比服务端要简单一下,注意项目结构中的AIDL定义,和服务端是一样的(包名和文件名),甚至里面的内容都是一样的,也必须一样,所以我们可以直接从服务端的项目中直接拷贝过来,这也是一种比较稳妥的方式,防止再手动创建一次而出错。同样的,我们需要Make Project。那么,现在不一样的就是ClientActivity了,我们先看看客户端运行后的界面:

非常简单,上面是一个TextView(这里没有背景看不出来),下面是一个按钮,通过点击这个按钮,我们就可以从服务端取到在服务端输入框中输入的数字,然后显示到TextView上,来看看具体的代码(同样布局文件就不在贴了,文末我会提供项目的源码地址):

package com.jucongyuan.aidlclient.activity;

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.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;

import com.jucongyuan.IConnect;
import com.jucongyuan.aidlclient.R;


public class ClientActivity extends Activity {

    private TextView tv;
    private IConnect connect;

    //第一部分
    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            try {
                connect = IConnect.Stub.asInterface(service);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
    //第一部分

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_client);
        tv = (TextView) findViewById(R.id.tv);

        //第二部分
        Intent intent = new Intent();
        intent.setAction(IConnect.class.getName());
        intent.setClassName("com.jucongyuan.aidlserver", "com.jucongyuan.aidlserver.service.ServerService");
        bindService(intent, conn, Context.BIND_AUTO_CREATE);
        //第二部分

        findViewById(R.id.btnGet).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    tv.setText(connect.getStr());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });

    }
}

其他代码都很容易理解,有两部分值得我们关注,代码中已注明,首先是实例化了一个ServiceConnection对象,ServiceConnection是个接口类,我们实现它的两个方法,这两个方法在下面连接成功或者失败是会被回调。第二部分就是连接的代码,intent中的action就使我们在服务端的manifest中注册Service时的那个action name。而下面的ClassName就是我们服务端程序的包名和ServerService所在位置。然后调用bindService方法,成功后,我们就可以在上面的onServiceConnected方法中获取到一个IConnect实例,然后我们点击按钮,通过IConnect的实例connect调用getStr()方法,就可以获取到服务端中ServerService中的变量str的值了,也就是服务端中输入框输入的值。

测试

先运行AIDLServer项目,然后在输入框中输入666,再点击下面的按钮:

接着运行AIDLClient项目,只需要点击按钮,然后就会显示出666了:

可以反复多试几次,输入不同数字测试。

AIDL中传输自定义的对象

上面的使用只是AIDL最简单的使用,而在实际的情况中,我们需要在两个程序中传递更为复杂的对象,那么怎么去实现呢?现在我们修改一下需求,在客户端点击按钮时,传递一个学生到服务端,然后服务端将这个学生的学号设为服务端输入框中的值,再返回给客户端,客户端显示这个学生的信息。首先在客户端中定义学生实体:

package com.jucongyuan;

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

public class Student implements Parcelable {

    private String name;
    private String number;


    public Student() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    protected Student(Parcel in) {
        name = in.readString();
        number = in.readString();
    }

    public static final Creator<Student> CREATOR = new Creator<Student>() {
        @Override
        public Student createFromParcel(Parcel in) {
            return new Student(in);
        }

        @Override
        public Student[] newArray(int size) {
            return new Student[size];
        }
    };

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(number);
    }
}

在客户端中,它所在的包为com.jucongyuan,注意,这个实体所在的包名和我们aidl文件所在的包名是一致的,接着我们需要新增一个aidl文件,这个aidl文件的作用就是连接这个实体,新增的方式和上面创建IConnect.aidl方法是一样的,右键包名新建。这个aidl文件的名字为Student.aidl:

// Student.aidl
package com.jucongyuan;

parcelable Student;

很简单,接下来修改IConnect.aidl文件中的方法:

// IConnect.aidl
package com.jucongyuan;

import com.jucongyuan.Student;

interface IConnect {
    Student getStr(in Student s);
}

这个方法修改为了传入一个Student参数,然后方法会返回一个Student,具体实现会在服务端做,接下来重要的一步Make Project,项目会报错,因为我们已经修改了方法,所以调用时也必须做修改:

package com.jucongyuan.aidlclient.activity;

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.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;

import com.jucongyuan.IConnect;
import com.jucongyuan.Student;
import com.jucongyuan.aidlclient.R;


public class ClientActivity extends Activity {

    private TextView tv;
    private IConnect connect;

    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            try {
                connect = IConnect.Stub.asInterface(service);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_client);
        tv = (TextView) findViewById(R.id.tv);
        Intent intent = new Intent();
        intent.setAction(IConnect.class.getName());
        intent.setClassName("com.jucongyuan.aidlserver", "com.jucongyuan.aidlserver.service.ServerService");
        bindService(intent, conn, Context.BIND_AUTO_CREATE);

        findViewById(R.id.btnGet).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    //修改部分
                    Student student = new Student();
                    student.setName("张三");
                    student = connect.getStr(student);
                    tv.setText(student.getName() + "的学号是:" + student.getNumber());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });

    }
}

除了上面注释的修改部分,其余部分客户端都没有变化,接下来是服务端,服务端需要怎么做呢,和上面的步骤一样,增加实体类、增加aidl文件、修改aidl中的方法,不过要注意,增加的位置要和客户端一模一样,所以我们可以直接把aidl文件的整个内容和实体类从客户端拷贝过去。

新的服务端和客服端项目的结构为:

首先aidl里包名、文件名和内容是一致的,实体类所在的包和内容也是一致的。拷贝完成后Make Project,同样会报错,因为方法已经变了,接着修改ServerService

package com.jucongyuan.aidlserver.service;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;

import com.jucongyuan.IConnect;
import com.jucongyuan.Student;

public class ServerService extends Service {

    private String str;

    IConnect.Stub stub = new IConnect.Stub() {
        // 修改部分
        @Override
        public Student getStr(Student s) throws RemoteException {
            s.setNumber(str);
            return s;
        }
    };

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        str = intent.getStringExtra("str");
        return Service.START_NOT_STICKY;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }
}

修改部分重新实现了getStr()方法,将输入框中的数字设置为该学生的学号再返回Student

测试

运行服务端,输入87564,点击下面的按钮:

运行客户端,点击按钮:

同样的,可以多测试几次。

注意事项

  • 客户端、服务端aidl文件的所有定义必须完全一致
  • 客户端、服务端实体类的内容和所在包必须完全一致
  • IConnect.aidl里方法的参数前面有一个in不能省略,不然Make Project会报错

项目源码

AIDLServer

AIDLClient

小结

AIDL的使用大致就是这些了,没有在更复杂的用法了,如果复杂也是用于项目后架构上的复杂,使用就这些内容了。不过刚也提到过,AIDL是一种定义语言,运行时没有任何意义,Android用它是为了让我更方便的使用进程间通讯,在后面,我们可以绕过AIDL直接使用Android中的类来实现一套自己的进程间通讯,不过再这之前还要了解更多Android中进程间通讯的设计和实现,在后面我们会逐步的介绍。

坚持原创分享,您的支持将鼓励我不断前行!