IPC通信Kotlin实现之AIDL

前言

Kotlin一个基于 JVM 的新的编程语言,由JetBrains开发。它和java完全兼容,可做android开发用,之前一直不温不火,在google宣布做为android官方开发语言之前一直是做为小三的存在而不受android开发人员的待见,而如今已转为正室,其名气也由之前的鲜为人知到现在的妇孺皆知,曾经的星星之火,如今已有燎原之势,所过之处,并没有哀鸿遍野,反倒是破土重生,当然这对于kotlin来说也算是实至名归吧,做为一名android开发,也要有一颗时刻在追逐新世界的心,也许最终不一定能得到one piece,但这伟大的航路中一路的风光不是对生命最好的诠释么。扯淡的话就不多说了,咱们言归正传,最近也在了解kotlin中,kotlin的好处本文不做探讨,本文主要是运用kotlin来实现AIDL机制。

正文

1,概述

PC是Inter-Process Communication的缩写,即进程间通信或夸进程通信。那什么是进程呢?进程和线程是什么关系呢?操作系统中讲,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元。一个进程中至少有一个线程(即主线程),也可以有多个,即进程包含线程。在android中,一个app如没有特殊需求开启一个进程,此时的一个app启动就是开启一个进程。那什么时候需要进程间通信?比如,在app中获取系统通讯录中的信息,在app中开启了桌面小部件功能时更部件就是跨进程的,再比如,有些app需要尽可能保证不被系统杀死,在app启动时候开启一个经量级的进程来监控主进程的运行情况,等等。当然IPC通信的使用场景不止这些,这里不做详细讨论。

AIDL是一个缩写,全称是Android Interface Definition Language,也就是Android接口定义语言。但其实上,AIDL只是一个方便系统为我们生成代码的工具,通过它系统回按照固定的格式生成Binder接口的实现java类,我们不用AIDL语言也完全可做到,AIDL只是简化了这个过程(关于这一点稍后的文章中会有分析)。

2,使用AIDL

  • 编写AIDL接口文件和相应的数据类
  • 创建一个Service端用来监听客户端的连接请求
  • 创建客户端并绑服务端

3,实现过程

1. AIDL接口和数据类的创建

AIDL如下代码

// Book.aidl
//第一类AIDL文件
//这个文件的作用是引入了一个序列化对象 Book 供其他的AIDL文件使用
//注意:Book.aidl与Book.java的包名应当是一样的
package com.ipcdemo.entity;

parcelable Book;
// IBookManager.aidl
//第二类AIDL文件
//作用是定义方法接口
package com.ipcdemo.entity;

import com.ipcdemo.entity.Book;
interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

注意:Book.aidl与Book.java的包名应当是一样的,当然也有其它方式的处理方法,具体请参考Android:学习AIDL,这一篇文章就够了(上)

数据类需要实现 Parcelable 接口,实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。Book对象的代码如下:

class Book(var bookId: Int,var bookName: String) : Parcelable {
    constructor(source: Parcel): this(source.readInt(),source.readString())
    /**
     * @return Int 当前对象的描述,几乎所有情况都是0
     * */
    override fun describeContents(): Int {
        return 0
    }

    /**
     * @param dest 将当前对象写入序列化结构中(即写入Parcel类中)
     * @param flags 1:表示当前对象需要作为返回值返回,不能立即释放,几乎所有情况都是0
     * */
    override fun writeToParcel(dest: Parcel?, flags: Int) {
        dest?.writeInt(bookId)
        dest?.writeString(bookName)
    }
    /**
     * @param dest Parcel类内部包装了可序列化数据,可以在Binder中存储与传输数据
     * */
    fun readFromParcel(dest: Parcel): Unit{
        //此处的读值顺序应当是和writeToParcel()方法中一致的
        bookId = dest.readInt()
        bookName = dest.readString()
    }
    /**
     * 用伴生对象和注解@JvmField来标识一个属性来表示一个java中的静态对象
     * */
    companion object {
        @JvmField final var CREATOR: Parcelable.Creator<Book> = object : Parcelable.Creator<Book> {
            override fun newArray(size: Int): Array<Book?> {
                return arrayOfNulls(size)
            }

            override fun createFromParcel(source: Parcel): Book {
                return Book(source)
            }

        }
    }
}

注意,只有在JVM 平台,如果使⽤ @JvmStatic 注解,你可以将伴⽣对象的成员⽣成为真正的 静态⽅法和字段。

2. 创建一个Service端

  • 创建一个Service并注册
  • 实现AIDL中的Binder的接口(即Stub)
  • 通过onBind返回这个Binder的实现给客户端

具体代码如下:

class KTAIDLService: Service() {
    val TAG = this.javaClass.simpleName
    var mBooks : List<Book> = ArrayList()
    /**
     * 创建⼀个继承⾃IBookManager.Stub类型的匿名类的对象,即实现AIDL中的Binder的接口
     * */
    var mIBookManager = object : IBookManager.Stub() {
        override fun getBookList(): MutableList<Book> {
            return rlock {
                if (mBooks==null) ArrayList<Book>() else mBooks as MutableList<Book>
            }
        }

        override fun addBook(book: Book?) {
            lock {
                if (book != null) {
                    //TODO 此处这个判断无效
                    if (!(mBooks as MutableList<Book>).contains(book)) {
                        (mBooks as MutableList<Book>).add(book)
                    }
                } else {
                    var booknew = Book(111,"百年孤独")
                    (mBooks as MutableList<Book>).add(booknew)
                }
            }
        }

    }
    override fun onCreate() {
        Log.i(TAG, "onCreate()")
        var book = Book(222,"毒木圣经")
        (mBooks as MutableList<Book>).add(book)
        super.onCreate()
    }
    override fun onBind(intent: Intent?): IBinder {
        Log.i(TAG, String.format("on bind,intent = %s", intent.toString()))
        return mIBookManager
    }

}

