本文作者:CodingBlock 文章链接:https://cloud.tencent.com/developer/article/1351740
在上一篇博文中介绍了一种轻量级的跨进程通讯方案-Messenger,Messenger实现起来非常简单,其底层原理也是AIDL,更像是一个简易版的AIDL,但简单的东西往往也有其局限性,Messenger的主要作用是传递消息,它无法实现RPC功能也就是无法让我们在客户端本地就能调用远程的方法,而且Messenger是以串行的方式处理,无法同时处理多个请求,只能一个一个的处理。而AIDL就可以很好弥补Messenger的不足,虽然实现起来相对复杂一些,但它功能强大,无疑是跨进程通讯的首选方案。接下来我们先看看AIDL是什么,都可以传递哪些数据,并且本文会用一个小例子来直观的体会AIDL的实现过程。
读完本文你将深入掌握以下几个知识点:
AIDL全称Android Interface Definition Language,即Android接口定义语言。AIDL是Android中可以实现跨进程通讯的一种方案,通过AIDL可以实现RPC方式,所谓RPC是指远程过程调用(Remote Procedure Call),可以简单的理解为就像在本地一样方便的调动远程的方法。在Android的跨进程通讯的方案中,只有AIDL可以实现RPC方式。
接下类用一个小例子来说明AIDL的创建过程及用法,尽管在同一个APP内依然可以指定两个进程,但为了更能凸显“跨进程”这一点,还是决定将此示例借助于两个APP来实现,毕竟在开发中真实的需求也是发生在两个APP中。
在实现AIDL的过程中服务端APP和客户端APP中要包含结构完全相同的AIDL接口文件,包括AIDL接口所在的包名及包路径要完全一样,否则就会报错,这是因为客户端需要反序列化服务端中所有和AIDL相关的类,如果类的完整路径不一致就无法反序列化成功。
小技巧:为了更加方便的创建AIDL文件,我们可以新建一个lib工程,让客户端APP和服务端APP同时依赖这个lib,这样只需要在这个lib工程中添加AIDL文件就可以了!
简要说明一下将要实现的小例子的需求:是一个通讯录,在服务端维护一个List用来存放联系人信息,客户端可以通过RPC方式来添加联系人、获取联系列表等功能。
首先新建一个Contact类,通过上面的介绍我们知道,普通的java类是不能在AIDL中使用的,必须要实现Parcelable接口,并在AIDL文件中声明:
Contact.java
/**
 * Created by liuwei on 18/2/8.
 */
public class Contact implements Parcelable {
    private int phoneNumber;
    private String name;
    private String address;
    public Contact(int phoneNumber, String name, String address) {
        this.phoneNumber = phoneNumber;
        this.name = name;
        this.address = address;
    }
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(phoneNumber);
        dest.writeString(name);
        dest.writeString(address);
    }
    private final static Creator<Contact> CREATOR = new Creator<Contact>() {
        @Override
        public Contact createFromParcel(Parcel source) {
            return new Contact(source);
        }
        @Override
        public Contact[] newArray(int size) {
            return new Contact[size];
        }
    };
    public Contact(Parcel parcel) {
        phoneNumber = parcel.readInt();
        name = parcel.readString();
        address = parcel.readString();
    }
    
    @Override
    public String toString() {
        return "Contact{" +
                "phoneNumber=" + phoneNumber +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}声明Contact类
Contact.aidl
package cn.codingblock.libaidl.contacts;
parcelable Contact;创建AIDL接口文件,声明需要暴露给客户端的方法。
IContactsManager.aidl
package cn.codingblock.libaidl.contacts;
import cn.codingblock.libaidl.contacts.Contact;
interface IContactsManager {
    int getPhoneNumber(in String name);
    String getName(int phoneNumeber);
    Contact getContact(int phoneNumber);
    List<Contact> getContactList();
    boolean addContact(in Contact contact);
}注:在AIDL接口文件中如果引用到了某个类,即使与这个类的AIDL声明在同一个包中也使用import导入此类。
aidl文件最终的结构如下:
小问题:AIDl文件中in、out、inout的区别?
对此问题感兴趣的同学可以查看AIDL所生成的Stub源码。
详细实现如下:ContactManagerService.java
/**
 * Created by liuwei on 18/2/8.
 */
