睿诚科技协会

Android网络文件下载如何实现?

  1. 获取下载链接:从服务器获取文件的 URL。
  2. 发起网络请求:使用 HTTP/HTTPS 请求获取文件数据流。
  3. 处理响应:检查服务器返回的状态码(如 200 OK)。
  4. 写入文件:将获取到的数据流以二进制形式写入到设备的存储中。
  5. 处理进度:实时计算并更新下载进度。
  6. 处理错误:处理网络中断、服务器错误、存储空间不足等各种异常情况。

下面我将从简单到复杂,介绍三种主流的实现方式:

Android网络文件下载如何实现?-图1
(图片来源网络,侵删)

使用 HttpURLConnection (原生 API,基础)

这是 Android 最基础的网络 API,不需要引入任何第三方库,适合简单的下载需求,但代码量稍多。

核心代码示例

public class DownloadUtil {
    private static final String TAG = "DownloadUtil";
    public interface DownloadListener {
        void onProgress(int progress);
        void onSuccess(File file);
        void onFailure(String error);
    }
    public static void downloadFile(String fileUrl, String saveDir, DownloadListener listener) {
        new Thread(() -> {
            InputStream is = null;
            FileOutputStream fos = null;
            HttpURLConnection connection = null;
            try {
                URL url = new URL(fileUrl);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                connection.setConnectTimeout(15000);
                connection.setReadTimeout(15000);
                connection.connect();
                int responseCode = connection.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_OK) {
                    // 获取文件总大小
                    int fileLength = connection.getContentLength();
                    is = connection.getInputStream();
                    // 确定保存路径
                    File dir = new File(saveDir);
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                    File outputFile = new File(dir, "downloaded_file_" + System.currentTimeMillis() + ".apk"); // 假设下载APK
                    fos = new FileOutputStream(outputFile);
                    int len;
                    int total = 0;
                    byte[] buffer = new byte[1024];
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                        total += len;
                        // 计算进度并回调 (注意:不要在子线程更新UI)
                        int progress = (int) ((total * 1.0 / fileLength) * 100);
                        // 使用Handler或其他方式将进度更新到UI线程
                        notifyProgress(listener, progress);
                    }
                    // 下载完成
                    notifySuccess(listener, outputFile);
                } else {
                    notifyFailure(listener, "Server returned HTTP " + responseCode);
                }
            } catch (IOException e) {
                notifyFailure(listener, "Download failed: " + e.getMessage());
                e.printStackTrace();
            } finally {
                // 关闭流和连接
                try {
                    if (is != null) is.close();
                    if (fos != null) fos.close();
                    if (connection != null) connection.disconnect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    // 使用Handler在UI线程回调
    private static final Handler handler = new Handler(Looper.getMainLooper());
    private static void notifyProgress(DownloadListener listener, int progress) {
        handler.post(() -> {
            if (listener != null) {
                listener.onProgress(progress);
            }
        });
    }
    private static void notifySuccess(DownloadListener listener, File file) {
        handler.post(() -> {
            if (listener != null) {
                listener.onSuccess(file);
            }
        });
    }
    private static void notifyFailure(DownloadListener listener, String error) {
        handler.post(() -> {
            if (listener != null) {
                listener.onFailure(error);
            }
        });
    }
}

使用示例

// 在Activity或Fragment中
DownloadUtil.downloadFile(
    "https://example.com/path/to/your/app.apk",
    getExternalFilesDir(null).getAbsolutePath(), // 下载到应用私有目录
    new DownloadUtil.DownloadListener() {
        @Override
        public void onProgress(int progress) {
            // 更新UI,例如设置ProgressBar
            progressBar.setProgress(progress);
            tvProgress.setText(progress + "%");
        }
        @Override
        public void onSuccess(File file) {
            // 下载成功,可以提示用户或打开文件
            Toast.makeText(MainActivity.this, "Downloaded to: " + file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
        }
        @Override
        public void onFailure(String error) {
            // 下载失败
            Toast.makeText(MainActivity.this, "Error: " + error, Toast.LENGTH_SHORT).show();
        }
    }
);

使用 OkHttp (强烈推荐)

OkHttp 是目前 Android 开发中最流行的网络库,它更高效、更易用,并且内置了对异步请求、回调处理的支持。

添加依赖

app/build.gradle 文件中添加:

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0") // 使用最新版本
}

核心代码示例

OkHttp 可以直接将响应体写入文件,代码非常简洁。

Android网络文件下载如何实现?-图2
(图片来源网络,侵删)
public class OkHttpDownloadUtil {
    private static final String TAG = "OkHttpDownload";
    public interface DownloadListener {
        void onProgress(int progress);
        void onSuccess(File file);
        void onFailure(String error);
    }
    public static void download(String url, String saveDir, DownloadListener listener) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(url).build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                // 请求失败,在主线程回调
                notifyFailure(listener, "Download failed: " + e.getMessage());
            }
            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                if (!response.isSuccessful()) {
                    notifyFailure(listener, "Server returned: " + response.code());
                    return;
                }
                // 获取输入流和文件总大小
                InputStream is = response.body().byteStream();
                long contentLength = response.body().contentLength();
                File dir = new File(saveDir);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File outputFile = new File(dir, "downloaded_file_" + System.currentTimeMillis() + ".apk");
                FileOutputStream fos = new FileOutputStream(outputFile);
                byte[] buffer = new byte[1024];
                int len;
                long total = 0;
                try {
                    while ((len = is.read(buffer)) != -1) {
                        total += len;
                        fos.write(buffer, 0, len);
                        // 计算进度
                        if (contentLength > 0) {
                            int progress = (int) ((total * 1.0 / contentLength) * 100);
                            notifyProgress(listener, progress);
                        }
                    }
                    fos.flush();
                    // 下载成功
                    notifySuccess(listener, outputFile);
                } catch (IOException e) {
                    // 如果发生异常,删除可能不完整的文件
                    if (outputFile.exists()) {
                        outputFile.delete();
                    }
                    notifyFailure(listener, "Write file failed: " + e.getMessage());
                } finally {
                    try {
                        if (is != null) is.close();
                        if (fos != null) fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }
    // 同样需要Handler在UI线程回调
    private static final Handler handler = new Handler(Looper.getMainLooper());
    // ... (notifyProgress, notifySuccess, notifyFailure 方法与上面HttpURLConnection示例中相同) ...
}

使用示例

HttpURLConnection 的使用方式几乎完全相同,只是调用方法名不同。


使用 DownloadManager (系统服务,推荐用于大文件)

对于系统级的、大文件的下载,Android 提供了一个强大的系统服务 DownloadManager,它会处理很多底层细节,如:

  • 在后台下载,即使应用关闭。
  • 系统重启后自动恢复下载。
  • 处理网络切换。
  • 提供系统下载通知栏。
  • 自动处理重试逻辑。

添加权限

AndroidManifest.xml 中添加必要的权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 如果目标SDK是33 (Android 13) 或更高,需要这个权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 如果需要下载到公共目录,需要这个权限,但更推荐使用应用私有目录 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

核心代码示例

public class DownloadManagerUtil {
    public static void downloadWithSystemManager(Context context, String url, String fileName) {
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        // 设置下载的保存位置
        // 1. 下载到应用私有目录(不需要READ/WRITE_EXTERNAL_STORAGE权限,更安全)
        File outputDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
        if (outputDir == null) {
            Toast.makeText(context, "Cannot access external storage", Toast.LENGTH_SHORT).show();
            return;
        }
        File outputFile = new File(outputDir, fileName);
        request.setDestinationUri(Uri.fromFile(outputFile));
        // 2. 下载到公共下载目录(需要权限)
        // request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
        // 设置通知的标题和描述
        request.setTitle("Downloading File");
        request.setDescription("Downloading " + fileName);
        // 设置为可见下载,并在通知栏显示进度
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        // 允许在移动网络和Wi-Fi上下文中下载
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
        // 获取系统DownloadManager服务
        DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        if (downloadManager != null) {
            long downloadId = downloadManager.enqueue(request);
            // 可以保存这个downloadId,之后用它来查询下载状态
            Log.d("DownloadManager", "Download started with ID: " + downloadId);
        }
    }
    // 如何查询下载状态?
    public static void checkDownloadStatus(Context context, long downloadId) {
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(downloadId);
        DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        if (downloadManager != null) {
            Cursor cursor = downloadManager.query(query);
            if (cursor.moveToFirst()) {
                int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
                switch (status) {
                    case DownloadManager.STATUS_PAUSED:
                        Log.d("DownloadStatus", "Download is paused");
                        break;
                    case DownloadManager.STATUS_PENDING:
                        Log.d("DownloadStatus", "Download is pending");
                        break;
                    case DownloadManager.STATUS_RUNNING:
                        Log.d("DownloadStatus", "Download is in progress");
                        // 可以在这里获取进度
                        int bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                        int totalBytes = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                        int progress = (int) ((bytesDownloaded * 100) / (double) totalBytes);
                        Log.d("DownloadStatus", "Progress: " + progress + "%");
                        break;
                    case DownloadManager.STATUS_SUCCESSFUL:
                        Log.d("DownloadStatus", "Download is completed");
                        String localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                        Log.d("DownloadStatus", "File saved at: " + localUri);
                        break;
                    case DownloadManager.STATUS_FAILED:
                        Log.d("DownloadStatus", "Download has failed");
                        break;
                }
            }
            cursor.close();
        }
    }
}

总结与对比

特性 HttpURLConnection OkHttp DownloadManager
依赖 无需 (Android原生) 需添加 okhttp 无需 (Android原生)
易用性 中等,代码量较多 ,API简洁 简单,但配置项较多
灵活性 ,可完全控制下载过程 ,可完全控制下载过程 ,功能固定
后台下载 不支持,应用关闭会中断 不支持,应用关闭会中断 支持,应用关闭甚至重启后仍可继续
通知栏 需自己实现 需自己实现 自动提供
断点续传 需自己实现 (设置 Range 请求头) 需自己实现 (设置 Range 请求头) 自动支持
大文件处理 不推荐,容易ANR 推荐,有异步回调 强烈推荐,专为后台大文件设计
适用场景 简单、小文件下载,或不想引入第三方库时 通用场景,几乎所有网络请求 下载APK、大文件、需要系统级通知和断点续传的场景

最佳实践建议

  1. 权限管理

    Android网络文件下载如何实现?-图3
    (图片来源网络,侵删)
    • Android 10 (API 29) 及以上,默认情况下应用只能访问自己的外部存储目录 (Context.getExternalFilesDir()),无需申请 WRITE_EXTERNAL_STORAGE 权限,这是最推荐的做法。
    • 如果必须访问公共目录,需要声明 MANAGE_EXTERNAL_STORAGE 权限,并且需要向用户解释原因。
  2. 线程管理

    • 所有网络操作都不能在主线程(UI线程)中进行,否则会抛出 NetworkOnMainThreadException 异常,使用 Thread, AsyncTask (已废弃), ExecutorService 或 OkHttp 自带的异步回调。
  3. 断点续传

    • 如果需要实现断点续传,你需要手动处理,做法是:
      1. 首先检查本地文件是否存在。
      2. 如果存在,获取文件当前大小。
      3. 在发起 HTTP 请求时,添加 Range 请求头,Range: bytes=已下载大小-
      4. 服务器需要支持 206 Partial Content 响应。
      5. 下载时,使用 RandomAccessFile 以追加模式 ("rw") 写入文件,而不是覆盖。
  4. 安全性

    强烈建议使用 HTTPS 协议下载文件,以防止中间人攻击和数据篡改。

对于大多数现代 Android 应用,OkHttp 是最平衡、最灵活的选择,如果你需要下载系统级的大文件(如应用更新包),DownloadManager 是不二之选。HttpURLConnection 则适合在不想引入任何第三方库的简单项目中使用。

分享:
扫描分享到社交APP
上一篇
下一篇