欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AIDL使用与踩坑部分总结

程序员文章站 2022-05-28 11:54:23
...

整理一下AIDL相关的部分信息,也算是总结一下重新回顾一下知识吧~

什么是AIDL?

AIDL(Android Interface Definition Language) Android接口定义语言 利用它定义客户端与服务均认可的编程接口,以便二者使用进程间通信 (IPC) 进行相互通信。实际上起作用的并不是AIDL文件,而是根据AIDL生成的实例代码,AIDL是安卓替我们设计好的一个模板,根据模板生成Interface的代码。

ADIL的存在就是为了实现进程间的通信,通过AIDL我们可以在不同进程中进行数据通信与逻辑交互。

一般使用都是有两个端:客户端与服务端

支持参数类型

  • 基本数据类型:boolean、byte、char、float、double、int、long
  • String 、CharSequence
  • List类型 (包含的数据必须是AIDL支持的类型或者其它声明的AIDL对象)
  • Map类型(包含的数据必须是AIDL支持的类型或者其它声明的AIDL对象)
  • 实现了Parcelable接口的数据类型 (例如:Bundle)
  • AIDL接口本身

(网上人家说 不支持short,说是因为Parcel没有办法对short进行序列化,但是我发现里面的writeValue()中有short的类型的处理。)
但是我一旦在AIDL参数添加了short类型的时候就一直报错再build的时候就一直报错不行
Process ‘command ‘D:\Android\sdk\build-tools\28.0.3\aidl.exe’’ finished with non-zero exit value 1 详细的报的是arguments 报错 可能是参数为short的时候不支持
这个问题如果有懂的同学麻烦下面评论提及一下,大恩不言谢~~

AIDL的使用

  • 先定义两端统一的包名跟接口aidl文件

AIDL使用与踩坑部分总结

IoneAidlInterface.aidl: 暴露给客户端使用的接口类,
(记得要导包,把数据类型或者引用到的实体类所在的包名明确标明导入!!)

package testview.zhen.com.myapplication;
import testview.zhen.com.myapplication.PersonBean;
import testview.zhen.com.myapplication.IPersonBeanCallBack;

interface IOneAidlInterface {
    void basis(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
     PersonBean getPerson();
    void setPerson(in PersonBean a);
    void registerCallback(IPersonBeanCallBack callback);
    void unregisterCallback(IPersonBeanCallBack callback);

}

IPersonBeanCallBack.aidl : 在IoneAidlInterface.aidl里面使用的回调对象声明类

package testview.zhen.com.myapplication;

interface IPersonBeanCallBack {
   void getName(String name);
   void getAge(int age);
}

PersonBean.aidl : 在IoneAidlInterface.aidl里面使用的实体类的一个声明文件,要声明了才能在aidl中使用,该实体类也必须实现接口parcelable

package testview.zhen.com.myapplication;//类所在的包地址
parcelable PersonBean; 

上面的都是aidl格式的文件,下面是实现的PersonBean.kt实体类文件

package testview.zhen.com.myapplication

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

/**
 * Create by ldr
 * on 2019/11/5 16:14.
 */
class PersonBean() :Parcelable{
     var name: String=""
    var age: Int? = null

    constructor(parcel: Parcel) : this() {
        name = parcel.readString()
        age = parcel.readValue(Int::class.java.classLoader) as? Int
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeValue(age)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<PersonBean> {
        override fun createFromParcel(parcel: Parcel): PersonBean {
            return PersonBean(parcel)
        }

        override fun newArray(size: Int): Array<PersonBean?> {
            return arrayOfNulls(size)
        }
    }
}

AIDL使用与踩坑部分总结

上面有定义的几个文件, 客户端服务端都要有并且包名必须要保持一致

  • 创建服务端

  1. AidlService : 实现 Service
class AidlService :Service(){
    companion object{
        val TAG = "AidlService"
    }
    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
    //这个适用于回调的使用的
    var remoteCallbackList =  RemoteCallbackList<IPersonBeanCallBack>()

