Android 断点续传显示进度的坑

前言:

之前有个需求就是打开APP的时候,判断当前版本是不是最新的,如果不是最新的就后台静默下载,下载完成后弹框让用户选择是否安装。

需要支持断点续传,比如你下载了20%,退出APP进程,然后再次打开的时候从20%的位置继续下载而不是从头开始下载。

代码很简单,我贴出来

dart 复制代码
package com.example.android_apk_install;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;

import androidx.core.content.FileProvider;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class ApkInstallHelper {

    private static final String TAG = "ApkInstallHelper";
    private final static String URL_PATH = "https://xxx/app/apk/";
    private final Context context;
    private OnDownloadListener onDownloadListener;
    private final static String SP_NAME = "DIM_APK_DOWNLOAD";
    private final static String SP_KEY = "DOWNLOAD_BYTES_SO_FAR";
    private final SharedPreferences sharedPreferences;

    public ApkInstallHelper(Context context) {
        this.context = context;
        this.sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
    }

    public void setOnDownloadListener(OnDownloadListener listener) {
        this.onDownloadListener = listener;
    }


    public void downloadByContinue(File apkDestFile, String apkName) {
        new Thread(() -> {
            long startPosition = 0;
            long endPosition = 0;
            String urlStr = URL_PATH + apkName;
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(20_000);
                conn.setRequestMethod("GET");
                //获取下载文件的总大小
                endPosition = conn.getContentLength();

                // 下载的临时文件存在的话,才会断点下载
                File tempFile = new File(apkDestFile, apkName + ".temp");
                if (tempFile.exists()) {
                    startPosition = sharedPreferences.getLong(SP_KEY, 0L);
                }

                DownLoadTask2 downLoadTask2 = new DownLoadTask2(urlStr, apkName, startPosition, endPosition, apkDestFile);
                Thread thread = new Thread(downLoadTask2);
                thread.setName("appDownloadThread");
                thread.start();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }).start();
    }

    class DownLoadTask2 implements Runnable {
        private final String url;
        private final String fileName;
        private final long startPosition;
        private final long endPosition;
        private final File destFile;

        public DownLoadTask2(String url, String fileName, long startPos, long endPos, File destFile) {
            this.url = url;
            this.fileName = fileName;
            this.startPosition = startPos;
            this.endPosition = endPos;
            this.destFile = destFile;
        }

        @Override
        public void run() {
            Log.e("DownLoadTask2", Thread.currentThread().getName() + " , startPosition >>> " + startPosition + " , endPosition = " + endPosition);
            // 下载临时文件名,下载完成后重新命名
            File tempFile = new File(destFile, fileName + ".temp");
            HttpURLConnection conn;
            try {
                Handler mainHandler = new Handler(Looper.getMainLooper());
                long start = System.currentTimeMillis();
                URL url2 = new URL(url);
                conn = (HttpURLConnection) url2.openConnection();
                conn.setConnectTimeout(5_000);
                conn.setRequestMethod("GET");

                //设置当前线程下载的起点,终点
                conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
                //使用java中的RandomAccessFile 对文件进行随机读写操作
                int responseCode = conn.getResponseCode();
                long fileSize = conn.getContentLength();
                if (206 == responseCode) {
                    RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "rw");
                    //设置开始写文件的位置
                    randomAccessFile.seek(startPosition);
                    int len;
                    byte[] buffer = new byte[1024];
                    InputStream is = conn.getInputStream();
                    long total = startPosition;
                    try {
                        while ((len = is.read(buffer)) != -1) {
                            randomAccessFile.write(buffer, 0, len);
//                            Log.e("DownLoadTask2", Thread.currentThread().getName() +
//                                    " , total >>> " + total + ", len = " + len +" , fileSize = " +fileSize +" , endPosition = " + endPosition);
                            total += len;
                            sharedPreferences.edit().putLong(SP_KEY, total).apply();
                            // 为什么这里是除以 endPosition 而不是 fileSize 呢? 因为如果是断点下载,假设文件大小 70M ,第一次下载了20M,APP杀掉进程,再次打开app会断点下载,从20M开始,那么
                            // 此时的 fileSize = 50M 了,那么下载之后进度就会大于 100%
                            String progress = total * 100 / endPosition + "%";
                            Log.e("DownLoadTask2", Thread.currentThread().getName() + " , progress >>> " + progress);
                            if (onDownloadListener != null) {
                                mainHandler.post(() -> onDownloadListener.downloadProgress(progress));
                            }
                        }
                    } catch (Exception exception) {
                        exception.printStackTrace();
                        Log.d(" Error:", exception.getMessage());
                    } finally {
                        is.close();
                        randomAccessFile.close();
                    }
                } else {
                    Log.e(TAG, "--------------下载出错-------------responseCode = " + responseCode + " , url = " + url);
                    new Handler(Looper.getMainLooper()).post(() -> {
                        if (onDownloadListener != null)
                            onDownloadListener.downloadSuccess(false);
                    });
                    return;
                }
                long time = System.currentTimeMillis() - start;
                Log.e("DownLoadTask2", Thread.currentThread().getName() + " time = " + time);

                Log.e(TAG, "--------------下载完成-------------");
                // 下载成功后,重新命名为正式名称,以此判断是否下载完成,
                boolean res = tempFile.renameTo(new File(destFile, fileName));
                if (!res) {
                    Log.e(TAG, tempFile.getAbsolutePath() + " , 重命名失败");
                    new Handler(Looper.getMainLooper()).post(() -> {
                        if (onDownloadListener != null)
                            onDownloadListener.downloadSuccess(false);
                    });
                    return;
                }
                // 下载完成后删除sp中的下载大小
                sharedPreferences.edit().clear().apply();
                new Handler(Looper.getMainLooper()).post(() -> {
                    if (onDownloadListener != null)
                        onDownloadListener.downloadSuccess(true);
                });
            } catch (IOException e) {
                Log.d(" Error:", e.getMessage());
            }
        }
    }

    


    public interface OnDownloadListener {
        void downloadSuccess(boolean success);
        void downloadProgress(String progress);
    }
}

