Android面试一天一题(Day 36:AIDL)

作者: goeasyway | 来源:发表于2017-02-20 10:25 被阅读5575次

    上一章节,我们讲到了Android中的Binder机制,一个Android开发天天用到,但又不明就理的神密存在。这一节接着Binder这个话题,讲一讲AIDL,让大家对Binder机制有一个直观的认识。

    面试题:AIDL是什么?你有使用过它吗,它支持哪些数据类型?

    AIDL是Android Interface Definition Language的简写,即Android接口定义语言。我们知道Android系统为每一个应用开启一个独立的虚拟机,每个应用都运行在各自进程里(默认情况下),彼此之间相互独立,无法共享内存。当一个应用想要访问另一个应用的数据或调用其方法,就要用到Android系统提供的IPC机制。而AIDL就是Android实现IPC机制的方式之一。

    除了AIDL,Android还提供了Messenger来实现跨进程通信,不过Messenger是以单线程串行方式(消息队列)来处理来自不同客户端的访问的,并不适合多线程并发访问。当需要提供跨进程以及多线程并发服务时就需要AIDL上场了。

    Messenger实际上也是以AIDL作为其底层结构。

    其实要应对AIDL相关的面试题,除了了解清楚它的作用和注意的事项外,最有效的手段莫过于自己动手写几次。这里给大家定两个小目标:

    • 会使用AIDL进行进程间通信;
    • 会手写AIDL的编码,加深对Binder机制的理解。

    创建AIDL

    我们先实现第一个,在Android Studio中创建一个简单的AIDL项目,实现IPC通信。

    Step1. 创建.aidl文件
    我们在对应的src的Package下创建一个AIDL文件(Android Studio->File->New->AIDL->AIDL file),创建后Android Studio会自动把这个.aidl文件放到一个aidl的目录下。

    Android SDK Tool会根据我们的.aidl文件自动生成一个同名的.java文件,如:AIDLTest/app/build/generated/source/aidl/debug/net/goeasyway/aidltest/IRemoteService.java

    basicTypes方法中给我们展示了AIDL支持的基本数据类型,除此之外,AIDL还支持:CharSequence, List & Map(List和Map中的所有元素都必须是AIDL支持的数据类型、其他AIDL生成的接口或您声明的可打包类型。)

    Step2. 创建一个Service暴露AIDL接口并实现AIDL的接口函数
    如下代码,创建一个Service:

    public class RemoteService extends Service {
        public RemoteService() {
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            return binder; //暴露给客户端
        }
    
        // 实现AIDL接口
        private final IRemoteService.Stub binder = new IRemoteService.Stub() {
    
            @Override
            public int getPid() throws RemoteException {
                return Process.myPid();
            }
    
            @Override
            public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
                                   double aDouble, String aString) throws RemoteException {
    
            }
        };
    }
    

    然后在MainActivity通过bindService绑定这个服务,即可以获得AIDL的接口调用的引用。运行前,我们设置一下AndroidManifest.xml文件记这个Service运行在一个单独的进程中:

            <service
                android:name=".RemoteService"
                android:process=":remote"
                android:enabled="true"
                android:exported="true"/>
    

    现在来bindService:

    public class MainActivity extends AppCompatActivity {
        private final static String TAG = "MainActivity";
        private IRemoteService remoteService;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Intent intent = new Intent();
            intent.setClass(this, RemoteService.class);
            bindService(intent, connection, Service.BIND_AUTO_CREATE); // 绑定服务
        }
    
    
        private ServiceConnection connection = new ServiceConnection() {
            public void onServiceConnected(ComponentName className, IBinder service) {
                remoteService = IRemoteService.Stub.asInterface(service); //获取AIDL的接口实现引用
                try {
                    Log.i(TAG, "Client pid= " + Process.myPid());
                    Log.i(TAG, "RemoteService pid= " + remoteService.getPid());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
    
            public void onServiceDisconnected(ComponentName className) {
                Log.e(TAG, "Service has unexpectedly disconnected");
                remoteService = null;
            }
        };
    }
    

    从输出的日志看到Service和Activity运行在两个不同的进程中:

    02-05 09:51:40.154 18992-18992/net.goeasyway.aidltest I/MainActivity: Client pid= 18992
    02-05 09:51:40.154 18992-18992/net.goeasyway.aidltest I/MainActivity: RemoteService pid= 19022
    

    到这里,我们完成了一个AIDL的范例,有几个地方可能会对我们造成困扰:

    • Stub类:Binder的实现类,服务端需要实现这个类来提供服务。
    • asInterface函数: 一个静态函数,用来将IBinder转换成对应的Binder的引用。先通过queryLocalInterface查询,如果服务端和客户端都是在同一个进程,那么就不需要跨进程了,直接将IRemoteService当做普通的对象来使用,否则会返回远程对象的代理对象(Proxy)。
    public static net.goeasyway.aidltest.IRemoteService asInterface(android.os.IBinder obj)
    {
        if ((obj==null)) {
            return null;
        }
        android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
        if (((iin!=null)&&(iin instanceof net.goeasyway.aidltest.IRemoteService))) {
            return ((net.goeasyway.aidltest.IRemoteService)iin);
        }
        return new net.goeasyway.aidltest.IRemoteService.Stub.Proxy(obj);
    }
    

    通过IPC传递对象

    现在,我们加大一下难度,AIDL的接口使用一个我们自己定义的类为参数(或者返回值)。实现步骤如下:

    • 添加一个自定义对象类,并且要实现Parcelable接口,如MyProcess.java;
    • 在AIDL目录下的相同Pacage下添加一个同名的AIDL文件,如MyProcess.aidl;

    注意:通过“Android Studio->File->New->AIDL->AIDL file”不让你创建和MyProcess.java同名的AIDL文件,你可以直接用通过“Android Studio->File->New->File”创建一个MyProcess.aidl。

    • 在AIDL接口类中添加一个接口函数,使用MyProcess做为参数或者返回值;

    其他的细节大家可以直接查看Github上的代码:https://github.com/goeasyway/AIDL_Test (或者查看提交的说明找到具体每次的代码区别:https://github.com/goeasyway/AIDL_Test/commits/master

    in、out & inout
    这节我们看到“MyProcess getProcess(in MyProcess clientProcess);”这个接口的参数有一个“in”修饰符,这也是一个常见的面试题,可以考察一下对方是否真的写过AIDL的代码。

    问题:AIDL中的接口函数有时会使用in、out或者inout的参数修饰符,它们各表示什么意思?在什么情况下要使用呢?

    in、out和inout表示数据的流向。大家可以把AIDL的客户端和服务端理解成两个进程(其实大多数情况也是这样才会使用AIDL),从客户端流向服务端用in表示,表示这个对象是从客户端中传递到服务端,在服务端修改这个对象不会对客户端输入的对象产生影响。

    而out则表示,数据只能从服务端影响客户端,即客户端输入这个参数时,服务端并不能获取到客户端的具体实例中的数据,而是生成一个默认数据,但是服务端对这个默认数据的修改会影响到客户端的这个类对象实例发生相应的改变。

    理解了in、out之后,inout自然不需要再解释了。AIDL默认支持的数据类型使用in修饰符,对于我们自定义的Parcelable对象,一般情况下我们也是使用in,如果没有必要,应该尽量避免inout。

    Intent也是Parcelable实现
    也许你会想到,我们在Activity间可以通过Intent携带参数,其实你去看的源码的话会发现Intent也是一个Parcelable的实现类,而且在系统的工程中也有一个Intent.aidl文件(路径:/frameworks/base/core/java/android/content/Intent.aidl)。所以,它才可以在进程间传递。

    注:目前Client端和Server端在同一工程中,如果分开在不同的工程的话,Client端所在的工程要把Server端提供的.aidl复制到同名Pacage的AIDL代码目录下。

    手动方式创建AIDL(不依赖AIDL工具,手写远程AIDL的代码完成跨进程通信)

    通过AIDL,可以让本地调用远程服务的接口就像调用本地接口那么简单,让用户无需关注内部细节,只需要实现自己的业务逻辑接口,内部复杂的参数序列化发送、接收、客户端调用服务端的逻辑,用户并不需要关心。

    AIDL的代码生成器,已经根据.aidl文件自动帮我们生成Proxy、Stub(抽象类)两个类,并且把客户端代理mRemote的transact()过程以及服务器端的onTtransact()过程默认实现好了,我们只需要在服务端继承Stub,实现自己的业务方法即可。

    但现在,为了进一步加深对Binder机制的理解,我们来做一个手动实现编写AIDL相关代码的练习。

    具体的代码大家可以参考:https://github.com/goeasyway/AIDL_Test/tree/master/app/src/main/java/net/goeasyway/aidltest/diy

    这个包里有三个类:IRmote.java为接口,Stub.java为Binder实现类(Service端要实例化它并在onBind返回),Proxy.java为代理类,提供给客户端使用的,通过Binder驱动和服务端通信。

    大家可以看到这个练习不需要.aidl文件。

    在这几个代码中,大家需要搞清楚这个类(或者接口)的关系:

    • Binder
      Binder本地对象。
    • IBinder
      IBinder是一个接口,它代表了一种跨进程传输的能力。
    • IInterface
      IBinder负责数据传输,那么client与server端的调用契约呢?这里的IInterface代表的就是远程server对象具有什么能力。具体来说,就是aidl里面的接口。
    • Proxy
      代表远程进程的Binder对象的本地代理,继承自IBinder,因而具有跨进程传输的能力。实际上,在跨越进程的时候,Binder驱动会自动完成代理对象和本地对象的转换。
    • Stub
      这个类继承了Binder, 说明它是一个Binder本地对象,它实现了IInterface接口,表明它具有远程Server承诺给Client的能力;Stub是一个抽象类,具体的IInterface的相关实现需要我们手动完成。

    如果觉得有点难理解的话,不妨先动手写了再来看。

    小结

    如何使用AIDL应该是一个高级工程师必备技能,如果你之前不太了解它的话,那么我强烈建议你完成上面的两个小练习,再回过头去看它的解说。

    之后,你将不再惧怕和AIDL相关的面试题。

    相关文章

      网友评论

      • Jdqm:关于in、out、inout这3个定向tag,可以参考 https://www.jianshu.com/p/382633129b53
      • sendtion:看起来有点吃力
      • 糖葫芦_倩倩:看不太懂后面的
      • 9b4f2400ea72:这一系列文章,没有一个专门的分类么,找起来真的太累了。。
      • 我是一只小壳蟆:先照着写了,没打印显示
        在简书已被:@阿姆斯特狸 是不是没有配置成远程服务
        阿姆斯特狸:private final IRemoteService.Stub binder = new IRemoteService.Stub() {

        @Override
        public int getPid() throws RemoteException {
        return Process.myPid();
        }

        @Override
        public MyProcess getProcess(MyProcess clientProcess) throws RemoteException {
        clientProcess.pid=2;
        return clientProcess;

        }

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
        double aDouble, String aString) throws RemoteException {

        }
        };

        private ServiceConnection connection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
        remoteService =IRemoteService. Stub.asInterface(service); //获取AIDL的接口实现引用
        try {
        MyProcess clientProcess = new MyProcess(Process.myPid(), MainActivity.this.getPackageName());
        MyProcess myProcess = remoteService.getProcess(clientProcess);
        Log.i(TAG, "RemoteService pName = " + myProcess.name);
        Log.i(TAG, "RemoteService pid= " + myProcess.pid);
        } catch (RemoteException e) {
        e.printStackTrace();
        }
        }

        public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        remoteService = null;
        }
        };

        打印结果是:
        03-16 11:22:02.539 19934-19934/net.goeasyway.aidltest I/MainActivity: RemoteService pName = net.goeasyway.aidltest
        03-16 11:22:02.539 19934-19983/net.goeasyway.aidltest W/OpenGLRenderer: Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED, retrying without...
        03-16 11:22:02.539 19934-19934/net.goeasyway.aidltest I/MainActivity: RemoteService pid= 2
        为什么使用in 在service端更改变量扔能影响客户端的变量?
        我是一只小壳蟆:自己写倒是调试出来了:grin:

      本文标题:Android面试一天一题(Day 36:AIDL)

      本文链接:https://www.haomeiwen.com/subject/ebhhittx.html