杰瑞科技汇

Java Android下载文件如何实现?

核心概念

无论使用哪种方法,下载文件的核心步骤都大同小异:

Java Android下载文件如何实现?-图1
(图片来源网络,侵删)
  1. 获取网络连接:通过 URL 对象建立与远程文件的连接。
  2. 获取输入流:从连接中获取 InputStream,用于读取网络数据。
  3. 获取输出流:创建一个 FileOutputStream,用于将数据写入本地存储。
  4. 读写数据:使用一个缓冲区(如 byte[]),循环从输入流读取数据,并写入输出流。
  5. 关闭资源:在 finally 块或使用 try-with-resources 语句中,确保所有流和连接都被正确关闭,防止内存泄漏。
  6. 处理UI更新:在主线程(UI线程)中更新进度条、提示文本等UI元素。

使用 HttpURLConnection (原生、基础)

这是 Java 标准库中自带的 HTTP 客户端,无需添加任何依赖,适合简单的下载任务。

优点:

  • 无需额外依赖,是 Android SDK 的一部分。
  • 代码直观,易于理解。

缺点:

  • API 较为底层,需要处理很多细节(如连接、流、错误码)。
  • 不支持现代的 HTTP/2 协议。
  • 实现起来代码量稍多。

完整代码示例

添加网络权限

AndroidManifest.xml 中添加:

<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果要下载到外部存储,还需要这个权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 对于 Android 10 (API 29) 及以上,推荐使用 scoped storage,而不是直接申请 WRITE_EXTERNAL_STORAGE -->

创建下载工具类

Java Android下载文件如何实现?-图2
(图片来源网络,侵删)
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class FileDownloader {
    private static final String TAG = "FileDownloader";
    // 下载文件
    public static void downloadFile(String fileUrl, String fileName, DownloadCallback callback) {
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        HttpURLConnection connection = null;
        try {
            // 1. 创建 URL 和连接
            URL url = new URL(fileUrl);
            connection = (HttpURLConnection) url.openConnection();
            connection.connect();
            // 检查是否是成功的响应码 (200 OK)
            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                throw new IOException("Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage());
            }
            // 2. 获取输入流
            inputStream = connection.getInputStream();
            // 3. 确定保存路径 (这里保存到外部公共下载目录)
            // 注意: Android 10+ 推荐使用 Context.getExternalFilesDir() 或 MediaStore API
            File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
            if (!directory.exists()) {
                directory.mkdirs();
            }
            File outputFile = new File(directory, fileName);
            // 4. 获取输出流
            outputStream = new FileOutputStream(outputFile);
            // 5. 读写数据
            byte[] buffer = new byte[4096]; // 4KB 缓冲区
            int bytesRead;
            long totalBytesRead = 0;
            long fileSize = connection.getContentLength(); // 获取文件总大小
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;
                // 6. 回调进度
                if (callback != null) {
                    int progress = (int) ((totalBytesRead * 100) / fileSize);
                    callback.onProgressUpdate(progress);
                }
            }
            // 下载完成
            if (callback != null) {
                callback.onSuccess(outputFile.getAbsolutePath());
            }
        } catch (IOException e) {
            Log.e(TAG, "Download failed", e);
            if (callback != null) {
                callback.onError(e.getMessage());
            }
        } finally {
            // 7. 关闭所有资源
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    Log.e(TAG, "Error closing output stream", e);
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    Log.e(TAG, "Error closing input stream", e);
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
    // 下载回调接口
    public interface DownloadCallback {
        void onProgressUpdate(int progress);
        void onSuccess(String filePath);
        void onError(String errorMessage);
    }
}

在 Activity 或 Fragment 中调用

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
public class MainActivity extends AppCompatActivity implements FileDownloader.DownloadCallback {
    private ProgressBar progressBar;
    private TextView tvProgress;
    private Button btnDownload;
    private static final String DOWNLOAD_URL = "https://example.com/path/to/your/file.zip";
    private static final String FILE_NAME = "downloaded_file.zip";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar = findViewById(R.id.progressBar);
        tvProgress = findViewById(R.id.tvProgress);
        btnDownload = findViewById(R.id.btnDownload);
        btnDownload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 在后台线程执行下载
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        FileDownloader.downloadFile(DOWNLOAD_URL, FILE_NAME, MainActivity.this);
                    }
                }).start();
            }
        });
    }
    @Override
    public void onProgressUpdate(int progress) {
        // 更新UI必须在主线程
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                progressBar.setProgress(progress);
                tvProgress.setText("下载进度: " + progress + "%");
            }
        });
    }
    @Override
    public void onSuccess(String filePath) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this, "下载成功! 文件保存在: " + filePath, Toast.LENGTH_LONG).show();
                btnDownload.setText("下载完成");
            }
        });
    }
    @Override
    public void onError(String errorMessage) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this, "下载失败: " + errorMessage, Toast.LENGTH_SHORT).show();
                btnDownload.setText("重新下载");
            }
        });
    }
}

使用第三方库 (推荐)

手动实现下载逻辑容易出错,且功能有限(如断点续传、并发下载),在实际开发中,强烈推荐使用成熟的第三方库。

