Android开发记事本--把DirectBuffer转成ByteArray

1,557 阅读3分钟

问题背景

最近在开发中遇到一个问题——CameraX的imageCapture在拍照成功以后onCaptureSuccess中得到的是一个ByteBuffer,我们通常处理图片需要用到的是ByteArray,第一反应是直接调用ByteBuffer.array()进行转换。这个时候报了UnsupportedOperationException的异常。

DirectBuffer

看一下源码为什么曝出这个问题

public final byte[] array() {
    if (hb == null)
        throw new UnsupportedOperationException();
    if (isReadOnly)
        throw new ReadOnlyBufferException();
    return hb;
}

可以看到当hb == null的时候会抛出这个异常,而hb其实就是HeapBuffer的缩写,也就是堆上的Buffer。

final byte[] hb;                  // Non-null only for heap buffers

而如果打印这个imageProxy.planes[0].buffer的类型,会发现这是一个NIO.DirectBuffer,那么需要了解一下ByteBuffer的原理才能明白这到底是什么东西。 ByteBuffer是一块缓冲区,它有三个属性capacity,limit还有postion,其中position代表缓冲区里的指针移动位置,而limit默认就是容量大小,limit一般用于设置读操作的终点。在读取ByteBuffer的时候的代码通常为

byteBuffer.limit(bytebuffer.position);
byteBuffer.position(0);
// 可以缩写为
byteBuffer.flip();

而HeapBuffer和DirectBuffer的区别就在于HeapBuffer里多了一块backingArraybyte[] 也叫支持数组。熟悉JVM的同学可以从Heap这个词就可以看出,HeapBuffer是在堆内分配内存的,而DirectBuffer是在堆外,堆内堆外的区别在于GC,ByteBuffer如果不断被GC整理改变了内存地址,会让IO操作更笨重,所以为了使用的效率就有了DirectBuffer。在Java中创建对象即在堆内开辟了一块内存操作,所以直接声明byte[] hb这样的方式当然是堆内创建,而DirectBuffer则是在堆外创建,这里会用到Native的方法,关于JVM的原理太多了就不深入探究了。 看一下创建DirectBuffer的方法

public static ByteBuffer allocateDirect(int capacity) {
    // Android-changed: Android's DirectByteBuffers carry a MemoryRef.
    // return new DirectByteBuffer(capacity);
    DirectByteBuffer.MemoryRef memoryRef = new DirectByteBuffer.MemoryRef(capacity);
    return new DirectByteBuffer(capacity, memoryRef);
}

关于MemoryRef创建初始化的代码:

MemoryRef(int capacity) {
    VMRuntime runtime = VMRuntime.getRuntime();
    buffer = (byte[]) runtime.newNonMovableArray(byte.class, capacity + 7);
    allocatedAddress = runtime.addressOf(buffer);
    // Offset is set to handle the alignment: http://b/16449607
    offset = (int) (((allocatedAddress + 7) & ~(long) 7) - allocatedAddress);
    isAccessible = true;
    isFreed = false;
    originalBufferObject = null;
}

这个newNonMovableArrayaddressOf在性能优化团队可能比较常见,可以简单理解为我们自己拿到了虚拟机来分配内存和设置指针。总而言之DirectBuffer由于堆外分配的原因是不存在backingArray的所以没有办法直接调用array方法来转换为ByteArray。

解决方案

先贴出最后的代码,这里参考的是Google的代码,解释一下就是首先 回到ByteBuffer的起点,然后新建一个等大的ByteArray,然后把ByteBuffer内容拷贝过去并返回这个ByteArray。

fun ByteBuffer.toByteArray(): ByteArray {
    rewind()
    val data = ByteArray(remaining())
    get(data)
    return data
}

比较迷惑的是这个get方法,是怎么做到内容的拷贝的。

public ByteBuffer get(byte[] dst) {
    return get(dst, 0, dst.length);
}
public ByteBuffer get(byte[] dst, int offset, int length) {
    checkBounds(offset, length, dst.length);
    if (length > remaining())
        throw new BufferUnderflowException();
    int end = offset + length;
    for (int i = offset; i < end; i++)
        dst[i] = get();
    return this;
}

其实也很简单,就是根据当前数组的大小,进行遍历并且拿buffer赋值。

private byte get(long a) {
    return Memory.peekByte(a);
}
public static native byte peekByte(long address);

最后调用到的是Memory的读字节方法,这个方法是一个native方法,就是操作内存去了,就不再往下看了。

总结

一个非常简单的小问题,HeapBuffer有backingArray可以直接拿来用,而DirectBuffer则需要自己设置个Array然后遍历。但是这里可以初窥到自己使用虚拟机来操作内存的方法,对于性能优化,尤其是bitmap的性能优化有一些参考价值。