进阶技术 - 如何实现跨进程传递大图?(腾讯面试题)

跨进程传输大图片有哪些常见的方案:

1. 文件传输

将图片保存在指定路径区域,然后将储存位置信息跨进程发送给其它进程,其它进程通过查找路径读取图片文件。
这种方案存在的缺点,在于进程首先需要进行写文件操作,然后再去读取文件,性能低下。

2.IPC传输

IPC传递图片的方式:

  • Binder,Binder是Android提供的一种类型共享内存的方式,使用方便,性能较高,但传输的数据有大小限制。
  • Socket、管道,Socket和管道传递数据会涉及到至少两次数据拷贝,当数据量大的时候性能低下,除此之外,Socket和管道对单次发送的数据量也是有限制的。
  • 共享内存,共享内存的传输方式性能较高,对数据没有大的限制。

在进行跨进程传输数据的时候,我们主要需要考虑的两个问题:

  1. 性能问题,尽量减少数据拷贝的次数;
  2. 内存限制,单次发送的数据量过大会造成内存泄漏。

当我们使用Intent跨进程传输数据的时候,如果数据量太打,会抛出TransactionTooLargeException的异常,这个异常表示两个Client的进行交互时,事物占用的内存过大,该异常发生的一些情况和解决方法:

  • 发送/返回的数据量过大,跨进程通信的过程中,发送数据和接收数据都是通过Buffer承载的,要是占用的内存过大,那么预留给对方的内存就有可能不足,当内存不足的时候,发送/返回数据申请不到足够的内存,就会抛这个异常。
  • Binder申请不到足够的内存缓存空间,应用在启动Binder机制的时候,会创建1M的缓存空间作为Binder通信的内存空间,所有的Binder公用这1M的内存,要是某个Binder服务占用的内存空间较大,导致其他的Binder无法申请到足够的内存空间,就会抛出这个异常。
  • 单个Binder调用数据传输量过大的问题,建议的解决方案是将数据打散分批发,或者按需发。
    在这里插入图片描述在这里插入图片描述

上图是通过Intent来跨进程传递一个Bitmap,当Bitmap占用的内存过大时,会抛出TRansactionTooLargeException的异常。
下面这种通过putBinder的方式传输同样的Bitmap则不会抛出异常:
在这里插入图片描述
如上面的代码,我们通过Intent传递一个Binder对象,接收端在收到Binder对象之后,通过调用BInder对象的getBitmap方法,同样可以获得传输的Bitmap,却不会造成异常。

当我们调用putParcelable传递Bitmap(Bitmap是实现了Parcelable接口的,可以进行序列化的)的时候,系统自动将allowFds设为false,禁止使用文件描述符,bitmap的传输不能利用共享内存的方式,只能将Bitmap拷贝Binder的缓存区,导致缓存区超限,需要申请的内存大于了Binder初始化的1M内存空间的限制,这样就会抛出TransactionTooLargeException的异常;

当我们使用putBinder的方式传递Bitmap的时候,系统是会将allowFds设置为true,运行带fd描述字符的,当传递数据的时候,首先会判断当前数据是否小于16K,小于16KB的时候会直接使用Binder的缓存空间,而当大于16KB的时候,则开辟一个ashmem,映射出一块内存,该数据会保存到ashmem中,在Intent中之写入一个fd的文件描述符,这样即使传输的数据再大,Intent中传输的也只是该资源的文件描述符。而ashmem就是利用了共享内存,在发送端和接收端之间映射同一块内存,无需多次拷贝,提高了数据传输的性能。

3. Android 匿名共享内存简介

Android 的匿名共享内存(Ashmem) 基于 Linux 的共享内存,都是在临时文件系统(tmpfs)上创建虚拟文件,再映射到不同的进程。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于 Linux 的共享内存,Ashmem 对内存的管理更加精细化,并且添加了互斥锁。Java 层在使用时需要用到 MemoryFile,它封装了 native 代码。 Java 层使用匿名共享内存的4个点:

  1. 通过 MemoryFile 开辟内存空间,获得 FileDescriptor;
  2. 将 FileDescriptor 传递给其他进程;
  3. 往共享内存写入数据;
  4. 从共享内存读取数据。

