Notes: Android中AIDL的基本用法

早些时候就听说过AIDL,也常在各种Android面试题、教程甚至大牛采访中看到过它的身影。可见AIDL在Android开发中的地位十分的重要。

于是决定先从AIDL的一些基本概念和基本用法开始着手学习它,下面是一些整理的笔记。

AIDL的全称为Android Interface Definition Language, 顾名思义,它主要就是用来定义接口的一种语言:

AIDL (Android Interface Definition Language) is similar to other IDLs you might have worked with. It allows you to define the programming interface that both the client and service agree upon in order to communicate with each other using interprocess communication (IPC). On Android, one process cannot normally access the memory of another process. So to talk, they need to decompose their objects into primitives that the operating system can understand, and marshall the objects across that boundary for you. The code to do that marshalling is tedious to write, so Android handles it for you with AIDL.

Android Developer的官方文档中对AIDL做了很好的概括。当作为客户的一方和要和作为服务器的一方进行通信时,需要指定一些双方都认可的接口,
这样才能顺利地进行通信。而AIDL就是定义这些接口的一种工具。为什么要借助AIDL来定义,而不直接编写接口呢(比如直接通过Java定义一个Interface)?
这里涉及到进程间通信(IPC)的问题。和大多数系统一样,在Android平台下,各个进程都占有一块自己独有的内存空间,各个进程在通常情况下只能访问自己的独有的内存空间,而不能对别的进程的内存空间进行访问。
进程之间如果要进行通信,就必须先把需要传递的对象分解成操作系统能够理解的基本类型,并根据你的需要封装跨边界的对象。而要完成这些封装工作,需要写的代码量十分地冗长而枯燥。因此Android提供了AIDL来帮助你完成这些工作。

AIDL的功能来看,它主要的应用场景就是IPC。虽然同一个进程中的client-service也能够通过AIDL定义接口来进行通信,但这并没有发挥AIDL的主要功能。
概括来说:

  1. 如果不需要IPC,那就直接实现通过继承Binder类来实现客户端和服务端之间的通信。
  2. 如果确实需要IPC,但是无需处理多线程,那么就应该通过Messenger来实现。Messenger保证了消息是串行处理的,其内部其实也是通过AIDL来实现。
  3. 在有IPC需求,同时服务端需要并发处理多个请求的时候,使用AIDL才是必要的

在了解了基本的概念和使用场景之后,使用AIDL的基本步骤如下:

  1. 编写.AIDL文件,定义需要的接口
  2. 实现定义的接口
  3. 将接口暴露给客户端调用

下面通过实现一个简单的远程Bound Service来练习这几个步骤:

1. 编写.AIDL文件,定义需要的接口

在Android Studio下,右键src文件夹,选择新建AIDL文件,并填写名字,这里我命名为IRemoteService

new_aidl.png

点击Finish按钮之后,会发现main下多了一个名字为AIDL的目录,目录下的包名和Java的包名保持一致,包下即是新建的IRemoteService.aidl文件。
内容我们编写如下:

    // IRemoteService.aidl
    package learn.android.kangel.learning;
    // Declare any non-default types here with import statements
    import learn.android.kangel.learning.HelloMsg;

    interface IRemoteService {

        HelloMsg sayHello();
    }

AIDL的写法和Java十分类似,这里我定义了一个sayHello()方法,用来获取一个从服务端返回的消息HelloMsg
这里的HelloMsg是我自己定义的一个类型。默认情况下,AIDL支持下列所述的数据类型:

  • 所有的基本类型(int、float等)
  • String
  • CharSequence
  • List
  • Map

其中,List和Map中的元素类型必须是上述类型之一或者由其他AIDL生成的接口类型,或者是已经声明的Pacelable类型。
List类型可以指定泛型类,比如写成List<String>, 并且对方接收到的具体实例都是ArrayList
Map类型不支持指定泛型类,比如Map<String,String>。只能Map表示类型,并且对方接收到的具体实例都是HashMap

在这个IRemoteService例子中,我们希望在进程间传递一个HelloMsg对象:他的定义如下:

    /*HelloMsg.java*/
    public class HelloMsg {
        private String msg;
        private int pid;

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }

        public int getPid() {
            return pid;
        }

        public void setPid(int pid) {
            this.pid = pid;
        }

        public HelloMsg(String msg, int pid) {
            this.msg = msg;
            this.pid = pid;
        }
    }

