AIDL快速使用上手

AIDL快速使用上手

  AIDL即Android接口定义语言,是用来实现跨进程通信的一种模板接口语言,AS可以根据我们编写的AIDL生成对应的Java代码,以方便我们的使用。它底层是使用Binder进行通信的,但是自己手写的话是还是比较麻烦的,因此可以使用AIDL定义接口语言,然后经过构建后就会生成对应的代码,减少我们的工作量。
  之前在学习AIDL的时候也写过Demo,但时间久了就容易忘,当再写的时候又得去查资料,因此这里记录一下AIDL的使用,方便日后查询。

新建AIDL文件

  首先直接右键选择new一个AIDL文件,命名为IMyServer,他默认会生成一个basicTypes方法,里面的参数是支持跨进程通信的一些基本类型。可以看到,AIDL和Java接口几乎是一样的,可以让我们轻松上手。

// IMyServer.aidl
package com.feng.server;

// Declare any non-default types here with import statements

interface IMyServer {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}
跨进程传递数据

  由生成的默认方法可以看到,基本数据类型int,long,boolean,float,double,String在AIDL中都是支持的,也就是说基本类型都是可以进行跨进程传递的,另外还支持ListMap集合,它们最终会转化成ArrayListHashMap进行传输。

  但是实际中这些类型肯定是不够用的,因此需要我们自己实现的对象类型。而自定义的对象类型为了能够实现跨进程的能力,必然能够支持序列化。在Android中有两种方法,一个是实现Serialzable接口,一个是实现Parcelable接口。虽然Serializable和Parcelable都可以实现对象的序列化,但是Serializable是java的序列化接口,实现简单,但是开销大,序列化和反序列化有大量io操作;Parcelable是Android的接口,效率很高,Android中推荐使用Parcelable。

使用说明:
    在对象序列化于内存中,尽量使用Parcelable
    在对象序列化于储存设备中,使用Serializable
    在对象序列化于网络传输中,使用Serializable

  而AIDL主要是为了实现进程中的通信,是序列化于内存中的,因此使用Parcelable比较好。

  比如我们定义一个Person对象

data class Person(var age: Int, var name: String?) : Parcelable {

    constructor(parcel: Parcel) : this(parcel.readInt(), parcel.readString())

	// 注意这个方法
    fun readFromParcel(parcel: Parcel) {
        age = parcel.readInt()
        name = parcel.readString()
    }

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

    override fun describeContents() = 0

    companion object CREATOR : Parcelable.Creator<Person> {
        override fun createFromParcel(parcel: Parcel) = Person(parcel)
        override fun newArray(size: Int): Array<Person?> = arrayOfNulls(size)
    }

    override fun toString() = "[${age},${name}]"
}

  实现Parcelable接口,必须实现writeToParceldescribeContents两个方法。writeToParcel是将数据写入Parcelable中,可以将需要进行传递的字段写入Parcel中进行传递。describeContents可以返回0或者CONTENTS_FILE_DESCRIPTOR,对于一个普通Bean,可以直接返回0。
  还有注意,向Parcel写入字段的顺序是有要求的,先写入的数据在读取的时候也要先读出来,类似于队列一样,先写先读。可以看上面的Person类,在writeToParcel方法中先写入age后写入name,那么在对应的构造方法中也是先读age属性后读name
  实现Parcelable还需要在对象中定义一个静态对象CREATOR,这个对象是Parcelable.Creator类型的,它负责构造对象。在跨进程传递后,会被调用CREATOR来将Parcel转化成对象。

  经过上述的修改,此时的Person就已经具有序列化的能力了,也就是能够跨进程传递了,于是我们就可以在AIDL中使用它了。AIDL主要是声明一系列的接口方法,这些方法应该是服务端的方法,也就是这里的方法会在服务端中实现,由客户端进行调用。

