最近遇到一个有意思的问题,能不能通过AIDL传输超过 1M 以上的文件?
我们先不细究,为什么要用AIDL传递大文件,单纯从技术的角度考虑能不能实现。众所周知,AIDL是一种基于Binder实现的跨进程调用方案,Binder 对传输数据大小有限制,传输超过 1M 的文件就会报 android.os.TransactionTooLargeException 异常。
如果文件相对比较小,还可以将文件分片,大不了多调用几次AIDL接口,但是当遇到大型文件或超大型文件时,这种方法就显得耗时又费力。好在,Android 系统提供了现成的解决方案,其中一种解决办法是,使用AIDL传递文件描述符ParcelFileDescriptor,来实现超大型文件的跨进程传输。
ParcelFileDescriptor
ParcelFileDescriptor 是一个实现了 Parcelable 接口的类,它封装了一个文件描述符 (FileDescriptor),可以通过 Binder 将它传递给其他进程,从而实现跨进程访问文件或网络套接字。ParcelFileDescriptor 也可以用来创建管道 (pipe),用于进程间的数据流传输。
ParcelFileDescriptor 的具体用法有以下几种:
-
通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。
-
通过 ParcelFileDescriptor.fromSocket() 方法将一个网络套接字 (Socket)转换为一个 ParcelFileDescriptor 对象,然后通过 Binder 将它传递给其他进程,实现跨进程访问网络套接字。
-
通过 ParcelFileDescriptor.open() 方法打开一个文件,并返回一个 ParcelFileDescriptor 对象,然后通过 Binder 将它传递给其他进程,实现跨进程访问文件。
-
通过 ParcelFileDescriptor.close() 方法关闭一个 ParcelFileDescriptor 对象,释放其占用的资源。
ParcelFileDescriptor.createPipe()和ParcelFileDescriptor.open() 都可以实现,跨进程文件传输,接下来我们会分别演示。
实践
- 第一步,定义AIDL接口
interface IOptions {
void transactFileDescriptor(in ParcelFileDescriptor pfd);
}
- 第二步,在「传输方」使用
ParcelFileDescriptor.open
实现文件发送
private void transferData() {
try {
// file.iso 是要传输的文件,位于app的缓存目录下,约3.5GB
ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(new File(getCacheDir(), "file.iso"), ParcelFileDescriptor.MODE_READ_ONLY);
// 调用AIDL接口,将文件描述符的读端 传递给 接收方
options.transactFileDescriptor(fileDescriptor);
fileDescriptor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
- 或,在「传输方」使用
ParcelFileDescriptor.createPipe
实现文件发送
ParcelFileDescriptor.createPipe 方法会返回一个数组,数组中的第一个元素是管道的读端,第二个元素是管道的写端。
使用时,我们先将「读端-文件描述符」使用AIDL发给「接收端」,然后将文件流写入「写端」的管道即可。
private void transferData() {
try {
/******** 下面的方法也可以实现文件传输,「接收端」不需要任何修改,原理是一样的 ********/
// createReliablePipe 创建一个管道,返回一个 ParcelFileDescriptor 数组,
// 数组中的第一个元素是管道的读端,
// 第二个元素是管道的写端
ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createReliablePipe();
ParcelFileDescriptor pfdRead = pfds[0];
// 调用AIDL接口,将管道的读端传递给 接收端
options.transactFileDescriptor(pfdRead);
ParcelFileDescriptor pfdWrite = pfds[1];
// 将文件写入到管道中
byte[] buffer = new byte[1024];
int len;
try (
// file.iso 是要传输的文件,位于app的缓存目录下
FileInputStream inputStream = new FileInputStream(new File(getCacheDir(), "file.iso"));
ParcelFileDescriptor.AutoCloseOutputStream autoCloseOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfdWrite);
) {
while ((len = inputStream.read(buffer)) != -1) {
autoCloseOutputStream.write(buffer, 0, len);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
注意,管道写入的文件流 总量限制在64KB,所以「接收方」要及时将文件从管道中读出,否则「传输方」的写入操作会一直阻塞。
- 第三步,在「接收方」读取文件流并保存到本地
public class ServerService extends Service {
private static final String TAG = "ServerService";
private final IOptions.Stub options = new IOptions.Stub() {
@Override
public void transactFileDescriptor(ParcelFileDescriptor pfd) {
Log.i(TAG, "transactFileDescriptor: " + Thread.currentThread().getName());
Log.i(TAG, "transactFileDescriptor: calling pid:" + Binder.getCallingPid() + " calling uid:" + Binder.getCallingUid());
File file = new File(getCacheDir(), "file.iso");
try (
ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd)
) {
file.delete();
file.createNewFile();
FileOutputStream stream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
// 将inputStream中的数据写入到file中
while ((len = inputStream.read(buffer)) != -1) {
stream.write(buffer, 0, len);
}
stream.close();
pfd.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
public ServerService() {
}
@Override
public void onCreate() {
super.onCreate();
startServiceForeground();
}
@Override
public IBinder onBind(Intent intent) {
return options;
}
private static final String CHANNEL_ID_STRING = "com.example.server.service";
private static final int CHANNEL_ID = 0x11;
private void startServiceForeground() {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel;
channel = new NotificationChannel(CHANNEL_ID_STRING, getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
Notification notification = new Notification.Builder(getApplicationContext(),
CHANNEL_ID_STRING).build();
startForeground(CHANNEL_ID, notification);
}
}
try(){}用法中,括号()的作用
属于Java7的新特性。
经常会⽤try-catch来捕获有可能抛出异常的代码。如果其中还涉及到资源的使⽤的话,最后在finally块中显⽰的释放掉有可能被占⽤的资源。
但是如果资源类已经实现了AutoCloseable这个接⼝的话,可以在try()括号中可以写操作资源的语句(IO操作),会在程序块结束时⾃动释放掉占⽤的资源,不⽤再在finally块中⼿动释放了。
不使用括号(try{}使用:)
InputStream is = null; OutputStream os = null; try { } catch (IOException e) { } finally { try { if (os != null) { os.close(); } if (is != null) { is.close(); } } catch (IOException e2) { } }
使用括号(try(){}使用)
try ( InputStream is = new FileInputStream("..."); OutputStream os = new FileOutputStream("..."); ) { } catch (IOException e) { }
- 运行程序
在程序运行之前,需要将一个大型文件放置到client app的缓存目录下,用于测试。目录地址:data/data/com.example.client/cache。
注意:如果使用模拟器测试,模拟器的硬盘要预留 3.5GB * 2 的闲置空间。
将程序运行起来,可以发现,3.5GB 的 file.iso 顺利传输到了Server端。
大文件是可以传输了,那么使用这种方式会很耗费内存吗?我们继续在文件传输时,查看一下内存占用的情况,如下所示:
- 传输方-Client,内存使用情况
- 接收方-Server,内存使用情况
从Android Studio Profiler给出的内存取样数据可以看出,无论是传输方还是接收方的内存占用都非常的克制、平缓。
总结
在编写本文之前,我在掘金上还看到了另一篇文章:一道面试题:使用AIDL实现跨进程传输一个2M大小的文件 - 掘金
该文章与本文类似,都是使用AIDL向接收端传输ParcelFileDescriptor
,不过该文中使用共享内存MemoryFile构造出ParcelFileDescriptor
,MemoryFile的创建需要使用反射,对于使用MemoryFile映射超大型文件是否会导致内存占用过大的问题,我个人没有尝试,欢迎有兴趣的朋友进行实践。
总得来说 ParcelFileDescriptor 和 MemoryFile 的区别有以下几点:
- ParcelFileDescriptor 是一个封装了文件描述符的类,可以通过 Binder 传递给其他进程,实现跨进程访问文件或网络套接字。MemoryFile 是一个封装了匿名共享内存的类,可以通过反射获取其文件描述符,然后通过 Binder 传递给其他进程,实现跨进程访问共享内存。
- ParcelFileDescriptor 可以用来打开任意的文件或网络套接字,而 MemoryFile 只能用来创建固定大小的共享内存。
- ParcelFileDescriptor 可以通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。MemoryFile 没有这样的方法,但可以通过 MemoryFile.getInputStream() 和 MemoryFile.getOutputStream() 方法获取输入输出流,实现进程内的数据流传输。
在其他领域的应用方面,ParcelFileDescriptor 和 MemoryFile也有着性能上的差异,主要取决于两个方面:
- 数据的大小和类型。
如果数据是大型的文件或网络套接字,那么使用 ParcelFileDescriptor 可能更合适,因为它可以直接传递文件描述符,而不需要复制数据。如果数据是小型的内存块,那么使用 MemoryFile 可能更合适,因为它可以直接映射到物理内存,而不需要打开文件或网络套接字。
- 数据的访问方式。
如果数据是需要频繁读写的,那么使用 MemoryFile 可能更合适,因为它可以提供输入输出流,实现进程内的数据流传输。如果数据是只需要一次性读取的,那么使用 ParcelFileDescriptor 可能更合适,因为它可以通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。
本文示例demo的地址:
好了,以上就是本文的所有内容了,感谢你的阅读,希望对你有所帮助。
一道面试题:使用AIDL实现跨进程高效传输大文件
前言
分析一下,个人认为这个面试题可以细分为两个问题:
- 如何使用AIDL进行跨进程通信?
- 如何传输一个大文件,如2M大小?
问题1很简单,可以参考AIDL官方文档,这里不做过多介绍。本文主要集中火力解决问题2,讲解如何通过匿名共享内存实现跨进程双向大文件传输。
AIDL简介
AIDL
是Android
中实现跨进程通信(Inter-Process Communication
)的一种方式。AIDL
的传输数据机制基于Binder
,Binder
对传输数据大小有限制, 传输超过1M的文件就会报android.os.TransactionTooLargeException
异常,一种解决办法就是使用匿名共享内存进行大文件传输。
共享内存简介
共享内存是进程间通信的一种方式,通过映射一块公共内存到各自的进程空间来达到共享内存的目的。
对于进程间需要传递大量数据的场景下,这种通信方式是十分高效的,但是共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。
Android
中的匿名共享内存(Ashmem)是基于Linux
共享内存的,借助Binder
+文件描述符(FileDescriptor
)实现了共享内存的传递。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于Linux
的共享内存,Ashmem对内存的管理更加精细化,并且添加了互斥锁。Java
层在使用时需要用到MemoryFile
,它封装了native
代码。Android
平台上共享内存通常的做法如下:
- 进程A通过
MemoryFile
创建共享内存,得到fd(FileDescriptor
) - 进程A通过fd将数据写入共享内存
- 进程A将fd封装成实现
Parcelable
接口的ParcelFileDescriptor
对象,通过Binder
将ParcelFileDescriptor
对象发送给进程B - 进程B获从
ParcelFileDescriptor
对象中获取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) {
/**
* 从ParcelFileDescriptor中获取FileDescriptor
*/
val fileDescriptor = pfd.fileDescriptor
/**
* 根据FileDescriptor构建InputStream对象
*/
val fis = FileInputStream(fileDescriptor)
/**
* 从InputStream中读取字节数组
*/
val data = fis.readBytes()
......
}
}
override fun onBind(intent: Intent): IBinder {
return mStub
}
}
客户端
- 绑定服务
- 在项目的
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()
}
}
- 发送数据
- 将发送文件转换成字节数组
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()
}
至此,我们实现了客户端和服务端双向通信和传输大文件。
GitHub
本文完整的代码已经上传GitHub,地址:GitHub - kongpf8848/aidldemo: 🔥使用AIDL+匿名共享内存实现跨进程双向通信和大文件传输。