为了让HelloMsg能够在进程间传递, 它必须实现Parcelable接口,Parcelable是Android提供的一种序列化方式,如果嫌手写麻烦的话,通过插件我们可以十分快捷为现有的类添加Parcelable实现:

    /*HelloMsg.java*/
    import android.os.Parcel;
    import android.os.Parcelable;
    
    public class HelloMsg implements Parcelable {
        private String msg;
        private int pid;
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    
        public int getPid() {
            return pid;
        }
    
        public void setPid(int pid) {
            this.pid = pid;
        }
    
        public HelloMsg(String msg, int pid) {
    
            this.msg = msg;
            this.pid = pid;
        }
    
        @Override
        public int describeContents() {
            return 0;
        }
    
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(this.msg);
            dest.writeInt(this.pid);
        }
    
        protected HelloMsg(Parcel in) {
            this.msg = in.readString();
            this.pid = in.readInt();
        }
    
        public static final Parcelable.Creator<HelloMsg> CREATOR = new Parcelable.Creator<HelloMsg>() {
            @Override
            public HelloMsg createFromParcel(Parcel source) {
                return new HelloMsg(source);
            }
    
            @Override
            public HelloMsg[] newArray(int size) {
                return new HelloMsg[size];
            }
        };
    }

定义好HelloMsg.java之后,还需要新增一个与其同名的AIDL文件。那么同样按照刚才的步骤右键src文件夹,添加一个名为HelloMsg的AIDL文件。
这个AIDL的编写十分简单,只需要简单的声明一下要用到的Pacelable类即可,有点类似C语言的头文件,这个AIDL文件是不参与编译的:

// HelloMsg.aidl
package learn.android.kangel.learning;

parcelable HelloMsg;

注意到parcelable的首字母是小写的,这算是AIDL一个特殊的地方。
接下来还需要再IRemoteService.aidl文件中使用import关键字导入这个HelloMsg类型。详细的写法参考上面的IRemoteService.aidl代码。
即便IRemoteService.aidlHelloMsg.aidl位于同一个包下,这里的import是必须要有的。这也是AIDL一个特殊的地方。

好了,至此编写.AIDL文件的步骤就基本结束了,这个时候需要make project或者make对应的module,Android SDK就会根据我这里编写的.AIDL文件生成对应的Java文件。
在Android Studio下,可以在build/generated/aidl目录下找到这些Java文件。

查看IRemoteService.java,可以看到其内部有一个静态抽象类Stub,这个Stub继承自Binder类,并抽象实现了其父接口,这里对应的是IRemoteService这个接口:

public static abstract class Stub extends android.os.Binder implements learn.android.kangel.learning.IRemoteService

Stub类除了声明了IRemoteService.aidl中的所有方法,还提供了一些有用的helper方法,比如asInterface():

public static learn.android.kangel.learning.IRemoteService asInterface(android.os.IBinder obj)

这个方法接受一个Binder对象,并将其转化成Stub对应的接口对象(也就是这里的IRemoteService)并返回。

对于这些生成的Java文件的进一步研究和学习可以帮助我们更好地理解Android的Binder,我会在之后发布的学习笔记中做相应的记录(挖坑233)

2. 实现定义的接口

要实现定义的接口,只需要继承自生成的Binder类,并实现其中的方法即可:

IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        @Override
        public HelloMsg sayHello() throws RemoteException {
            return new HelloMsg("msg from service at Thread " + Thread.currentThread().toString() + "\n" +
                    "tid is " + Thread.currentThread().getId() + "\n" +
                    "main thread id is " + getMainLooper().getThread().getId(), Process.myPid());
        }
    };

这里的实现十分简单,返回一个HelloMsg,消息部分是当前线程的信息,当前线程的id,以及主线程的id,Process Id部分就是当前进程的Id

3. 将接口暴露给客户端调用

在此之前需要了解Bound Service,关于Bound Service的具体细节可以参考这里,本次笔记不再做额外记录。

需要注意一点,如果希望多个Application都能够通过这个接口与服务端通信,那么所有使用这个接口的Application的src目录下都要有对应.aidl文件的副本。

在这个例子中我们编写一个名为RemoteServiceService类,并在onBind()方法中返回上述第二步中实现的接口,这样就把接口传给了客户端供其调用:

package learn.android.kangel.learning;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.widget.Toast;

