阿里一面:如何使用 AIDL 实现跨进程传输一个2M大小的文件 ?

700次阅读
没有评论

前言

分析一下,个人认为这个阿里的面试题可以细分为两个问题:

  1. 如何使用AIDL进行跨进程通信?
  2. 如何传输一个2M大小的文件?

问题1很简单,可以参考AIDL官方文档,这里不做过多介绍。本文主要集中火力解决问题2,讲解如何通过匿名共享内存实现跨进程双向大文件传输。

AIDL简介

AIDLAndroid中实现跨进程通信(Inter-Process Communication)的一种方式。AIDL的传输数据机制基于BinderBinder对传输数据大小有限制, 传输超过1M的文件就会报android.os.TransactionTooLargeException异常,一种解决办法就是使用匿名共享内存进行大文件传输。

阿里一面:如何使用 AIDL 实现跨进程传输一个2M大小的文件 ?

共享内存简介

共享内存是进程间通信的一种方式,通过映射一块公共内存到各自的进程空间来达到共享内存的目的。

阿里一面:如何使用 AIDL 实现跨进程传输一个2M大小的文件 ?

对于进程间需要传递大量数据的场景下,这种通信方式是十分高效的,但是共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。

Android中的匿名共享内存(Ashmem)是基于Linux共享内存的,借助Binder+文件描述符(FileDescriptor)实现了共享内存的传递。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于Linux的共享内存,Ashmem对内存的管理更加精细化,并且添加了互斥锁。Java层在使用时需要用到MemoryFile,它封装了native代码。Android平台上共享内存通常的做法如下:

  • 进程A通过MemoryFile创建共享内存,得到fd(FileDescriptor)
  • 进程A通过fd将数据写入共享内存
  • 进程A将fd封装成实现Parcelable接口的ParcelFileDescriptor对象,通过BinderParcelFileDescriptor对象发送给进程B
  • 进程B获从ParcelFileDescriptor对象中获取fd,从fd中读取数据

客户端和服务端双向通信+传输大文件实战

先放上实现效果图:

阿里一面:如何使用 AIDL 实现跨进程传输一个2M大小的文件 ?

我们先实现客户端向服务端传输大文件,然后再实现服务端向客户端传输大文件。

定义AIDL接口

//IMyAidlInterface.aidl
interface IMyAidlInterface {
    void client2server(in ParcelFileDescriptor pfd);
}

服务端

  • 实现IMyAidlInterface接口
//AidlService.kt
class AidlService : Service() {

    private val mStub: IMyAidlInterface.Stub = object : IMyAidlInterface.Stub() {

        @Throws(RemoteException::class)
        override fun sendData(pfd: ParcelFileDescriptor) {

        }
    }

    override fun onBind(intent: Intent): IBinder {
        return mStub
    }
}
  • 接收数据
//AidlService.kt
@Throws(RemoteException::class)
override fun sendData(pfd: ParcelFileDescriptor) {

    /**
     * 从ParcelFileDescriptor中获取FileDescriptor
     */
    val fileDescriptor = pfd.fileDescriptor

    /**
     * 根据FileDescriptor构建InputStream对象
     */
    val fis = FileInputStream(fileDescriptor)

    /**
     * 从InputStream中读取字节数组
     */
    val data = fis.readBytes()

    ......
}

客户端

  1. 绑定服务
  • 在项目的src目录中加入.aidl文件
  • 声明一个IMyAidlInterface接口实例(基于AIDL生成)
  • 创建ServiceConnection实例,实现android.content.ServiceConnection接口
  • 调用Context.bindService()绑定服务,传入ServiceConnection实例
  • onServiceConnected()实现中,调用IMyAidlInterface.Stub.asInterface(binder),将返回参数转换为IMyAidlInterface类型
//MainActivity.kt
class MainActivity : AppCompatActivity() {

