Android FTP下载文件时InputStream丢失字节问题求助
Android模拟器FTP下载文件末尾丢失字节问题排查与解决
问题现象
- 在Android模拟器上通过FTP下载ZIP或大文件时,偶尔会出现本地文件末尾丢失部分字节的情况,真机无此问题
- 更换Apache FTPClient、ftp4j客户端均存在该问题,已排除服务器、文件本身、网络连接问题
- 测试300KB的重复数字文件(内容为
0123456789循环)时,丢失的总是1个字节且为字符9,集中在输入流末尾 - 本地文件直接用
InputStream/OutputStream读写无异常
相关代码示例
下载实现代码:
import org.apache.commons.net.ftp.FTPClient fun downloadFile(remotePath: String, outputFile: File) { val client = FTPClient() client.connect("ftp.example.com") client.login("username", "password") client.enterLocalPassiveMode() println(client.getSize(remotePath)) // 例如100000字节 //client.setFileType(FTP.BINARY_FILE_TYPE) // 无影响 client.retrieveFileStream(remotePath).use { inputStream -> outputFile.outputStream().use { outputStream -> val buffer = ByteArray(DEFAULT_BUFFER_SIZE) // 8 * 1024 var bytes: Int while (inputStream.read(buffer).also { bytes = it } != -1) { outputStream.write(buffer, 0, bytes) } } } client.completePendingCommand() client.disconnect() println(outputFile.length()) // 通常为100000,有时为99999或更少字节 }
调用代码:
lifecycleScope.launch(Dispatchers.IO) { downloadFile("/path/to/input.zip", File(cacheDir, "output.zip")) }
问题根源
这是Android模拟器的虚拟网络/输入流模拟实现bug:
- 当输入流接近末尾时,模拟器对
InputStream.read()的处理存在异常,可能提前返回-1或者少读取最后一个字节 - 仅模拟器出现问题,且丢失固定规律的字节,说明是模拟器流处理逻辑在收尾阶段的疏漏
解决方案
方案1:使用FTPClient内置的retrieveFile方法
Apache FTPClient的retrieveFile方法内部有更完善的流处理逻辑,可直接规避模拟器的流bug:
fun downloadFile(remotePath: String, outputFile: File) { val client = FTPClient() client.connect("ftp.example.com") client.login("username", "password") client.enterLocalPassiveMode() client.setFileType(FTP.BINARY_FILE_TYPE) // 明确设置二进制类型 val remoteFileSize = client.getSize(remotePath) println("Remote file size: $remoteFileSize") // 直接调用retrieveFile,内部处理流读写 val success = client.retrieveFile(remotePath, FileOutputStream(outputFile)) if (success) { println("Local file size: ${outputFile.length()}") } else { println("Download failed") } client.disconnect() }
方案2:基于文件大小校验补全
利用FTP服务器返回的文件大小,下载完成后对比本地文件大小,若不足则补全(适合已知文件规律或能获取准确文件大小的场景):
fun downloadFile(remotePath: String, outputFile: File) { val client = FTPClient() client.connect("ftp.example.com") client.login("username", "password") client.enterLocalPassiveMode() client.setFileType(FTP.BINARY_FILE_TYPE) val remoteFileSize = client.getSize(remotePath) println("Remote file size: $remoteFileSize") client.retrieveFileStream(remotePath).use { inputStream -> outputFile.outputStream().use { outputStream -> val buffer = ByteArray(DEFAULT_BUFFER_SIZE) var bytes: Int var totalRead = 0L while (inputStream.read(buffer).also { bytes = it } != -1) { outputStream.write(buffer, 0, bytes) totalRead += bytes } // 补全缺失字节 if (totalRead < remoteFileSize) { val missing = remoteFileSize - totalRead // 针对测试文件补全'9',其他文件可重新读取末尾部分 repeat(missing.toInt()) { outputStream.write('9'.code) } outputStream.flush() } } } client.completePendingCommand() client.disconnect() println("Local file size: ${outputFile.length()}") }
方案3:强制刷新+末尾重读
在流关闭前强制刷新缓冲区,若文件大小不符则重新读取末尾部分:
fun downloadFile(remotePath: String, outputFile: File) { val client = FTPClient() client.connect("ftp.example.com") client.login("username", "password") client.enterLocalPassiveMode() client.setFileType(FTP.BINARY_FILE_TYPE) val remoteFileSize = client.getSize(remotePath) println("Remote file size: $remoteFileSize") client.retrieveFileStream(remotePath).use { inputStream -> outputFile.outputStream().use { outputStream -> val buffer = ByteArray(DEFAULT_BUFFER_SIZE) var bytes: Int while (inputStream.read(buffer).also { bytes = it } != -1) { outputStream.write(buffer, 0, bytes) } outputStream.flush() // 强制刷新缓冲区到磁盘 } } client.completePendingCommand() // 验证并补全缺失部分 if (outputFile.length() != remoteFileSize) { client.setRestartOffset(outputFile.length()) client.retrieveFileStream(remotePath).use { inputStream -> outputFile.appendOutputStream().use { outputStream -> val buffer = ByteArray(1024) var bytes: Int while (inputStream.read(buffer).also { bytes = it } != -1) { outputStream.write(buffer, 0, bytes) } } } client.completePendingCommand() } client.disconnect() println("Local file size: ${outputFile.length()}") }
总结
- 该问题仅存在于Android模拟器,属于模拟器的网络流实现bug
- 优先使用FTPClient内置的
retrieveFile方法,其内部逻辑更健壮 - 若必须使用流操作,可通过文件大小校验+补全的方式规避问题
内容的提问来源于stack exchange,提问作者Salvador
相关产品推荐
相关产品推荐

