Android 使用AIDL传输超大型文件
Posted 林栩link
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 使用AIDL传输超大型文件相关的知识,希望对你有一定的参考价值。
最近在写车载android的第5篇视频教程「AIDL的实践与封装」时,遇到一个有意思的问题,能不能通过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,所以「接收方」要及时将文件从管道中读出,否则「传输方」的写入操作会一直阻塞。
- 第三步,在「接收方」读取文件流并保存到本地
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();
;
- 运行程序
在程序运行之前,需要将一个大型文件放置到client app的缓存目录下,用于测试。目录地址:data/data/com.example.server/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的地址:https://github.com/linxu-link/Aidl_transfer_file
好了,以上就是本文的所有内容了,感谢你的阅读,希望对你有所帮助。
Android中AIDL的使用
AIDL(Android接口定义语言)
---------------------------------
AIDL用于定义跨进程通信时需要使用到的接口,即当多个应用程序都需要使用到相关的接口时,应该使用AIDL来定义。
【使用AIDL定义接口的步骤】
1. 使用一般的创建interface的方式创建Java接口文件
2. 将创建的interface的权限删掉,即例如public interface IMusicPlayer修改为interface IMusicPlayer
3. 打开Windows的资源管理器,找到该接口文件,修改扩展名为aidl
AIDL的数据类型
---------------------------------
AIDL默认只识别:
1. 基本数据类型,例如int、long、float、boolean等……
2. String,CharSeqence
3. List
自定义数据类型:
1. 自定义类,例如Music,并且实现Parcelable接口
2. 添加自定义类的aidl文件,例如Music.aidl,在该文件中,只需要package语句,和parcelable 类名,例如parcelable Music,即可
3. 在aidl接口文件中,显式的添加import语句,导入自定义的数据类型,例如import cn.tedu.ipc.Music,无论当前aidl接口文件与自定义类的aidl文件是否在同一个包中,都必须显式的导包
4. 当跨进程访问时,访问者(客户端)需要将服务
端的aild接口文件、实体类的java文件、实体类的aidl文件全部复制到客户端,并且,保证包名与服务器端是一致的
Parcelable接口
---------------------------------
实现步骤:
1. 自定义类,例如Music,实现Parcelable接口
2. 重写describeContents()方法,直接返回0即可
3. 重写writeToParcel()方法,调用第1个Parcel类型的参数的write???系列方法,将当前类(Music)类的成员写出
4. 自定义readFromParcel(Parcel src)方法,根据第3步骤中调用wite???系列的顺序,依次调用Parcel参数的read???系列方法,并为当前类(Music)的各个成员赋值
5. 自定义当前类(Music)的带Parcel参数的构造方法,并在构造方法中调用readFromParcel()
6. 声明public static final Parcelable.Creator<Music> CREATOR常量,并使用匿名内部类的语法直接赋值,在匿名内部类中,public Music[] newArray(int size)方法直接返回new Music[size]即可,public Music createFromParcel(Parcel src)中,直接返回通过构造方法创建对象即可。
以上是关于Android 使用AIDL传输超大型文件的主要内容,如果未能解决你的问题,请参考以下文章