在安卓开发中把Image的ByteBuffer数据通过虚拟内存工具进行复制,以节约实际内存的做法

Camera API回传的Image信息,如果不是立即使用,则在JVM中长时间保存的话会大量占用JVM内存,增加OOM风险。而我通过阅读ByteBuffer、DirectByteBuffer和Buffer等源码发现,如果使用Direct方式保存数据,则Buffer类中的long address变量保存的是native内存块的起始地址,并且可以通过isAccessible方法判断是否可读,于是我们可以通过这个地址把native数据通过MMAP工具或者文件读写函数放到外存先,实现Image数据不耗内存的持久化。
关于MMAP工具的编写可以先看看:
《安卓应用开发中通过JNI使用虚拟内存节约物理内存的一个例子》
《一种基于linux mmap特性的应用层虚拟内存工具的编写》
取数据过程的例子:

public Object getField(Object desObj, String fieldName) throws NoSuchFieldException { if (desObj == null || fieldName == null) { throw new IllegalArgumentException("parameter can not be null!"); } Class desClass = desObj.getClass(); return getFieldStepwise(desObj, desClass, fieldName); }private Object getFieldStepwise(Object desObj, Class rootClass, String fieldName) throws NoSuchFieldException { Class desClass = rootClass; while (desClass != null) { try { return getField(desObj, desClass, fieldName); } catch (NoSuchFieldException ignore) { } try { desClass = desClass.getSuperclass(); } catch (Exception e) { desClass = null; } } throw new NoSuchFieldException(fieldName); }public Object invoke(Object obj, String methodName, Object[] params) throws Exception { return invoke(obj.getClass(), obj, methodName, params); }private Object invoke(Class objClass, Object obj, String methodName, Object[] params) throws Exception { Method method; if (params == null || params.length == 0) { //Method method = objClass.getMethod(methodName); synchronized (mMethodMap) { method = mMethodMap.get(methodName); if (method == null) { method = objClass.getMethod(methodName); method.setAccessible(true); mMethodMap.put(methodName, method); } } return method.invoke(obj); } else { synchronized (mMethodMap) { Class[] clazzes = getParamsTypes(params); String fullName = objClass.getName() + '#' + methodName + getParamsTypesString(clazzes) + "#bestmatch"; method = mMethodMap.get(fullName); if (method == null) { method = objClass.getMethod(methodName, clazzes); method.setAccessible(true); mMethodMap.put(fullName, method); } } return method.invoke(obj, params); } }private long getByteArrayInVirtualMem(Image yuvImage, int width, int height, int stride, long vMemAdd) { ByteBuffer yBuf = yuvImage.getPlanes()[0].getBuffer(); ByteBuffer vuBuf = yuvImage.getPlanes()[2].getBuffer(); if (yBuf.isDirect() && vuBuf.isDirect()) { try { //long vMemAdd = VirtualMemoryUtil.createVirtualMemory(filePath, vuBuf.remaining() + yBuf.remaining() + 10 * 1024); long yBufferAddress = (long) ReflectHelper.getField(yBuf, "address"); long vuBufferAddress = (long) ReflectHelper.getField(vuBuf, "address"); int totalSize = yBuf.remaining() + vuBuf.remaining(); long yuvBufAddress = -1; yuvBufAddress = VirtualMemoryUtil.virtualMemoryMalloc(vMemAdd, totalSize); if (stride == width) { boolean canRead = (boolean) ReflectHelper.invoke(yBuf, "isAccessible", null); if (canRead) { VirtualMemoryUtil.copyNativeDataBlockToVMem(yuvBufAddress, yBufferAddress, yBuf.remaining()); } else { yuvImage.close(); return -1; } canRead = (boolean) ReflectHelper.invoke(vuBuf, "isAccessible", null); if (canRead) { VirtualMemoryUtil.copyNativeDataBlockToVMem(yuvBufAddress + yBuf.remaining(), vuBufferAddress, vuBuf.remaining()); } else { yuvImage.close(); return -1; } yuvImage.close(); return yuvBufAddress; } else { for (int i = 0; i < height && canRead; i++) { VirtualMemoryUtil.copyNativeDataBlockToVMem(yuvBufAddress + width * i, yBufferAddress + stride * i, width); canRead = (boolean) ReflectHelper.invoke(yBuf, "isAccessible", null); } long area = width * height; canRead = (boolean) ReflectHelper.invoke(vuBuf, "isAccessible", null); for (int i = 0; i < height / 2 && canRead; i++) { VirtualMemoryUtil.copyNativeDataBlockToVMem(yuvBufAddress + area + width * i, vuBufferAddress + stride * i, width); canRead = (boolean) ReflectHelper.invoke(vuBuf, "isAccessible", null); } yuvImage.close(); return yuvBufAddress; } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } yuvImage.close(); return -1; }

整个把image数据持久化的过程中除了image数据本身,没有增加一点JVM内存占用。
【在安卓开发中把Image的ByteBuffer数据通过虚拟内存工具进行复制,以节约实际内存的做法】通过这样的方法,可以把Image中的数据快速转移到外存的一个文件中,并得到外存地址。在需要使用的时候可以通过我的工具类中的方法
VirtualMemoryUtil.copyVMemDataToByteArray

即可通过传入地址把数据重新从外存中写入到byte数组中,更能直接在native中操纵该地址的数据实现不占物理内存的image数据操作,例如加水印等图像处理,但受限于外存速度远远小于内存,请不要在时间要求高的场合下使用该技巧。

另外必须提及一个风险,因为address中的内存内容可能被修改,所以可能读到脏数据或者读写时被free掉的话会引起SEGV_MAPPER,需要慎重使用。

    推荐阅读