/**
 * Created by Kangel on 2016/7/21.
 */

public class RemoteService extends Service {

    IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        @Override
        public HelloMsg sayHello() throws RemoteException {
            return new HelloMsg("msg from service at Thread " + Thread.currentThread().toString() + "\n" +
                    "tid is " + Thread.currentThread().getId() + "\n" +
                    "main thread id is " + getMainLooper().getThread().getId(), Process.myPid());
        }
    };

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

以上三步完成之后,我们来继续完善这个例子来进行一些测试:

编写作为客户端的Activity:

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

/**
 * Created by Kangel on 2016/7/21.
 */

public class ClientActivity extends AppCompatActivity {
    private IRemoteService mRemoteService = null;
    private boolean mBind = false;
    private TextView mPidText;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.acticity_client);
        mPidText = (TextView) findViewById(R.id.my_pid_text_view);
        mPidText.setText("the client pid is " + Process.myPid());
    }


    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, RemoteService.class);
        bindService(intent, mConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        unbindService(mConnection);
        mBind = false;
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mRemoteService = IRemoteService.Stub.asInterface(service);
            mBind = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteService = null;
            mBind = false;
        }
    };

    public void onButtonClick(View view) {
        switch (view.getId()) {
            case R.id.show_pid_button:
                if (mBind) {
                    try {
                        Log.i("HELLO_MSG", "the service pid is " + mRemoteService.sayHello().getPid());
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.say_hello_button:
                if (mBind) {
                    try {
                        Log.i("HELLO_MSG", mRemoteService.sayHello().getMsg());
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }

                }
                break;
        }
    }
}

布局文件中有两个Button和一个TextView,Button的点击事件都在xml文件中完成了注册。分别用来获取服务端返回的Pid和返回的Msg。
TextView用于展示当前Activity所在线程的id。

onServiceConnected()回调中,我们使用IRemoteService.Stub.asInterface(Binder)方法返回我们的接口的引用。接着客户端就可以通过它来对服务端发送请求了。
onButtonClick()方法中就是对接口的调用。

如果客户端和服务端处于同一个进程,onServiceConnected()回调中,是可以通过强制类型转换将返回的Binder对象转换为我们需要的接口对象的,像这样:

mRemoteService = (IRemoteService) service;

但如果客户端和服务端处于不同进程,执行这样的强转,系统会报错:

java.lang.ClassCastException: android.os.BinderProxy cannot be cast to learn.android.kangel.learning.IRemoteService

我的对此理解是,由于不同进程之间的内存空间是不能够互相访问的,A进程中的对象当然也就不能为B进程所理解。因此强制类型转换只适用于同一个进程中。

在Manifest中声明作为服务端的Service和作为客户端的Acticity

<activity android:name=".ClientActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
</activity>
<service
            android:name=".RemoteService"
            android:process=":remote" />

在这里我为RemoteService设置了process属性,让它运行在与默认进程不同的进程中。

接下来运行我们的应用:

client_activity.png

可以看到客户端进程id为31704
尝试点击两个按钮,查看Log:

log_1.png

可以看到服务端的进程id为31720,不同于客户端进程。
而且可以看到,service所在的主线程id为1,而处理该请求的线程id为4621。

来自远程进程的调用分发自系统为你的进程所维持的一个线程池中。这也许有点难理解。假如你通过AIDL实现了一个远程服务端的接口,然后有另外一个客户端进程调用了该接口中的方法,因为客户端和你所实现的服务端处于两个不同的进程,
因此客户端对于你而言,就是一个远程进程。当客户端对接口进行调用时,调用过程并不是由客户端进程进行处理的。而是由系统进行封装后,传递到服务端进程所持有的一个线程池中进行处理。最终线程池中的其中一个线程会被用来执行调用的具体逻辑。
而具体选择哪个线程来进行处理,是无法提前预知的。
因此作为服务端接口的实现者,应该能够处理多线程并发的情况,时刻准备好处理来自未知线程的调用,并能保证AIDL接口的实现是线程安全的。

如果服务端和客户端处于同一个进程,那么服务端将会在与发起请求的客户端所处的相同线程上处理该请求。把上述android:process=":remote"属性去掉,则可以对其进行验证。
但这种单进程的情况,AIDL的使用实际上是完全没必要的。

参考链接:

AIDL
Bound Services

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容