下面用一个例子介绍匿名共享内存的使用,假设需要开辟一段共享内存,写入一些数据,再在另外一个进程读取这段数据。

1. 创建 MemoryFile 和 数据写入

/**
 * 需要写入到共享内存中的数据
 */
private val bytes = "落霞与孤鹜齐飞,秋水共长天一色。".toByteArray()


/**
 * 创建 MemoryFile 并返回 ParcelFileDescriptor
 */
private fun createMemoryFile(): ParcelFileDescriptor? {
    // 创建 MemoryFile 对象,1024 是最大占用内存的大小。
    val file = MemoryFile("TestAshmemFile", 1024)

    // 获取文件描述符,因为方法被标注为 @hide,只能反射获取
    val descriptor = invokeMethod("getFileDescriptor", file) as? FileDescriptor

    // 如果获取失败,返回
    if (descriptor == null) {
        Log.i("ZHP", "获取匿名共享内存的 FileDescriptor 失败")
        return null
    }

    // 往共享内存中写入数据
    file.writeBytes(bytes, 0, 0, bytes.size)

    // 因为要跨进程传递,需要序列化 FileDescriptor
    return ParcelFileDescriptor.dup(descriptor)
}


/**
 * 通过反射执行 obj.name() 方法
 */
private fun invokeMethod(name: String, obj: Any): Any? {
    val method = obj.javaClass.getDeclaredMethod(name)
    return method.invoke(obj)
}

MemoryFile 有两个构造方法,上面是一种,另一种是根据已有的 FileDescriptor 创建。 MemoryFile 创建时指定的大小并不是实际占用的物理内存大小,实际占用内存大小由写入的数据决定,但不能超过指定的大小。

2. 将文件描述符传递到其他进程
这里选择用 Binder 传递 ParcelFileDescriptor。 我们定义一个 Code,用于 C/S 两端通信确定事件:

/**
 * 两个进程在传递 FileDescriptor 时用到的 Code。
 */
const val MY_TRANSACT_CODE = 920511

再在需要的地方 bindService:

// 创建服务进程
val intent = Intent(this, MyService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)

bind 成功之后将 文件描述符 和 数据大小 序列化,然后通过 Binder 传递到 Service 进程:

private val serviceConnection = object: ServiceConnection {

    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        if (binder == null) {
            return
        }

        // 创建 MemoryFile,并拿到 ParcelFileDescriptor
        val descriptor = createMemoryFile() ?: return

        // 传递 FileDescriptor 和 共享内存中数据的大小
        val sendData = Parcel.obtain()
        sendData.writeParcelable(descriptor, 0)
        sendData.writeInt(bytes.size)

        // 保存对方进程的返回值
        val reply = Parcel.obtain()

        // 开始跨进程传递
        binder.transact(MY_TRANSACT_CODE, sendData, reply, 0)

        // 读取 Binder 执行的结果
        val msg = reply.readString()
        Log.i("ZHP", "Binder 执行结果是:「$msg」")
    }

    override fun onServiceDisconnected(name: ComponentName?) {}

}

两个进程的文件描述符指向同一个文件结构体,文件结构体指向了一片内存共享区域(ASMA),使得两个文件描述符对应到同一片ASMA中。

3. 在其他进程接收 FileDescriptor 并读取数据
先定义一个 MyService 用于开启子进程:

class MyService : Service() {

    private val binder by lazy { MyBinder() }

    override fun onBind(intent: Intent) = binder
}

再实现具体的 MyBinder 类,主要包含3个步骤:

  1. 从序列化数据中读取 FileDescriptor 和 共享内存中保存的数据大小;
  2. 根据 FileDescriptor 创建 FileInputStream;
  3. 读取共享内存中的数据。
/**
 * 这里不必使用 AIDL,继承 Binder 类 重写 onTransact 即可。
 */
class MyBinder: Binder() {