    private val binder = object: IOneAidlInterface.Stub() {
        override fun registerCallback(callback: IPersonBeanCallBack?) {
            remoteCallbackList.register(callback)
         Log.i(TAG,"服务端注册回调")
        }
        override fun unregisterCallback(callback: IPersonBeanCallBack?) {
            remoteCallbackList.unregister(callback)
       Log.i(TAG,"服务端取消回调")
        }
        override fun basis(anInt: Int, aLong: Long, aBoolean: Boolean, aFloat: Float, aDouble: Double, aString: String?) {
                Log.i(TAG,"得到客户端传入的数据anInt=${anInt},aLong=${aLong}" +
                        "aBoolean=${aBoolean} , aFloat=${aFloat}, aDouble=${aDouble}, aString=${aString}")
        }
        override fun getPerson(): PersonBean {
            return  PersonBean().also {
                    it.name = "小明"
                    it.age = 18
            }
        }
        //设置人名的时候回调一下通知给客户端
        override fun setPerson(a: PersonBean?) {
           Log.i(TAG,"得到客户端传入的实体Bean信息名字=${a!!.name},岁数=${a!!.age}")
          //关键代码1
            //从remoteCallbackList获取回调的对象并调用回调对象中的方法
            var n =   remoteCallbackList.beginBroadcast()
             for (i in 0 until n){
                remoteCallbackList.getBroadcastItem(i).getName(a!!.name)
                remoteCallbackList.getBroadcastItem(i).getAge(a!!.age!!)
            }
            remoteCallbackList.finishBroadcast()
        }
//关键代码2 
  //这里用于捕获里面出现的异常,防止服务端有错误的异常直接抛向客户端
        override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
            try{
                return  super.onTransact(code, data, reply, flags)
            }catch (e:RuntimeException){
                Log.w(TAG, "Unexpected remote exception", e)
              throw e
            }
        }
    }
}

onbind()的方法中将IOneAidlInterface.Stub()的对象进行绑定。remoteCallbackList则是用于注册回调使用的,callback的注册跟取消注册在这里。
关键代码1:remoteCallbackList.getBroadcastItem(i)用于获取回调对象, beginBroadcastfinishBroadcast 必须配套使用。
关键代码2:这个是当服务端这边自己被调用的函数出现问题,然后让客户端抛出java.lang.NullPointerException的时候进行自己异常的捕获跟打印 不然客户端莫名其妙抛出,但是你服务端自己却没有捕获到异常定位不到异常的抛出真正地方(参考CSDN:https://blog.csdn.net/zxfrdas/article/details/51009691
具体可看一下这篇文章)
2. 在AndroidManifest.xml

        <service android:name=".service.AidlService" android:exported="true">
            <intent-filter>
                <action android:name="testview.zhen.com.myapplication.service.aidservice"></action>
                <category android:name="android.intent.category.DEFAULT"></category>
            </intent-filter>
        </service>

android:exported="true"这个属性记得加上去,外部才能访问,并且添加<acition>节点,调用的时候可以使用acition来调用 ,当然你也可以直接指定Service类名方式来绑定Service

  • 创建客户端

class MainActivity : AppCompatActivity() {

    companion object{
        val TAG = "MainActivity"
    }

    var isConnection = false
    lateinit var iOne:IOneAidlInterface
  //关键代码1
    private var servirveConnection = object : ServiceConnection {
        override fun onServiceDisconnected(name: ComponentName?) {
            isConnection =false
        }
//关键代码2
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            iOne =  IOneAidlInterface.Stub.asInterface(service)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            if (isConnection) aaa@qq.com
            iOne.basis(1,1000*1000,true,1.0f,1.00,"hello world")
        }
  //关键代码3
        var intent = Intent().also {
            it.setPackage("testview.zhen.com.myapplication")
            it.action = "testview.zhen.com.myapplication.service.aidservice"
        }
        bindService(intent,servirveConnection,Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isConnection) unbindService(servirveConnection)
    }

}

关键代码1 中创建了 servirveConnection对象,其并在匿名内部类中实现的onServiceConnected(name: ComponentName?, service: IBinder?)iOne = IOneAidlInterface.Stub.asInterface(service)iOne实例化 ,最后关键代码3,将intentpackageaction 设置为服务端的service配置信息,传入 bindService() 实现绑定。

以下调用,具体详情代码请看上面的几个类的方法中的具体逻辑

  1. 点击客户端 MainActivity的按钮 button1
        button1.setOnClickListener {
            if (isConnection) aaa@qq.com
            iOne.basis(1,1000*1000,true,1.0f,1.00,"hello world")
        }

服务端AidlService.kt打印的信息

2019-11-06 16:03:30.277 5195-5210/testview.zhen.com.myapplication I/AidlService: 得到客户端传入的数据anInt=1,aLong=1000000aBoolean=true , aFloat=1.0, aDouble=1.0, aString=hello world

ok~~ 大功告成 基础的调用调通了~
2. 客户端获取getPerson()返回person对象

        button2.setOnClickListener {
            if (isConnection) aaa@qq.com
            // 调用Person()方法 获取PersonBean对象
            var person =  iOne.person
            Log.i(TAG,"从服务端获取回来的Person对象信息name = ${person.name},age =${person.age}")
        }

客户端打印的信息:

2019-11-06 17:59:33.702 30666-30666/com.mx.testaidldemo I/MainActivity: 从服务端获取回来的Person对象信息name = 小明,age =18
  1. 注册回调 返回回调信息