上面Service实现中,在onCreate()方法中初始化添加一本图书,然后创建一个Binder对象并在onBind中返回它,这个Binder对象继承了IBookManager.Stub并实现AIDL中的方法,getBookList,addBook(),这里对mBooks对象的操作加锁是因为,AIDL方法是在服务端的Binder线程池中执行的,当多个客户端连接时会有多个线程操作同一个方法的情况,所以对mBooks对象的读写需要加锁还有个问题是 (mBooks as MutableList<Book>).contains(book)这个判断是没有用的,这里先卖个关子,下文会有说明。

这里的rlock{} ,lock{} 方法用了kotlin中的扩展方法相关的知识,具体实现如下:

/**
 * 给Any添加锁同步的扩展函数
 * */
fun Any.lock(body: ()->Unit) {
    synchronized(this) {
        body()
    }
}
fun <T>Any.rlock(body: ()-> T): T{
    synchronized(this) {
        return body()
    }
}

上述的lock方法,接收一个无参无返回值Lambda表达式的函数体,对这个函数体进行加锁。rlock方法接收一个有参无返回值的函数体。

这个Service需要在AndroidMainfest.xml中注册,如下

<service
       android:name="com.ipcdemo.service.KTAIDLService"
       android:enabled="true"
       android:process=":remotejv">
       <intent-filter>
            <action android:name="com.ipcdemo.service.KTAIDLService" />
       </intent-filter>
 </service>

上述中 android:process = “true” 表示在一个新的进程里面启动一个服务。

3. 客户端的实现

  • 绑定一个远程的服务
  • 绑定成功后将服务返回的Binder对象换成AIDL接口
  • 通这这个AIDL接口去调用远程的方法
    代码如下:
class KTClientActivity : AppCompatActivity() {
    val TAG = this.javaClass.simpleName
    //由AIDL生成的类
    private lateinit var mIBookManager: IBookManager
    //标志当前与服务端连接状况的布尔值,false 为未连接,true
    private var mBound: Boolean = false
    //包含Book对象的List
    private lateinit var mBooks: List<Book>

    val book = Book(333,"天黑以后")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ktclient)
        initEvent()
    }

    private fun initEvent() {
        kotlin_addbook.setOnClickListener {
            if (!mBound) {
                attemptToBindService()
                toast("当前与服务端处于未连接状态,正在尝试重连,请稍后再试")
                return@setOnClickListener
            }
            
            mIBookManager?.addBook(book)
        }
        kotlin_getbooklist.setOnClickListener {
            if (!mBound) {
                attemptToBindService()
                toast("当前与服务端处于未连接状态,正在尝试重连,请稍后再试")
                return@setOnClickListener
            }
            mBooks = mIBookManager?.bookList ?: mBooks
            var sb = StringBuffer()
            for (book in mBooks) {
                sb.append("{"+book.bookId+"},"+"{"+book.bookName+"}\n")
            }
            content.text = sb.toString()
        }
    }
    fun attemptToBindService() {
        val intent = Intent()
        intent.action = "com.ipcdemo.service.KTAIDLService"
        intent.`package`="com.ipcdemo"
        //1,绑定一个远程的服务
        bindService(intent,mServiceConnection, Context.BIND_AUTO_CREATE)
    }
    var mServiceConnection: ServiceConnection = object: ServiceConnection{
        override fun onServiceDisconnected(name: ComponentName?) {
            Log.i(TAG, "service disconnected")
            mBound = false
        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.i(TAG, "service connected")
            //注意:这里的service对象就是KTAIDLService中onBind()方法中返回的AIDL的实现
            //2,将服务返回的Binder对象换成AIDL接口
            mIBookManager = IBookManager.Stub.asInterface(service)
            mBound = true
            mBooks = mIBookManager?.bookList
        }
    }

    override fun onStart() {
        super.onStart()
        if (!mBound) {
            attemptToBindService();
        }
    }

    override fun onStop() {
        super.onStop()
        if (mBound) {
            //解绑远程服务
            unbindService(mServiceConnection);
            mBound = false;
        }
    }
}

绑定成功后,会通过mIBookManager去调用AIDL中的方法,然后通过AIDL中的方法调用KTAIDLService中实现IBookManager.Stub接口的方法。

效果图如下

ezgif.com-video-to-gif.gif

对于图中的问题,KotlinAIDL实现中,每次调用addBook添加一个Book对象,每次在客户端添加相同的Book对象,在服务端用(mBooks as MutableList<Book>).contains(book),contains这个方法是List中是否有相同的对象,而我们客户端确确实实是添加同一个Book对象,其中的原因是,对象是不能跨进程直接传输的,对象跨进程传输的本质都是序列化反序列化的过程,这里的客户端添加一Book对象,AIDL中Binder会把客户端传过来的对象重新转化并生成一个新的对象。所以这里的contains判断是无用的,当然具体怎么排重,我想你应该有办法的,在项目中的java实现中有这个功能,具体请自行下载查看。

本文Demo下载地址

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

推荐阅读更多精彩内容