    /**
     * 文件描述符 和 数据大小 通过 data 传入。
     */
    override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
        val parent = super.onTransact(code, data, reply, flags)
        if (code != MY_TRANSACT_CODE && code != 931114) {
            return parent
        }

        // 读取 ParcelFileDescriptor 并转为 FileDescriptor
        val pfd = data.readParcelable<ParcelFileDescriptor>(javaClass.classLoader)
        if (pfd == null) {
            return parent
        }
        val descriptor = pfd.fileDescriptor

        // 读取共享内存中数据的大小
        val size = data.readInt()

        // 根据 FileDescriptor 创建 InputStream
        val input = FileInputStream(descriptor)

        // 从 共享内存 中读取字节,并转为文字
        val bytes = input.readBytes()
        val message = String(bytes, 0, size, Charsets.UTF_8)

        Log.i("ZHP", "读取到另外一个进程写入的字符串:「$message」")

        // 回复调用进程
        reply?.writeString("Server 端收到 FileDescriptor, 并且从共享内存中读到了:「$message」")

        return true
    }

}

这里拿到 FileDescriptor 后不仅可以读也能写入数据,还可以再创建一个 MemoryFile 对象。

这就是Android匿名共享内存的使用啦~

4. Android进程共享内存的传递-fd文件描述符的传递
原生Linux共享内存是通过传递已知的key来处理的,但是Android中不存在这种机制,Android是怎么处理的呢?
那就是通过Binder传递文件描述符来处理,Android的Binder对于fd的传递也做了适配,原理其实就是在内核层为要传递的目标进程转换fd,因为在linux中fd只是对本进程是有效、且唯一,进程A打开一个文件得到一个fd,不能直接为进程B使用,因为B中那个fd可能压根无效、或者对应其他文件,不过,虽然同一个文件可以有多个文件描述符,但是文件只有一个,在内核层也只会对应一个inode节点与file对象,这也是内核层可以传递fd的基础,Binder驱动通过当前进程的fd找到对应的文件,然后为目标进程新建fd,并传递给目标进程,核心就是把进程A中的fd转化成进程B中的fd,看一下Android中binder的实现:

void binder_transaction(){
   ...
        case BINDER_TYPE_FD: {
        int target_fd;
        struct file *file;
        <!--关键点1 可以根据fd在当前进程获取到file ,多个进程打开同一文件,在内核中对应的file是一样-->
        file = fget(fp->handle);
        <!--关键点2,为目标进程获取空闲fd-->
        target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
        <!--关键点3将目标进程的空闲fd与file绑定-->
        task_fd_install(target_proc, target_fd, file);
        fp->handle = target_fd;
    } break;    
    ...
 }

<!--从当前进程打开的files中找到file在内核中的实例-->
struct file *fget(unsigned int fd)
{
    struct file *file;
    struct files_struct *files = current->files;
    rcu_read_lock();
    file = fcheck_files(files, fd);
    rcu_read_unlock();
    return file;
}


static void task_fd_install(
    struct binder_proc *proc, unsigned int fd, struct file *file)
{
    struct files_struct *files = proc->files;
    struct fdtable *fdt;
    if (files == NULL)
        return;
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    rcu_assign_pointer(fdt->fd[fd], file);
    spin_unlock(&files->file_lock);
}

在这里插入图片描述
5.匿名共享内存的优点也是BUG
匿名共享内存不会占用Dalvik Heap与Native Heap,不会导致OOM,这是优点,同时也是缺点,因为如果肆意使用,会导致系统资源不足,性能下降,
在这里插入图片描述
另外共享存占用空间的计算,只会计算到第一个创建它的进程中,其他进程不将ashmem计算在内。

