下载图片的时候因为使用输出流传输数据的,某些时候中断后,本地图片会因为丢失数据出现下半部分图片灰色撕裂,获取其宽高也不能判断图片是否完整。
# 是什么决定文件的格式?
首先肯定不是文件后缀啦,文件本质就是二进制数据,文件头部二进制数据决定了文件格式。
# JPG格式分析
通过 Notepad++ 二进制模式打开JPG文件,文件开始的2个字节是 FF D8 即 SOI(start of image), 结束2个字节是 FF D9 即 EOD(end of image)。
那么通过读取 EOD 是否匹配,即可判断出文件下载是否完整。
# 字节读取方案
读取 SOI 基本没有什么可说的,只需要读取前面2个字节。
/** * 部分思路 */ fun readSOI(input: InputStream, length: Int = 2): String { val bytes = ByteArray(length) IO.read(input, bytes, 0, length) return Hex.encodeHexString(bytes) } // println FF D8 //
## 读取 EOD
1 ) 阻塞读取至结束
/** * 逐字节读取到末尾,如果遇到大文件比较耗时 * 适当增加缓冲区大小可加快读取速度 * 注意需要手动关闭输入流 * @param input 输入流 * @param bufferSize 读取缓冲区大小 */ fun readEOD(input: InputStream, bufferSize: Int): String { var transferred = -1 var buffer = ByteArray(bufferSize) var prevTransferred = -1 var prevBuffer = mutableListOf<Byte>() var read: Int while (input.read(buffer, 0, bufferSize).also { read = it } >= 0) { transferred = read if (transferred == 1) { // 最后2个字节被拆分 把 prevBuffer.addAll(buffer.copyOfRange(0, transferred).toList()) buffer = prevBuffer.toByteArray() transferred = prevTransferred + 1 } else { prevBuffer = buffer.copyOf().toMutableList() prevTransferred = transferred } } val bf = buffer.copyOfRange(0, transferred).let { it.copyOfRange(it.size - 2, it.size) } return Hex.encodeHexString(bf) }
Tip:这种方式分段读取 Bytes ,避免了全部读取到内存中,然后对 读取速度 和 内存使用 做了平衡,仅保留一个分段副本。
2 ) 已知输入流长度 skip 读取
fun readEODFast(input: InputStream, skip: Long): String { val lastBytes = input.apply { skip(skip) }.readAllBytes().let { it.copyOfRange(it.size - 2, it.size) } return Hex.encodeHexString(lastBytes) }
Tip:使用 skip 抛弃指定长度的Byte ,比上述方法更优,只是前提需要数据流的长度。
3 ) 使用 RandomAccessFile 随机访问
/** * 读取尾部最优方案 * 前提是需要输入流长度 */ fun readBigFileEOD(file: File): String { if (!file.exists() || file.isDirectory || !file.canRead()) { throw Exception("file is error") } val raf = RandomAccessFile(file, "r") raf.seek(file.length() - 2) val buffer = ByteArray(2) raf.read(buffer) raf.close() return Hex.encodeHexString(buffer) }
Tip:这种方案在读取本地文件的情况下是最优解 ,RandomAccessFile 的指针尤其适合超大文件,seek 直接指定位置进行读取,无需从头读取。
# 完整的代码
package test import java.io.File import java.io.InputStream import java.io.RandomAccessFile class ValidIMG { fun readSOI(input: InputStream, length: Int = 2): String { val bytes = ByteArray(length) IO.read(input, bytes, 0, length) return Hex.encodeHexString(bytes) } /** * 逐字节读取到末尾,如果遇到大文件比较耗时 * 适当增加缓冲区大小可加快读取速度 * 注意需要手动关闭输入流 * @param input 输入流 * @param bufferSize 读取缓冲区大小 */ fun readEOD(input: InputStream, bufferSize: Int): String { var transferred = -1 var buffer = ByteArray(bufferSize) var prevTransferred = -1 var prevBuffer = mutableListOf<Byte>() var read: Int while (input.read(buffer, 0, bufferSize).also { read = it } >= 0) { transferred = read if (transferred == 1) { // 最后2个字节被拆分 prevBuffer.addAll(buffer.copyOfRange(0, transferred).toList()) buffer = prevBuffer.toByteArray() transferred = prevTransferred + 1 } else { prevBuffer = buffer.copyOf().toMutableList() prevTransferred = transferred } } val bf = buffer.copyOfRange(0, transferred).let { it.copyOfRange(it.size - 2, it.size) } return Hex.encodeHexString(bf) } fun readEOD(input: InputStream): String { return readEOD(input, 100) } fun readEODFast(input: InputStream, skip: Long): String { val lastBytes = input.apply { skip(skip) }.readAllBytes().let { it.copyOfRange(it.size - 2, it.size) } return Hex.encodeHexString(lastBytes) } /** * 读取尾部最优方案 * 前提是需要输入流长度 */ fun readBigFileEOD(file: File): String { if (!file.exists() || file.isDirectory || !file.canRead()) { throw Exception("file is error") } val raf = RandomAccessFile(file, "r") raf.seek(file.length() - 2) val buffer = ByteArray(2) raf.read(buffer) raf.close() return Hex.encodeHexString(buffer) } class Hex { companion object { private val DIGITS_LOWER = charArrayOf( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' ) /** * Used to build output as hex. */ private val DIGITS_UPPER = charArrayOf( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' ) fun encodeHexString(data: ByteArray): String { return String(encodeHex(data)) } private fun encodeHex(data: ByteArray): CharArray { return org.apache.commons.codec.binary.Hex.encodeHex(data, true) } fun encodeHex(data: ByteArray, toLowerCase: Boolean): CharArray { return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER) } private fun encodeHex(data: ByteArray, toDigits: CharArray): CharArray { val l = data.size val out = CharArray(l shl 1) encodeHex(data, 0, data.size, toDigits, out, 0) return out } fun encodeHex( data: ByteArray, dataOffset: Int, dataLen: Int, toLowerCase: Boolean ): CharArray { val out = CharArray(dataLen shl 1) encodeHex(data, dataOffset, dataLen, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER, out, 0) return out } fun encodeHex( data: ByteArray, dataOffset: Int, dataLen: Int, toLowerCase: Boolean, out: CharArray, outOffset: Int ) { encodeHex( data, dataOffset, dataLen, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER, out, outOffset ) } private fun encodeHex( data: ByteArray, dataOffset: Int, dataLen: Int, toDigits: CharArray, out: CharArray, outOffset: Int ) { // two characters form the hex value. var i = dataOffset var j = outOffset while (i < dataOffset + dataLen) { out[j++] = toDigits[0xF0 and data[i].toInt() ushr 4] out[j++] = toDigits[0x0F and data[i].toInt()] i++ } } } } class IO { companion object { fun read(input: InputStream, buffer: ByteArray, offset: Int, length: Int): Int { require(length >= 0) { "Length must not be negative: $length" } var remaining = length while (remaining > 0) { val location = length - remaining val count = input.read(buffer, offset + location, remaining) if (-1 == count) { // EOF break } remaining -= count } return length - remaining } } } }
文章评论