注:本文内容转载自如下文章:使用AIDL实现跨进程高效传输大文件
AIDL
AIDL 是 Android 中实现跨进程通信(Inter-Process Communication)的一种方式。AIDL 的传输数据机制基于 Binder,Binder 对传输数据大小有限制,传输超过1M
的文件就会报android.os.TransactionTooLargeException
异常,一种解决办法就是使用匿名共享内存进行大文件传输。
共享内存简介
Linux 中的共享内存:
- 通过映射同一块公共物理内存到不同进程的用户虚拟地址空间来达到共享内存的目的。
对于进程间需要传递大量数据的场景下,这种通信方式是十分高效的,但是共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。
匿名共享内存Ashmem(Anonymous Shared Memory),提供了一种使 APP 跨进程传递大数据能突破1M-8KB
的限制的方案。
Android 中的匿名共享内存(Ashmem) 是基于 Linux 共享内存的,都是在 tmpfs
文件系统上新建文件,只是 Android 在 Linux 的基础上进行了改造,借助 Binder+文件描述符(FileDescriptor) 实现了共享内存的传递。相对于 Linux 的共享内存,Ashmem 对内存的管理更加精细化,并且添加了互斥锁。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。Android 中的 SurfaceFlinger 进程和 App 进程之间View数据的传递就是通过匿名共享内存。
MemoryFile
是 Android 为匿名共享内存而封装的一个 Java 层对象,它封装了 native 代码,同时MemoryFile
也是进程间大数据传递的一个手段,开发的时候可使用。
Android 平台上共享内存通常的做法如下:
- 进程 A 通过
MemoryFile
创建共享内存,得到fd
(FileDescriptor
) - 进程 A 通过
fd
将数据写入共享内存 - 进程 A 将
fd
封装成实现Parcelable
接口的ParcelFileDescriptor
对象,通过Binder
将ParcelFileDescriptor
对象发送给进程 B - 进程 B 从
ParcelFileDescriptor
对象中获取fd
,从fd
中读取数据
本质就是通过 Binder 机制传递 MemoryFile
的 fd
到不同进程, 不同进程通过这个 fd
进行操作共享内存。
客户端和服务端双向通信+传输大文件实战
效果图:
运行的时候先启动服务端,然后再启动客户端,手机上可以使用分屏功能将客户端和服务端显示在同一个屏幕上,客户端绑定服务后,双方就可以相互发送图片了。
我们先实现客户端向服务端传输大文件,然后再实现服务端向客户端传输大文件。
定义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 client2server(pfd: ParcelFileDescriptor) {val fileDescriptor = pfd.fileDescriptor // 从ParcelFileDescriptor中获取FileDescriptorval fis = FileInputStream(fileDescriptor) // 根据FileDescriptor构建InputStream对象val data = fis.readBytes() // 从InputStream中读取字节数组......}}override fun onBind(intent: Intent): IBinder {return mStub}
}
客户端
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? = nullprivate 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()}
}
2. 发送数据
- 将发送文件转换成字节数组
ByteArray
- 创建
MemoryFile
对象 - 向
MemoryFile
对象中写入字节数组 - 获取
MemoryFile
对应的FileDescriptor
- 根据
FileDescriptor
创建ParcelFileDescriptor
- 调用 IPC 方法,发送
ParcelFileDescriptor
对象
// MainActivity.kt
private fun sendLargeData() {if (mStub == null) returntry {val inputStream = assets.open("large.jpg") // 读取assets目录下文件val byteArray = inputStream.readBytes() // 将inputStream转换成字节数组val memoryFile = MemoryFile("image", byteArray.size) // 创建MemoryFilememoryFile.writeBytes(byteArray, 0, 0, byteArray.size) // 向MemoryFile中写入字节数组val fd = MemoryFileUtils.getFileDescriptor(memoryFile) // 获取MemoryFile对应的FileDescriptorval pfd = ParcelFileDescriptor.dup(fd) // 根据FileDescriptor创建ParcelFileDescriptormStub?.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.fileDescriptorval 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()
}
至此,我们实现了客户端和服务端双向通信和传输大文件。
GitHub地址: https://github.com/kongpf8848/aidldemo