public class ContactManagerService extends Service {
    private final static String TAG = ContactManagerService.class.getSimpleName();
    private CopyOnWriteArrayList<Contact> contacts = new CopyOnWriteArrayList<>();
    @Override
    public void onCreate() {
        super.onCreate();
        contacts.add(new Contact(110, "报警电话", "派出所"));
        contacts.add(new Contact(119, "火警电话", "消防局"));
        contacts.add(new Contact(112, "故障电话", "保障局"));
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new ContactManagerBinder();
    }
    private class ContactManagerBinder extends IContactsManager.Stub{
        /**
         * 根据号码返回手机号
         * @param name
         * @return
         * @throws RemoteException
         */
        @Override
        public int getPhoneNumber(String name) throws RemoteException {
            if (!TextUtils.isEmpty(name)) {
                for (Contact contact:contacts) {
                    if (contact.name.equals(name)){
                        return contact.phoneNumber;
                    }
                }
            }
            return 0;
        }
        /**
         * 根据号码返回名称
         * @param phoneNumber
         * @return
         * @throws RemoteException
         */
        @Override
        public String getName(int phoneNumber) throws RemoteException {
            for (Contact contact:contacts) {
                if (contact.phoneNumber == phoneNumber){
                    return contact.name;
                }
            }
            return null;
        }
        /**
         * 根据号码返回联系人对象
         * @param phoneNumber
         * @return
         * @throws RemoteException
         */
        @Override
        public Contact getContact(int phoneNumber) throws RemoteException {
            for (Contact contact:contacts) {
                if (contact.phoneNumber == phoneNumber) {
                    return contact;
                }
            }
            return null;
        }
        /**
         * 获取联系人集合
         * @return
         * @throws RemoteException
         */
        @Override
        public List<Contact> getContactList() throws RemoteException {
            return contacts;
        }
        /**
         * 添加联系人
         * @param contact
         * @return
         * @throws RemoteException
         */
        @Override
        public boolean addContact(Contact contact) throws RemoteException {
            if (contact != null) {
                return contacts.add(contact);
            }
            return false;
        }
    }
}<service android:name=".aidl.contact.ContactManagerService"
         android:exported="true"/>上面代码很简单,值得一提的是AIDL的方法都是在服务端的Binder线程池中执行的,如果有多个客户端同时请求,就会有多个线程来操作这些方法,本次示例将存放联系人的集合采用了CopyOnWriteArrayList实现,由于CopyOnWriteArrayList本身是线程安全的,所以在此我们不需要做额外的同步处理。
==从上文我们知道,在List中AIDL只支持ArrayList的传输,那么在此处为什么可以使用CopyOnWriteArrayList呢?==
这是因为AIDL支持的是List,之所以说AIDL只支持传递ArrayList
,是因为它在传递其他List类型时就会自动将其他类型在传递之前转换成ArrayList然后再返回给服务端,也就是说无论你在服务端使用其他的任何list的子类型,在客户端接收到的类型都是ArrayList。
所以本次示例中虽然服务端返回的事CopyOnWriteArrayList,但是在Binder中会按照List的规范去读取它并最终形成一个新的ArrayList返回给客户端,类似的还有ConcurrentHashMap对应于HashMap。(其实不光CopyOnWriteArrayList,还有LinkedList等其他的List子类型也都是可以的。)
首先向Intent指定Component,需要传入两个参数,一个是远程Service所在工程包名,另一个是远程Service的全量限定名,然后使用bindService绑定远程Service:
Intent intent = new Intent();
intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService"));
bindService(intent, serviceConnection, BIND_AUTO_CREATE);在serviceConnection中获取返回的Binder并使用IContactsManager.Stub.asInterface()方法将Binder对象转换成IContactsManager类型。
private ServiceConnection serviceConnection = new ServiceConnection(){
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mIContactsManager = IContactsManager.Stub.asInterface(service);
        Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager);
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
        mIContactsManager = null;
        Log.i(TAG, "onServiceDisconnected: ");
    }
};/**
 * Created by liuwei on 18/2/8.
 */