    private var mStub: IMyAidlInterface? = null

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, binder: IBinder) {
            mStub = IMyAidlInterface.Stub.asInterface(binder)
        }

        override fun onServiceDisconnected(name: ComponentName) {
            mStub = null
        }
    }

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

    private fun bindService() {
        if (mStub != null) {
            return
        }
        val intent = Intent("io.github.kongpf8848.aidlserver.AidlService")
        intent.setClassName("io.github.kongpf8848.aidlserver","io.github.kongpf8848.aidlserver.AidlService")

        try {
            val bindSucc = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
            if (bindSucc) {
                Toast.makeText(this, "bind ok", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "bind fail", Toast.LENGTH_SHORT).show()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun onDestroy() {
        if(mStub!=null) {
            unbindService(serviceConnection)
        }
        super.onDestroy()
    }
}
  1. 发送数据
  • 将发送文件转换成字节数组ByteArray
  • 创建MemoryFile对象
  • MemoryFile对象中写入字节数组
  • 获取MemoryFile对应的FileDescriptor
  • 根据FileDescriptor创建ParcelFileDescriptor
  • 调用IPC方法,发送ParcelFileDescriptor对象
//MainActivity.kt
private fun sendLargeData() {
   if (mStub == null) {
      return
   }
   try {
    /**
     * 读取assets目录下文件
     */
    val inputStream = assets.open("large.jpg")

    /**
     * 将inputStream转换成字节数组
     */
    val byteArray=inputStream.readBytes()

    /**
     * 创建MemoryFile
     */
    val memoryFile=MemoryFile("image", byteArray.size)

    /**
     * 向MemoryFile中写入字节数组
     */
    memoryFile.writeBytes(byteArray, 0, 0, byteArray.size)

    /**
     * 获取MemoryFile对应的FileDescriptor
     */
    val fd=MemoryFileUtils.getFileDescriptor(memoryFile)

    /**
     * 根据FileDescriptor创建ParcelFileDescriptor
     */
    val pfd= ParcelFileDescriptor.dup(fd)

    /**
     * 发送数据
     */
    mStub?.client2server(pfd)

    } catch (e: IOException) {
    e.printStackTrace()
    } catch (e: RemoteException) {
    e.printStackTrace()
    }
}

至此,我们已经实现了客户端向服务端传输大文件,下面就继续实现服务端向客户端传输大文件功能。 服务端主动给客户端发送数据,客户端只需要进行监听即可。

  • 定义监听回调接口
//ICallbackInterface.aidl
package io.github.kongpf8848.aidlserver;

interface ICallbackInterface {
    void server2client(in ParcelFileDescriptor pfd);
}
  • IMyAidlInterface.aidl中添加注册回调和反注册回调方法,如下:
//IMyAidlInterface.aidl
import io.github.kongpf8848.aidlserver.ICallbackInterface;

interface IMyAidlInterface {

    ......

    void registerCallback(ICallbackInterface callback);

    void unregisterCallback(ICallbackInterface callback);
}
  • 服务端实现接口方法
//AidlService.kt
private val callbacks=RemoteCallbackList<ICallbackInterface>()

private val mStub: IMyAidlInterface.Stub = object : IMyAidlInterface.Stub() {

     ......

    override fun registerCallback(callback: ICallbackInterface) {
        callbacks.register(callback)
    }

    override fun unregisterCallback(callback: ICallbackInterface) {
        callbacks.unregister(callback)
    }
}
  • 客户端绑定服务后注册回调
//MainActivity.kt
private val callback=object: ICallbackInterface.Stub() {
    override fun server2client(pfd: ParcelFileDescriptor) {
        val fileDescriptor = pfd.fileDescriptor
        val fis = FileInputStream(fileDescriptor)
        val bytes = fis.readBytes()
        if (bytes != null && bytes.isNotEmpty()) {
           ......
        }
    }

}

private val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName, binder: IBinder) {
        mStub = IMyAidlInterface.Stub.asInterface(binder)
        mStub?.registerCallback(callback)
    }

    override fun onServiceDisconnected(name: ComponentName) {
        mStub = null
    }
}
  • 服务端发送文件,回调给客户端。此处仅贴出核心代码,如下:
//AidlService.kt
private fun server2client(pfd:ParcelFileDescriptor){
    val n=callbacks.beginBroadcast()
    for(i in 0 until n){
        val callback=callbacks.getBroadcastItem(i);
        if (callback!=null){
            try {
                callback.server2client(pfd)
            } catch (e:RemoteException) {
                e.printStackTrace()
            }
        }
    }
    callbacks.finishBroadcast()
}

至此,我们实现了客户端和服务端双向通信和传输大文件

PS:关于我

阿里一面:如何使用 AIDL 实现跨进程传输一个2M大小的文件 ?

本人是一个拥有6年开发经验的帅气Android攻城狮,目前是菊厂某组任职 Android 架构,记得看完点赞,养成习惯,关注这个喜欢写干货的程序员

另外耗时两年整理收集的Android一线大厂面试完整考点PDF出炉,【完整版】已更新在我的【Github】,如有面试、进阶需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!

地址:【https://github.com/733gh/xiongfan】

阿里一面:如何使用 AIDL 实现跨进程传输一个2M大小的文件 ?
正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 
评论(没有评论)