最近在写车载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.client/cache。

留意:假如运用模拟器测验,模拟器的硬盘要预留 3.5GB * 2 的闲置空间。

将程序运转起来,能够发现,3.5GB 的 file.iso 顺畅传输到了Server端。

Android 使用AIDL传输超大型文件

大文件是能够传输了,那么运用这种办法会很消耗内存吗?咱们持续在文件传输时,查看一下内存占用的状况,如下所示:

  • 传输方-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的地址:github.com/linxu-link/…

好了,以上便是本文的一切内容了,感谢你的阅读,期望对你有所协助。