public class ContactMangerActivity extends AppCompatActivity {
    private static final String TAG = ContactMangerActivity.class.getSimpleName();
    private IContactsManager mIContactsManager;
    private EditText et_contact_name;
    private EditText et_contact_phone_number;
    private EditText et_contact_address;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact_manger);
        ViewUtils.findAndOnClick(this, R.id.btn_add_contact, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_phone_number, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_name, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_contact, mOnClickListener);
        ViewUtils.findAndOnClick(this, R.id.btn_get_list, mOnClickListener);
        et_contact_name = ViewUtils.find(this, R.id.et_contact_name);
        et_contact_phone_number = ViewUtils.find(this, R.id.et_contact_phone_number);
        et_contact_address = ViewUtils.find(this, R.id.et_contact_address);
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService"));
        bindService(intent, serviceConnection, BIND_AUTO_CREATE);
        
    }
    private ServiceConnection serviceConnection = new ServiceConnection(){
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mIContactsManager = IContactsManager.Stub.asInterface(service);
            Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mIContactsManager = null;
            Log.i(TAG, "onServiceDisconnected: ");
        }
    };
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_add_contact:
                    Contact contact = new Contact(getEtContactPhoneNumber(), getEtContactName(), getEtContactAddress());
                    try {
                        mIContactsManager.addContact(contact);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case R.id.btn_get_phone_number:
                    String name = getEtContactName();
                    try {
                        Log.i(TAG, "onClick: " + name + "的电话:" + mIContactsManager.getPhoneNumber(name));
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case R.id.btn_get_name:
                    int number = getEtContactPhoneNumber();
                    try {
                        Log.i(TAG, "onClick: " + number + " 对应的名称:" + mIContactsManager.getName(number));
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case R.id.btn_get_contact:
                    int number1 = getEtContactPhoneNumber();
                    try {
                        Contact contact1 = mIContactsManager.getContact(number1);
                        System.out.println(contact1);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case R.id.btn_get_list:
                    try {
                        List<Contact> contacts = mIContactsManager.getContactList();
                        System.out.println(contacts);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
        }
    };
    private String getEtContactName() {
        String str = et_contact_name.getText().toString();
        if (TextUtils.isEmpty(str)) {
            Toast.makeText(this, "请输入联系人名称", Toast.LENGTH_SHORT).show();
            return null;
        }
        return str;
    }
    private int getEtContactPhoneNumber() {
        String str = et_contact_phone_number.getText().toString();
        if (TextUtils.isEmpty(str)) {
            Toast.makeText(this, "请输入联系人电话", Toast.LENGTH_SHORT).show();
            return 0;
        }
        return Integer.valueOf(str);
    }
    private String getEtContactAddress() {
        String str = et_contact_address.getText().toString();
        if (TextUtils.isEmpty(str)) {
            Toast.makeText(this, "请输入联系人地址", Toast.LENGTH_SHORT).show();
            return null;
        }
        return str;
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(serviceConnection);
    }
}布局文件也就几个EditText和Button比较简单,这里就不贴出来了,接下来运行测试一下。
两端都运行后,客户端界面如下图:
查看ipcclient工程的log如下,发现已经成功绑定了远程的Service:
.../cn.codingblock.ipcclient I/ContactMangerActivity: onServiceConnected: mIContactsManager=cn.codingblock.libaidl.contacts.IContactsManager$Stub$Proxy@6b60cb6此时查看ipc工程的log如下:
.../cn.codingblock.ipc I/ContactManagerService: onCreate: ContactManagerService started...
.../cn.codingblock.ipc I/System.out: 现有的联系人:[Contact{phoneNumber=110, name='报警电话', address='派出所'}, Contact{phoneNumber=119, name='火警电话', address='消防局'}, Contact{phoneNumber=112, name='故障电话', address='保障局'}]通过上面两个log说明客户端和服务端已经链接成功了,接下类测试一下各按钮远程方法,在号码输入框中输入110,依次点击获取联系人名称按钮和获取联系人信息按钮,log如下:
.../cn.codingblock.ipcclient I/ContactMangerActivity: onClick: 110 对应的名称:报警电话
.../cn.codingblock.ipcclient I/System.out: Contact{phoneNumber=110, name='报警电话', address='派出所'}接着在三个输入框里面分别输入David,111,david`s home,然后点击添加联系人信息将联系人添加到远程列表里面,在点击获取联系人列表,log如下:
.../cn.codingblock.ipcclient I/System.out: [Contact{phoneNumber=110, name='报警电话', address='派出所'}, Contact{phoneNumber=119, name='火警电话', address='消防局'}, Contact{phoneNumber=112, name='故障电话', address='保障局'}, Contact{phoneNumber=111, name='David', address='david`s home'}]可以看到david的信息已经成功添加进来了。
其实在正式的开发工作中,我们不希望任何客户端都能绑定我们的服务端,因为这会存在极大安全隐患,所以当客户端想我们发来绑定请求是我们需要做权限校验,符合我们权限要求的客户端才可以与我们的服务端建立链接。
添加权限校验可能会有很多方法,没有对错之分,在实际开发中适合就好,接下来我们介绍一种相对来说比较方便的权限验证的方案:
<!--声明权限-->
<uses-permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"/>
<!--定义权限-->
<permission
    android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"
    android:protectionLevel="normal"/>@Nullable
@Override
public IBinder onBind(Intent intent) {
    if (checkCallingOrSelfPermission("cn.codingblock.permission.ACCESS_CONTACT_MANAGER") == PackageManager.PERMISSION_DENIED) {
        Log.i(TAG, "onBind: 权限校验失败,拒绝绑定...");
        return null;
    }
    Log.i(TAG, "onBind: 权限校验成功!");
    return new ContactManagerBinder();
}客户端先不做修改,运行测试一下,此时在客户端已经无法获取服务端的Binder对象,在客户端点击按钮操作时可以看到报空指针异常了:
/cn.codingblock.ipcclient E/AndroidRuntime: FATAL EXCEPTION: main
        Process: cn.codingblock.ipcclient, PID: 4726
        java.lang.NullPointerException: Attempt to invoke interface method 'java.util.List cn.codingblock.libaidl.contacts.IContactsManager.getContactList()' on a null object reference
            at cn.codingblock.ipcclient.aidl.ContactMangerActivity$2.onClick(ContactMangerActivity.java:127)
            at android.view.View.performClick(View.java:6256)
            at android.view.View$PerformClick.run(View.java:24701)
            at android.os.Handler.handleCallback(Handler.java:789)
            at android.os.Handler.dispatchMessage(Handler.java:98)
            at android.os.Looper.loop(Looper.java:164)
            at android.app.ActivityThread.main(ActivityThread.java:6541)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)<!--声明权限-->
<uses-permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"/>
<!--定义权限-->
<permission
    android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"
    android:protectionLevel="normal"/>注意:要在客户端和服务端两个工程中都加入以上声明权限和定义权限的代码。
经反复测试发现:服务端工程中声明权限和定义权限的代码缺一不可,而客户端工程中如果只加入声明权限的代码,那么如果在安装时,客户端APP先于服务端APP安装,客户端就会由于找不到定义权限而无法成功获取权限!
所以为了保险起见,将两端都同时加入定义权限的代码和声明权限的代码,当然本示例中最好的方法是直接统一加入libaidl工程中,一次加入,两端可用!
虽然AIDL在创建的时候步骤比较繁琐,但其功能十分强大。最后概括一下AIDL的创建步骤:
在服务端:
接着在客户端:
最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!
参考文献:
本文作者:CodingBlock 文章链接:https://cloud.tencent.com/developer/article/1351740