3.1 客户端回调方法逻辑

private var  istb:IPersonBeanCallBack.Stub =  object : IPersonBeanCallBack.Stub() {
        override fun getName(name: String?) {
                Log.d(TAG,"获取到回调的名字信息name = ${name}")
        }
        override fun getAge(age: Int) {
            Log.d(TAG,"获取到回调的岁数信息age = ${age}")
        }
    }

3.2 客户端调用注册回调 调用setPerson

        button3.setOnClickListener {
            if (isConnection) aaa@qq.com
            //注册回调
            iOne.registerCallback(istb)
            Log.i(TAG,"注册回调")
        }
        button4.setOnClickListener {
            if (isConnection) aaa@qq.com
            iOne.person = PersonBean().apply {
                name = "同志"
                age = 40
            }
        }
        button5.setOnClickListener {
            if (isConnection) aaa@qq.com
            //取消回调
            iOne.unregisterCallback(istb)
            Log.i(TAG,"取消回调")
        }

点击button3 注册回调
客户端Log:

2019-11-07 10:20:18.654 17960-17960/com.mx.testaidldemo I/MainActivity: 注册回调

服务端Log:

2019-11-07 10:20:18.652 17671-17694/testview.zhen.com.myapplication I/AidlService: 服务端注册回调

点击button4 iOne.person触发服务端回调,
客户端Log:

2019-11-07 10:22:28.017 17960-17960/com.mx.testaidldemo D/MainActivity: 获取到回调的名字信息name = 同志
2019-11-07 10:22:28.018 17960-17960/com.mx.testaidldemo D/MainActivity: 获取到回调的岁数信息age = 40

服务端Log:

2019-11-07 10:22:28.015 17671-17694/testview.zhen.com.myapplication I/AidlService: 得到客户端传入的实体Bean信息名字=同志,岁数=40

点击button5 注销回调
客户端Log:

2019-11-07 10:29:27.351 20514-20514/com.mx.testaidldemo I/MainActivity: 取消回调

服务端Log:

2019-11-07 10:29:27.350 20291-20306/testview.zhen.com.myapplication I/AidlService: 服务端取消回调

AIDL的回调

RemoteCallbackList 并不是一个List ,不能像 List 一样去操作它,遍历RemoteCallbackList必须要以上面AidlService类的使用方式进行,其中 beginBroadcastfinishBroadcast 必须配套使用,哪怕我们仅仅是想要获取 RemoteCallbackList 中的元素个数,

  • 如果你不使用beginBroadcast ,然后直接就去调remoteCallbackList.getBroadcastItem(0)那么它会返回空给你 而不是你所期待的对象,
  • 如果你不使用finishBroadcast 然后操作步骤 绑定->使用->注销->使用,就会抛异常说 java.lang.IllegalStateException: beginBroadcast() called while already in a broadcast!!!

定向TAG

android官网的文章上

All non-primitive parameters require a directional tag indicating which way the data goes . Either in , out , or inout . Primitives are in by default , and connot be otherwise .
所有的非基本参数都需要一个定向Tag来指出数据流通的方式,基本参数的定向Tag默认是并且只能是in

  • in:表示数据只能由客户端流向服务端
  • out:表示数据只能由服务端流向客户端
  • inout:表示数据可在服务端与客户端之间双向流通
    数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。

AIDL鉴权

关于鉴权相关的可以看一下简书其它作者的这篇文章https://www.jianshu.com/p/69e5782dd3c3

注意事项(各种碰到的大坑小坑)

  1. 在导入以前可以运行的程序出现 AIDL 解析时已到达文件结尾,错误。
    解决: AIDL 中有中文的注释。IDE升级到android studio 3.5以后就会出现这个错误。把所有aidl 的注释删除 运行就可以。

  2. 客户端调用远程服务端的方法时客户端线程会被挂起,如果远程的方法有长时间的耗时操作是会引起我们的客户端ANR现象的,这种情况就要避免在客户端UI线程中访问远程方法;

  3. 当服务端回调客户端的方法时,也要注意耗时的问题。

  4. 由于服务端的方法在服务端的Binder线程池中运行的,方法本身可以进行耗时操作,所以切记不要在服务端方法中开线程进行异步任务;

  5. Binder是会意外死亡的(比如服务被杀了,或者突然中断),每次Aidl意外断开都会调起linkToDeath和onServiceDisconnected方法,只是linkToDeath方法调用在onServiceDisconnected之前,(相关信息跟处理方法,可看看相关的参考有:https://blog.csdn.net/Small_Lee/article/details/79181985
    https://www.jianshu.com/p/9af02aa66da9