相关文章推荐
温柔的鸵鸟  ·  "nginx: [emerg] ...·  2 月前    · 
曾经爱过的吐司  ·  Performance ...·  5 月前    · 
叛逆的豌豆  ·  用ls grep ...·  11 月前    · 
严肃的苹果  ·  HTML5 新元素 | 菜鸟教程·  1 年前    · 

JavaFX图像的本地渲染方法

概述

在JavaFX的使用过程中,我有一个需要大量渲染和显示PDF页面的功能,我在这里使用了PDFBox,而PDFBox想要渲染一个图像,它会使用BufferedImage,而这个东西似乎很难释放掉,我只能看着随着翻过页面的增多,内存消耗也越来越大。

我很早就听过JavaFX是可以渲染native图像的,只是一直兴趣不大,或者说没有找到需要使用它的场景,直到现在。

本地图像渲染的基本流程

图像的色彩模式

图像通常被存储为各种图像文件,而在平常操作的图像API都隐藏了很多关于它们的细节,例如图像的色彩模式,简单来说,色彩模式说明了这个图片将会如何组织一个像素,例如“ARGB”的模式下,像素将会以“透明度”——“红色”——“绿色”——“蓝色”这样的顺序组合在一起,每一种是一个8位2进制,最大值是255,它们将会组成一个32位的值,这个值代表着某一个像素的内容。

之所以需要这样详细的说明色彩模式,是因为想要在JavaFX渲染一个本地的图像,它是一个重要的前置知识,目前在Windows上,JavaFX在本地渲染是仅仅支持一种色彩模式,也就是上述提到的“ARGB”,如果渲染的图像,它并不是ARGB格式,就需要转换为上述的组织形式才能被正确的显示。

Java如何使用本地内存

JNI通常是一种以C语言为基础的,专供JVM加载和使用的类库所遵守的一种标准,如果需要把本地的内存指针交给Java,让Java可以使用这种来自本地类库所申请的内存空间,就需要将指针传递给Java,直接使用指针读取内存的数据,对于Java来说非常麻烦,因此需要借助DirectByteBuffer的类型。

DirectByteBuffer在一般情况下是Java申请的堆外内存,内部存储了一个内存指针,并且会为这个DirectByteBuffer分配一个Cleaner,这个Cleaner会在合适的时候回收掉这些堆外的内存空间,当然,直接在JNI中创建的DirectByteBuffer是没有这种待遇的,如果你的DirectByteBuffer需要回收,你必须自己提供一个额外的接口来释放它,以防止内存泄露的发生。

// 在这里通过PDFium渲染图片
void* buffer = FPDFBitmap_GetBuffer(bitmap);
// 通过JNI接口创建DirectByteBuffer
(*env)->NewDirectByteBuffer(env,buffer, bufSize);

这里的Env在之前一段时间让我很疑惑,有些JNI示例中这里还需要传入一个env,而这一次我使用“.c”作为文件后缀的时候,发现标准的c语言下,接口的定义和使用“.cpp”,也就是C++的形式下,是不同的。

另外顺便一提,除了这种DirectByteBuffer以外,还存在一种MappedByteBuffer,它使用文件-内存映射的接口创建,类似于“mmap”。

JavaFX的本地渲染API

准备工作到此结束,接下来就是使用JavaFX这个神奇的本地渲染接口的时候了。

PdfiumDocumentPage pdfiumPage = pdfiumDocument.getPage(page);
PdfiumBitmapImage image = pdfiumPage.renderPage(config.getRenderQuality().intValue());
// 本地渲染好的图片,它就在这个ByteBuffer里面
ByteBuffer imageBuf = image.getBuffer();
// 申请内存,这里的Unsafe是我自己写的,不是Java的那个Unsafe,Copy一份方便我释放PDF的内存。
ByteBuffer renderedBuffer = Unsafe.memcpy(imageBuf);
// 这里是JavaFX提供的PixelBuffer类,它是本地图片渲染的关键。
// 由于格式是RGBA,也就是32位一个像素,所以需要使用的内存空间(byte[]数组的大小)
// 是图片的宽度乘以图片的高度乘以4,而且图片必须进行内存对齐,否则就会出现歪斜。
// 但是我不会做对齐所以每次都会生成一个新的WritableImage,反正内存是我自己Copy的,
// 不用的Image能够手动释放。
// 当然我使用的是本地渲染好的图和JNI申请的内存,也就不需要像上面这样计算了。
PixelBuffer<ByteBuffer> pixelBuffer = new PixelBuffer<>(
        image.getWidth(),