// IMyServer.aidl
package com.feng.server;
parcelable Person;
interface IMyServer {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
                double aDouble, String aString);
    void sendPerson(inout Person person);
    Person getPerson();
}

  我们在aidl中定义了两个方法,sendPersongetPerson,一个是将Person对象发送到服务端,一个是从服务端获取Person对象,刚好用来测试Person的跨进程能力。

  虽然Person已经实现了Parcelable,但若要在aidl中使用,还是需要声明的,就像import一样。可以看到上面的aidl文件中,在import的位置加上了parcelable Person,这样才可以在aidl中使用Person。
当然,也可以新建另一个aidl文件,但是这个文件中不写接口,只用来声明这些Parcelable类,然后在aidl接口文件中import这个aidl声明文件,如下:

//Person.aidl
package com.feng.server;
parcelable Person;

  然后此时的IMyServer中应该这样写:

package com.feng.server;
import com.feng.server.Person;
interface IMyServer {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
                double aDouble, String aString);
    void sendPerson(inout Person person);
    Person getPerson();
}

  这里的Person.aidl文件名是可以不设置为Person的,当然最好跟声明的对象保持一致,而且该文件中也是可以声明多个Parcelable对象的。若是要使用Person.aidl的话,在新建AIDL的时候可能会提示Person已存在而无法创建,这时候可以先改为其他名字,然后再右键-refactor-rename修改回来。或者先定义Person.aidl,再定义Person.java

修饰符

  还有就是对象定向修饰符,就像在sendPerson(inout Person person)中的inout一样。这样的修饰符一共有三个,in/out/inout。基本类型默认为in,自定义的类型必须要指明。

  • in:数据由客户端流向服务端,即对象Person会由客户端传递给服务端,即正常的传递。服务端接收的对象内部数据与客户端是的一致的,相当于复制了一个对象传给了服务端。
  • out:数据由服务端流向客户端,若要使用这种tagPerson对象还要增加一个默认的空构造方法。在服务端接收到Person后,这个Person的内部数据都为空,也就是不保留内部数据,实际上会调用Person的空构造方法构造一个对象。虽然服务端拿到的这个对象并没有数据,但是它却是一个类似于客户端那个对象的分身的存在,也就是说服务端对这个Person对象的字段进行修改,会同步到客户端。比如客户端调用sendPerson(Person)传递的Person.age = 10,而在服务端中将收到的Person的age改为20,此时客户端的那个Person对象的age就会变成20。
  • inout:综合了in和out,即客户端既能传递带数据的对象,服务端对他的修改也能同步到客户端。使用这个tag需要给Person对象增加一个readFromParcel(Parcel)方法,这个方法用于从Parcel中读取数据。注意读取字段的顺序要与写入的顺序一致。

为了更好的应对这些定向tag,最好在定义Person的时候就加上空构造方法和readFromParcel方法

注意包名

  在新建aidl文件时,默认使用的包名为项目的包名,因此,定义的Person类必须放在该包目录下,也就是新建项目的那个默认包,否则会报错找不到这个类,也就是说Person的包名必须和在AIDL的包名一致。
因此,当客户端和服务端不在同一个项目中的时候,为了实现aidl通信,需要将aidl复制到另一个项目中,此时一定要注意aidl的包名,保证aidl的包名和Person的包名一致。

生成代码

  做完以上这些后可以直接build/rebuild project,然后就会生成对应的Java代码。该代码分为两个部分,接口实现部分和对象占位部分。如下图中的IMyServer.javaPerson.java,其中IMyServer是主要的实现代码,而Person.java则是一个空文件,我们声明的parcelable都会生成对应的占位文件,因为上面只声明了一个Person对象,因此也只生成了一个。

在这里插入图片描述

服务端

  这个IMyServer.java就是对AIDL的实现,生成这个后就可以编写具体的客户端和服务端代码了。首先编写服务端代码:

// MyService.java
class MyService : Service() {
    
