AIDL使用與踩坑部分總結

整理一下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文件

aidle文件預覽.png

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)
        }
    }
}

PersonBean.kt所在路徑.png

上面有定義的幾個文件, 客戶端服務端都要有並且包名必須要保持一致

  • 創建服務端

  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) return@setOnClickListener
            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) return@setOnClickListener
            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) return@setOnClickListener
            // 調用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) return@setOnClickListener
            //註冊回調
            iOne.registerCallback(istb)
            Log.i(TAG,"註冊回調")
        }
        button4.setOnClickListener {
            if (isConnection) return@setOnClickListener
            iOne.person = PersonBean().apply {
                name = "同志"
                age = 40
            }
        }
        button5.setOnClickListener {
            if (isConnection) return@setOnClickListener
            //取消回調
            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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章