Android 使用AIDL传输超大型文件

本文探讨了如何通过AIDL在Android中处理大文件传输的问题,介绍了ParcelFileDescriptor的使用方法,以及如何通过它和MemoryFile实现跨进程高效文件传输,包括实践步骤和内存占用分析。
摘要由CSDN通过智能技术生成

最近遇到一个有意思的问题,能不能通过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的地址: 

GitHub - linxu-link/Aidl_transfer_file: 一个示例,演示使用aidl传输超大型文件(An example to demonstrate using AIDL to transfer very large files)

好了,以上就是本文的所有内容了,感谢你的阅读,希望对你有所帮助。


一道面试题:使用AIDL实现跨进程高效传输大文件

前言

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

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

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

AIDL简介

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

5543706-e4e77957cf15842a.png

共享内存简介

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

shmem.png

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

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接口

//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+匿名共享内存实现跨进程双向通信和大文件传输。

ParcelFileDescriptorAndroid 平台上的一个类,用于传输和管理文件描述符。它提供了一些方法来创建、读取、写入和关闭文件描述符。 要使用 ParcelFileDescriptor,首先需要创建一个文件描述符。可以通过以下方式之一来获取文件描述符: 1. 从文件路径创建:使用 ParcelFileDescriptor 的静态方法 `open(File file, int mode)`,其中 `file` 是要打开的文件对象,`mode` 是打开模式(例如,`ParcelFileDescriptor.MODE_READ_ONLY` 用于只读访问,`ParcelFileDescriptor.MODE_WRITE_ONLY` 用于只写访问,`ParcelFileDescriptor.MODE_READ_WRITE` 用于读写访问)。 2. 从文件描述符创建:如果已经有一个文件描述符,可以使用 ParcelFileDescriptor 的静态方法 `adoptFd(int fd)` 来创建 ParcelFileDescriptor 实例。 一旦获取了 ParcelFileDescriptor 实例,就可以使用它来执行各种操作,例如读取、写入或关闭文件描述符。可以通过以下方式之一来执行这些操作: 1. 读取文件描述符:使用 ParcelFileDescriptor 的 `read(byte[] buffer, int offset, int length)` 方法来读取文件描述符中的数据。将读取的数据存储在给定的字节数组 `buffer` 中。 2. 写入文件描述符:使用 ParcelFileDescriptor 的 `write(byte[] buffer, int offset, int length)` 方法来将数据写入文件描述符。 3. 关闭文件描述符:使用 ParcelFileDescriptor 的 `close()` 方法来关闭文件描述符。 需要注意的是,使用完 ParcelFileDescriptor 后,应该及时关闭文件描述符资源,以避免资源泄漏。 希望这些信息能帮助到你使用 ParcelFileDescriptor。如果有更多问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值