    override fun onBind(intent: Intent): IBinder = object : IMyServer.Stub() {
        override fun basicTypes(
            anInt: Int,
            aLong: Long,
            aBoolean: Boolean,
            aFloat: Float,
            aDouble: Double,
            aString: String?
        ) {
            "basic type:[${anInt},${aLong},${aBoolean},${aFloat},${aDouble},${aString}]".logD()
        }

        override fun sendPerson(person: Person?) {
            person?.logD()
            person?.age = 19
        }

        override fun getPerson() = Person(23, "李华")

    }
}

  AIDL基本上都是基于Service实现的,因此这里服务端也是一个Service。这里定义了一个MyService,继承自Service,只实现了onBind方法。在onBind中,我们的返回对象就是IMyServer.Stub。这个Stub是AS根据我们的AIDL文件生成的IMyServer的一个静态内部抽象类,它继承自Binder并且包含有我们的定义的方法。这个Stub就是实现跨进程通信的基础,因此在onBind中直接返回一个Stub对象,并实现了我们在aidl中定义的那三个方法。实现都比较简单,都是打印一下传递过来的对象的信息,logD()是定义的一个拓展方法,内部调用的是Log.d()。其中在sendPerson中改了person的age属性,是为了测试inout的双向传递能力。
  写完Service后不要忘记在Manifest中注册:

<service
    android:name=".MyService"
    android:enabled="true"
    android:exported="true">   
      <intent-filter>
            <action android:name="com.feng.server.service" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
</service>

  注意要将exported设置为true,这样就可以在其他进程中调用到了。还要设置intent-filter,因为其他项目中是无法访问到MyService对象的,只能通过intent-filter进行绑定。这里因为是在两个项目中访问的,所以没有设置service的进程,它默认运行在当前项目进程中,若是在同一个项目中进行测试,可以设置process属性,以让它运行在其他进程中。

客户端

  服务端完成之后,就要编写客户端代码了:

class MainActivity : AppCompatActivity() {

	// IMyServer接口,用于调用服务端代码
    private var iMyServer: IMyServer? = null

	// 连接属性,在连接上服务端Service后,将onBind返回的IBinder转化成IMyServer
    private val conn = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            iMyServer = IMyServer.Stub.asInterface(service)
            "bind connected".logD()
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            "bind disconnected".logD()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

		// 按钮,点击进行绑定
        bind.setOnClickListener {
            val intent = Intent().apply {
                action = "com.feng.server.service"
                addCategory(Intent.CATEGORY_DEFAULT)
                setPackage("com.feng.servce")
            }
           bindService(intent, conn, Context.BIND_AUTO_CREATE).logD()
        }

		// 按钮,点击开始进行交互,分别测试三个方法
       send.setOnClickListener {
            iMyServer?.basicTypes(12, 13L, true, 13.1F, 13.2, "from client")
            
            val person = Person(18,"client Person")
            iMyServer?.sendPerson(person)
            person.logD()// 打印person对象,该对象在服务端中age被改为了19,这里会显示age为19
            
            iMyServer?.person.logD()
        }
    }

    override fun onDestroy() {
        unbindService(conn)
        super.onDestroy()
    }
}

  上面就是在Activity中绑定服务的过程,在绑定上Service的时候,在onConnected中会将IBinder转化成IMyServer。这里的IBinder就是Service中的onBind方法返回的对象,若是同一个进程该对象就是它本身,若是不同的进程中则会转化成BinderProxy对象。在生成的IMyServer.Stub的静态方法asInterface中,会根据这种情况进行转换。因此我们可以直接使用IMyServer来进行调用Server的方法而不用考虑跨进程的问题。
  注意,在不同的项目中是无法访问到MyService对象的,因此绑定服务只能通过隐式绑定,即在intent中设置actioncategory,另外通过隐式绑定的话需要加上查找包名,即设置packageservice所在程序的包名。

总结

  可见在AIDL的帮助下,我们跨进程的代码非常简单,就像是在同一个进程下一样,可以直接通过接口引用调用服务端的方法。而背后都是AIDL帮我们生成的代码去实现的,这些代码全部在生成的文件IMyServer.java中,可以根据这个文件简单了解一下背后如何通过Binder进行通信。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值