推荐库:

  1. OkHttp + Retrofit:现代 Android 开发的标准组合,OkHttp 专注于网络请求,Retrofit 对其进行封装,提供更友好的 API。
  2. Android Download Manager:一个系统服务,专为下载任务设计,它处理了线程、通知、断点续传等复杂逻辑,非常适合下载大文件。

方式二.1:使用 OkHttp 下载

OkHttp 的 ResponseBody 提供了 byteStream() 方法,可以方便地获取输入流,与 HttpURLConnection 的实现方式非常相似,但代码更简洁。

添加依赖

app/build.gradle 中添加:

implementation 'com.squareup.okhttp3:okhttp:4.9.3' // 使用最新版本

创建下载方法

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class OkHttpDownloader {
    private static final String TAG = "OkHttpDownloader";
    public static void downloadWithOkHttp(String fileUrl, String fileName, DownloadCallback callback) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(fileUrl).build();
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response);
            }
            // 获取响应体
            ResponseBody body = response.body();
            if (body == null) {
                throw new IOException("Response body is null");
            }
            long fileSize = body.contentLength();
            InputStream inputStream = body.byteStream();
            // ... (保存文件的逻辑与 HttpURLConnection 示例中的第3,4,5,6步完全相同) ...
            // 这里为了简洁,省略了重复代码,实际项目中应复用或封装。
        } catch (IOException e) {
            e.printStackTrace();
            if (callback != null) {
                callback.onError(e.getMessage());
            }
        }
    }
    // 回调接口与之前相同
    public interface DownloadCallback {
        void onProgressUpdate(int progress);
        void onSuccess(String filePath);
        void onError(String errorMessage);
    }
}

方式二.2:使用 Android Download Manager (系统服务)

这是处理大文件下载最简单、最高效的方式,它会自动在后台处理下载,并在通知栏显示进度。

添加权限

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Android 10+ 处理同上 -->

代码实现

import android.app.DownloadManager;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.widget.Toast;
public class DownloadManagerHelper {
    public static void startDownload(Context context, String fileUrl, String fileName) {
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(fileUrl));
        // 设置下载的保存位置
        // 对于 Android 10+,推荐使用 getExternalFilesDir()
        File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
        // 设置网络类型
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
        // 显示通知
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        // 设置标题
        request.setTitle("文件下载");
        request.setDescription("正在下载: " + fileName);
        // 获取 DownloadManager 服务并启动下载
        DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        if (downloadManager != null) {
            downloadManager.enqueue(request);
            Toast.makeText(context, "下载已开始", Toast.LENGTH_SHORT).show();
        }
    }
}

优点:

  • 简单:几行代码即可启动下载。
  • 强大:自动处理断点续传、并发下载、网络切换。
  • 用户体验好:自动在通知栏显示下载进度。

缺点:

  • 灵活性低:难以在自定义 UI(如 Activity 进度条)中实时获取精确的下载进度。
  • 依赖系统:不同 Android 版本的行为可能略有不同。

最佳实践与注意事项

  1. 不能在主线程进行网络操作 Android 4.0 (API 13) 之后,禁止在主线程执行网络请求,否则会抛出 NetworkOnMainThreadException,必须使用子线程、AsyncTaskExecutorServiceRxJavaKotlin Coroutines 来处理。

  2. 处理存储权限

    • Android 10 (API 29) 及以上:引入了 Scoped Storage,直接访问 Environment.getExternalStoragePublicDirectory() 可能会受限,推荐使用 Context.getExternalFilesDir() 目录,它不需要 WRITE_EXTERNAL_STORAGE 权限,但应用卸载后数据会被删除,如果需要公共访问,应使用 MediaStore API。
    • Android 11 (API 30) 及以上:对 MANAGE_EXTERNAL_STORAGE 权限的申请更加严格。
  3. 处理网络变化 在下载过程中,网络可能会断开。HttpURLConnectionOkHttp 默认会抛出异常,你需要捕获并处理。DownloadManager 会自动处理网络切换和断点续传。

  4. UI 更新 任何时候都不要在子线程中更新 UI,必须使用 Activity.runOnUiThread(), View.post(), Handler 等方式将 UI 操作切换回主线程。

  5. 资源释放 InputStreamOutputStream 是宝贵的系统资源,一定要确保它们在使用后被关闭,最佳实践是使用 try-with-resources 语句,它能自动关闭实现了 AutoCloseable 接口的对象。

总结与选择

特性 HttpURLConnection OkHttp Android Download Manager
易用性
灵活性
功能 基础 强大 (拦截器等) 强大 (系统级)
断点续传 需手动实现 需手动实现 自动
UI 集成 容易获取进度 容易获取进度 困难
依赖 无需 需添加 OkHttp 依赖 无需 (系统服务)
  • 新手学习/简单任务:从 HttpURLConnection 开始,有助于理解底层原理。
  • 常规 App 开发强烈推荐 OkHttp,它是现代网络请求的事实标准,灵活且强大。
  • 下载大文件/追求简单和稳定性强烈推荐 Android Download Manager,它能让你从复杂的下载逻辑中解放出来,专注于用户体验。
分享:
扫描分享到社交APP
上一篇
下一篇