从文件中快速读取小端整数

11

我需要在我的Android应用程序中读取由4字节整数(小端)组成的二进制文件到一个2D数组中。我的当前解决方案如下:

DataInputStream inp = null;
try {
    inp = new DataInputStream(new BufferedInputStream(new FileInputStream(procData), 32768));
}
catch (FileNotFoundException e) {
    Log.e(TAG, "File not found");
}

int[][] test_data = new int[SIZE_X][SIZE_Y];
byte[] buffer = new byte[4];
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
for (int i=0; i < SIZE_Y; i++) {
    for (int j=0; j < SIZE_X; j++) {
        inp.read(buffer);
        byteBuffer = ByteBuffer.wrap(buffer);
        test_data[j][SIZE_Y - i - 1] = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).getInt();
    }
}

对于一个2k*2k的数组来说,这个速度相当慢,需要大约25秒。 我可以在DDMS中看到垃圾收集器正在加班工作,所以这可能是速度变慢的原因之一。

肯定有更有效的方法使用ByteBuffer将文件读入数组中,但我目前还没有看到它。 有任何想法如何加快速度吗?


你真的需要同时读取所有数据吗?并且你经常访问很多条目吗?如果不是,你可以避免将整个数组解析为整数。只需读取或包装整个文件,并通过计算其偏移量从x y坐标提供所需的条目。 - Luzifer42
1
@Luzifer,我需要在开始时至少获取所有数据。 - Mad Scientist
4个回答

12

为什么不先读入一个4字节的缓冲区,然后手动重新排列字节顺序呢?它看起来会像这样:

for (int i=0; i < SIZE_Y; i++) {
    for (int j=0; j < SIZE_X; j++) {
        inp.read(buffer);
        int nextInt = (buffer[0] & 0xFF) | (buffer[1] & 0xFF) << 8 | (buffer[2] & 0xFF) << 16 | (buffer[3] & 0xFF) << 24;
        test_data[j][SIZE_Y - i - 1] = nextInt;
    }
}
当然,假设read读取了全部四个字节,但你应该检查当它没有读取完时的情况。这样一来,在读取过程中就不会创建任何对象(从而不会对垃圾收集器造成压力),也不会调用任何东西,只需使用按位运算即可。

谢谢,这个版本比我的原始版本快了大约5倍,现在只需要5秒钟。我不习惯直接操作位。 - Mad Scientist
这是我发现的唯一可行的将原始字节转换为unsigned int的方法。谢谢! - dinigo
为什么要使用FF进行按位与运算?字节不是由8位组成的吗?如果是这样,那么这个操作不会有任何作用...我错过了什么吗? - Sushi271
4
由于字节是有符号的,如果只将一个字节转换为整数,可能会得到负数。比如,如果一个字节包含 0b11111111,它将变成 -1 而不是 255。 - Malcolm

5
如果您使用支持内存映射文件的平台,请考虑使用java.nio中的MappedByteBuffer及其相关方法。
FileChannel channel = new RandomAccessFile(procData, "r").getChannel();
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_ONLY, 0, 4 * SIZE_X * SIZE_Y);
map.order(ByteOrder.LITTLE_ENDIAN);
IntBuffer buffer = map.asIntBuffer();

int[][] test_data = new int[SIZE_X][SIZE_Y];
for (int i=0; i < SIZE_Y; i++) {
    for (int j=0; j < SIZE_X; j++) {
        test_data[j][SIZE_Y - i - 1] = buffer.get();
    }
}

如果您需要跨平台支持或平台缺乏内存映射缓冲区,则可能仍希望避免使用IntBuffer自行执行转换。考虑放弃BufferedInputStream,自行分配较大的ByteBuffer,并获得数据上的小字节序IntBuffer视图。然后在循环中将缓冲区位置重置为0,使用DataInputStream.readFully一次性将大区域读入ByteBuffer中,并从IntBuffer中获取int值。


3
首先,你的 'inp.read(buffer)' 是不安全的,因为 read 合同不能保证它将读取所有 4 个字节。
除此之外,为了快速转换,请使用 DataInputStream.readInt 中的算法。
我已经为你调整了一个长度为 4 个字节的字节数组的情况。
int little2big(byte[ ] b) {
    return (b[3]&0xff)<<24)+((b[2]&0xff)<<16)+((b[1]&0xff)<<8)+(b[0]&0xff);
}

1
我认为没有必要重新发明轮子并再次执行字节排序以进行字节序。这很容易出错,而且类似于ByteBuffer的类存在是有原因的。
你的代码可以优化,因为它浪费了对象。当byte[]ByteBuffer包装时,缓冲区添加了一个视图,但原始数组仍然相同。无论直接修改/读取原始数组还是使用ByteBuffer实例都没有关系。
因此,只需要初始化一个ByteBuffer实例,并设置一次ByteOrder即可。
要重新开始,请使用rewind()将计数器重新设置为缓冲区的开头。
我已经按描述修改了你的代码。请注意,如果输入中剩余的字节不足,则不检查错误。我建议使用inp.readFully,因为如果没有找到足够的字节来填充缓冲区,它会抛出EOFException
int[][] test_data = new int[SIZE_X][SIZE_Y];
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN);
for (int i=0; i < SIZE_Y; i++) {
    for (int j=0; j < SIZE_X; j++) {
        inp.read(byteBuffer.array());
        byteBuffer.rewind();
        test_data[j][SIZE_Y - i - 1] = byteBuffer.getInt();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接