我出错误的地方在哪里呢?就是在计算下载进度的时候,开始写的是 String progress = total * 100 / fileSize+ "%"; 结果发现,下载部分后,退出 app,再次打开 app 的时候 进度超过 100%了,简直不忍直视啊,幸亏我自测严谨啊,哈哈,然后debug 了一下,发现 fileSize 不是文件的原始大小了,而是减去已下载的部分了,找到问题原因就好解决了,代码改成如下方式:

dart 复制代码
String progress = total * 100 / endPosition + "%";

endPosition 表示的是下载文件的原始大小。

最终,问题圆满解决。

当你在使用 HttpURLConnection 设置请求头 "Range" 来请求文件的部分内容时,httpURLConnection.getContentLength() 返回的将是从 startPositionendPosition 之间的字节大小 ,而不是整个文件的大小

我这里使用的就是 : conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);

详细说明:

  • Range 请求:Range 请求头允许你指定文件中你想获取的字节范围。例如,"Range": "bytes=500-999" 将请求从文件中第500到999字节的内容。
  • Content-Length 返回值:当你设置了 Range 请求头后,HttpURLConnection 的 getContentLength() 方法会返回该请求范围内的数据大小,也就是从 startPosition 到 endPosition 的字节数,而不是整个文件的大小。

举个例子:

如果文件大小为1000字节,你设置了 "Range": "bytes=500-999",那么 getContentLength() 将返回500(即999-500+1)

注意:

  1. 如果没有设置 Range 请求头,则 getContentLength() 返回的是整个文件的大小。
  2. 如果你想获取整个文件的大小,可以考虑在不设置 Range 请求头的情况下,单独发起一个请求获取文件大小,然后再进行分块下载。
    通过使用这个方法,你可以有效地处理大文件的下载或者断点续传。
  3. conn = (HttpURLConnection) url2.openConnection() 一次 connection 只能调用一次 conn.getContentLength(),否则会抛出异常。
相关推荐
安之若素^4 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan9911 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc38 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list