6. 总结
Android匿名共享内存是基于Linux共享内存的,都是在tmpfs文件系统上新建文件,并将其映射到不同的进程空间,从而达到共享内存的目的,只是,Android在Linux的基础上进行了改造,并借助Binder+fd文件描述符实现了共享内存的传递。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: comparator是Java中的一个接口,用于比较两个对象的大小。它可以用于对集合中的元素进行排序,也可以用于自定义排序规则。实现comparator接口需要重写compare方法,该方法返回一个整数值,表示两个对象的大小关系。如果返回负数,则表示第一个对象小于第二个对象;如果返回正数,则表示第一个对象大于第二个对象;如果返回,则表示两个对象相等。comparator接口可以与Java中的排序算法一起使用,例如Collections.sort()方法。 ### 回答2: jmu-java-04面向对象进阶--02-接口-comparator讲述了Java中的接口以及比较器的使用。接口是一种约束,它规定了某个类必须要实现哪些方法,但不需要具体的实现方式。比较器则是一种接口,它规定了两个对象之间的排序方式。 在Java中,接口的定义方式为interface,其中的方法默认为public abstract形式。定义接口时,需要注意接口只能继承接口,并且可以有常量,但不能有成员变量。另外,接口中所有的方法都没有方法体,必须由实现它的类去具体实现。举例来说,如果我们定义一个接口Animal,可以定义一个方法move(),而实现这个接口的类必须实现move()方法,并且可以自由决定具体的实现方式,如Dog类可以实现为跑步,Bird类可以实现为飞行。 在讨论了接口的使用之后,jmu-java-04面向对象进阶--02-接口-comparator着重介绍了比较器的使用。比较器类似于一个工具箱,可以定义多种比较方式供其他类使用。比较器的核心类是Comparator,其定义的方法为compare(),用于比较两个对象并返回结果(0、1或-1)。比较器可以用于对对象进行排序或查找指定的对象。 在使用比较器时,需要实现Comparator接口,并覆盖compare()方法。比如,我们可以定义一个Person类,并在其中实现Comparator接口,然后在compare()方法中指定按照年龄从小到大排序。当我们使用Collections.sort()对Person列表进行排序时,就会按照我们定义的比较方式进行排序。 总的来说,jmu-java-04面向对象进阶--02-接口-comparator讲述了Java中的接口和比较器的使用,这是Java中优秀的编程方式之一,也是开发者必备的基本知识。掌握了接口和比较器的使用,我们就可以更好地实现面向对象编程,并对Java中的集合框架有更深刻的理解。 ### 回答3: Comparator是Java中一个非常重要的接口,它主要用于定义对象之间的比较规则。在Java中,比较规则是由比较器来实现的。比较器可以用于排序、查找和其他需要比较的场景。 Comparator接口有一个方法compare(Object o1, Object o2),用于比较两个对象的大小。如果o1大于o2,则该方法返回一个正整数;如果o1小于o2,则该方法返回一个负整数;如果o1等于o2,则该方法返回0。 我们可以使用Comparator接口来实现自定义的比较规则。比如,我们可以定义一个Student类,包含姓名和年龄两个属性,然后实现一个比较器,按照年龄从小到大的顺序对Student对象进行排序。 可以通过使用Collections.sort()方法对Student对象进行排序,提供一个实现Comparator接口的比较器作为参数进行排序。 实现一个比较器还可以实现多种排序方式。例如,按照姓名从小到大排序,实现如下: ``` public class NameComparator implements Comparator<Student> { public int compare(Student s1, Student s2) { return s1.getName().compareTo(s2.getName()); } } ``` 在使用时,我们可以将NameComparator对象作为参数传递给sort()方法,进行姓名排序。 Comparator接口的使用不仅仅局限于对象的比较排序,还可以用于其他需要比较的场景,比如查找、筛选等。例如,我们可以按照年龄筛选出年龄大于20岁的Student对象,并将它们存储在一个新的List中,实现如下: ``` List<Student> ageGreaterThan20 = students.stream() .filter(s -> s.getAge() > 20) .sorted(new AgeComparator()) .collect(Collectors.toList()); ``` 以上的代码使用了Java 8的新特性,使用流将年龄大于20岁的Student对象筛选出来,并按照年龄进行排序,最后存储在一个新的List中。 总之,Comparator是一个非常重要的接口,在Java中有着广泛的应用。掌握Comparator的使用可以帮助我们快速地实现对象比较、排序、筛选等操作,提高我们的编程效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值