Android Java下载文件报unexpected end of stream异常排查
问题说明
业务需求为Android端下载图片并保存至手机存储,同时通过textview_progress_status_id控件展示下载进度状态。实际运行时下载任务启动后耗时过长、无法完成下载,抛出异常java.net.ProtocolException: unexpected end of stream。
原业务实现代码如下:
private String downloadFile(String MY_URL) { int count; try { URL url = new URL(MY_URL); URLConnection connection = url.openConnection(); connection.connect(); long startTime = System.currentTimeMillis(); int lengthOfFile = connection.getContentLength(); InputStream input = new BufferedInputStream(url.openStream(), 8192); fileName = MY_URL.substring(MY_URL.lastIndexOf('/') + 1, MY_URL.length()); File outputFile = new File(directory, fileName); OutputStream output = new FileOutputStream(outputFile); byte data[] = new byte[1024]; long total = 0; while ((count = input.read(data)) != -1) { total += count; int status = (int) ((total * 100) / lengthOfFile); output.write(data, 0, count); handler.post(new Runnable() { @Override public void run() { // Connectivity Manager //speedTV.setText(downSpeed+" mbps"); // speedTV.setText(finalRate); progress_circular_id.setProgress(status); textview_progress_status_id.setText(String.valueOf(status) + "%"); if (status == 100) { dialog.dismiss(); } } }); } output.flush(); output.close(); input.close(); return "Downloaded at: " + directory.getPath() + fileName; } catch (IOException e) { Log.e("Abhan", "Error: " + e.getLocalizedMessage()); runOnUiThread(new Runnable() { public void run() { hideProgressDialog(); } }); } hideProgressDialog(); return "Downloading Error"; }
unexpected end of stream 异常根本原因 - 直接触发点:代码对同一个URL重复发起了两次连接:第一次调用
connection.connect()建立连接,仅用来获取响应头的Content-Length字段;之后又调用url.openStream()新建了第二条独立连接获取下载输入流。多数服务端会对同一会话下的重复连接主动截断前序连接的响应流,导致第一次连接拿到的长度标识和第二次连接实际返回的流数据不匹配,流未读取完成就被服务端断开,触发协议异常。 - 辅助诱因:代码未设置连接超时、读取超时参数,网络波动、服务端无响应时线程会无限阻塞,表现为下载长时间无进展。
现有代码存在的编写错误
- 重复建立连接的逻辑错误:既浪费网络资源,也是触发上述协议异常的核心原因。
- 进度计算逻辑存在崩溃风险:如果服务端采用分块传输不返回
Content-Length(字段值为-1),(total * 100) / lengthOfFile会直接触发除零崩溃;大文件下载时long类型转int计算进度还会出现数值溢出,进度显示错乱。 - UI更新逻辑性能差:每读取1KB数据就向主线程post一次UI更新任务,高频次的消息会阻塞主线程,导致进度条、文本刷新卡顿,甚至触发ANR。
- 资源释放逻辑不严谨:如果写入文件过程中抛出异常,已打开的输入输出流不会被主动关闭,会造成文件句柄泄漏;catch块外直接调用
hideProgressDialog(),会出现下载未完成时进度弹窗提前消失的问题。 - 文件名解析逻辑鲁棒性差:如果URL携带查询参数(如
https://xxx.com/1.jpg?time=123),直接截取最后一个/后的内容会把参数拼入文件名,导致文件后缀错误,下载后无法正常打开。 - 缺少系统版本适配:Android 6.0+未动态申请存储权限、Android 10+未做分区存储适配时,文件写入会直接失败。
修正方案
核心修复点
- 全局复用同一个
HttpURLConnection实例获取响应头和输入流,避免重复建连;可额外添加Connection: close请求头,强制使用短连接,规避部分服务端keep-alive机制兼容问题。 - 设置合理的连接、读取超时时间,避免线程无限阻塞。
- 增加响应码校验,服务端返回非200状态码时直接终止下载流程。
- 优化进度计算逻辑:仅当
Content-Length为有效值时计算百分比进度,降低UI刷新频率(进度变化超过1%才触发更新),避免主线程阻塞。 - 把资源释放逻辑统一放到
finally块,确保任何异常场景下流、连接都能被正确关闭,避免资源泄漏。 - 优化文件名解析逻辑,优先从
Content-Disposition响应头取文件名,从URL截取时自动剔除查询参数。
修正后参考代码
private String downloadFile(String MY_URL) { int count; HttpURLConnection connection = null; InputStream input = null; FileOutputStream output = null; try { URL url = new URL(MY_URL); connection = (HttpURLConnection) url.openConnection(); // 设置超时时间 connection.setConnectTimeout(15000); connection.setReadTimeout(15000); // 强制短连接,规避流截断问题 connection.setRequestProperty("Connection", "close"); connection.connect(); // 校验服务端响应码 if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { return "Download Error: Server response code " + connection.getResponseCode(); } long startTime = System.currentTimeMillis(); int lengthOfFile = connection.getContentLength(); input = new BufferedInputStream(connection.getInputStream(), 8192); // 正确解析文件名 String fileName = ""; String disposition = connection.getHeaderField("Content-Disposition"); if (disposition != null) { int nameIndex = disposition.indexOf("filename="); if (nameIndex > 0) { fileName = disposition.substring(nameIndex + 10, disposition.length() - 1); } } if (fileName.isEmpty()) { int lastSlashPos = MY_URL.lastIndexOf('/'); int queryParamPos = MY_URL.indexOf('?', lastSlashPos); fileName = queryParamPos == -1 ? MY_URL.substring(lastSlashPos + 1) : MY_URL.substring(lastSlashPos + 1, queryParamPos); } File outputFile = new File(directory, fileName); output = new FileOutputStream(outputFile); byte[] data = new byte[4096]; long total = 0; int lastUpdateStatus = 0; while ((count = input.read(data)) != -1) { total += count; output.write(data, 0, count); // 仅当内容长度有效时计算进度,且进度变化超过1%才更新UI if (lengthOfFile > 0) { int currentStatus = (int) ((total * 100) / lengthOfFile); if (currentStatus != lastUpdateStatus) { lastUpdateStatus = currentStatus; int finalStatus = currentStatus; handler.post(() -> { progress_circular_id.setProgress(finalStatus); textview_progress_status_id.setText(finalStatus + "%"); if (finalStatus == 100) { dialog.dismiss(); } }); } } } output.flush(); return "Downloaded at: " + outputFile.getAbsolutePath(); } catch (IOException e) { Log.e("Abhan", "Download Error: " + e.getLocalizedMessage()); runOnUiThread(this::hideProgressDialog); return "Downloading Error"; } finally { // 统一释放资源 try { if (output != null) output.close(); if (input != null) input.close(); } catch (IOException ignored) {} if (connection != null) connection.disconnect(); runOnUiThread(this::hideProgressDialog); } }
额外适配注意事项
- Android 6.0~9版本需要提前动态申请
WRITE_EXTERNAL_STORAGE权限,再执行下载写入逻辑。 - Android 10及以上版本建议使用
MediaStoreAPI写入公共存储目录,或使用应用私有目录存储文件,避免分区存储权限限制导致写入失败。 - 如果服务端采用分块传输不返回
Content-Length,可切换为不确定模式的进度条,提示用户下载中即可,无需强行计算百分比进度。
内容的提问来源于stack exchange,提问作者MARSH
相关产品推荐
相关产品推荐

