26
2017
09

安卓文件下载之断点续传(一)

最近学了一段时间node js,结果公司暂时没有业务代码用的到,又先转回了安卓这边。因为之前手机应用更新这一块逻辑一直有些问题,在讨论了一番后决定花段时间把这块重做一遍,更新怎么少的了文件下载,文件下载中又有许多需要注意或者说可以优化的地方,断点续传就是其中的一种优化方式,看了许多网上的例子,自己在经过一周的时间后,总算是做了一个过的去的 demo ,目前 demo 比较完善的实现了开启一个线程断点下载的功能,DownloadManager的方式正在实现中,两者的思路是完全相同的,下面就让我们对这个文件下载 demo 制作的过程进行一下分析:

首先想要实现断点续传当然需要记录终端的 在哪,这里的点指的就是文件已经下载的大小,你需要让服务器知道你所已经下载的大小是多少,然后服务器才能返回具体那一段的数据,当然我们还需要记录一些别的内容,如文件名,文件大小,临时文件等等。这里我选用数据库记录这些字段,选择使用了 greedao 这个第三方库,目前项目添加的依赖为:

    compile 'de.greenrobot:DaoGenerator:1.3.0'
    compile 'de.greenrobot:greendao:1.3.7'

库的版本比较老了,有想使用新版本的可以去百度搜一下,介绍有很多,这里就不再赘述,该 demo 以1.3版本为例。

首先我们创建一个 CreateDBImpl 类用来生成数据库相关文件:


/** * Created by 冒险者ztn on 2017/9/11. */

public class CreateDBImpl {

    public static void main(String args[]) throws Exception {
        Schema schema = new Schema(1, "com.example.zhangtianning.download.dao");
        addDownloadFileSize(schema);
        DaoGenerator daoGenerator = new DaoGenerator();
        String PATH = "app/src/main/java/";
        daoGenerator.generateAll(schema, PATH);
    }

    private static void addDownloadFileSize(Schema schema) {
        Entity downloadInfo = schema.addEntity("DownloadFileInfo");
        downloadInfo.addIdProperty().primaryKey();

        downloadInfo.addLongProperty("fileSize");
        downloadInfo.addStringProperty("filePath");
        downloadInfo.addStringProperty("fileName");

        downloadInfo.addStringProperty("tempFilePath");

        downloadInfo.addStringProperty("downloadUrl");

        downloadInfo.addLongProperty("hadDownloadSize");
        downloadInfo.addIntProperty("downloadProgress");

        downloadInfo.addIntProperty("version");

    }
}

我在数据库中记录了这么几个字段,右键run一下生成对应的文件:

这里写图片描述

写一个接口,看看我们可能会有那些用到的方法:


/** * 接口 * Created by 冒险者ztn on 2017/9/11. */

public interface DBInstances {

    /** * 获取所有下载内容信息 */
    List<DownloadFileInfo> getDownloadFileInfo();

    /** * 根据url获取对应下载内容信息 */
    DownloadFileInfo getDownloadFileInfoWithUrl(String url);


    /** * 保存下载内容信息 */

    void setDownloadFileInfo(DownloadFileInfo downloadFileInfo);


    /** * 清除数据 */

    void clearDownloadFileInfo();


    /** * 是否有完整文件 * * @param downloadFileInfo * @return */
    Boolean hasCompleteFile(DownloadFileInfo downloadFileInfo);

    /** * 初始化一条数据 * * @param url * @return */
    DownloadFileInfo initBaseDownloadFileInfo(String url);


    /** * 更新一条数据 * * @param downloadFileInfo */
    void Updata(DownloadFileInfo downloadFileInfo);
}

因为做的是个demo 功能与实际需求还有一定的区别,这里大家可以根据需求实现自己的方法。

然后就是下载的流程了,这里我制作了一张流程图,方便大家能看的更清楚一些,之后我们代码的书写过程也会参考这张流程图来写,让人有更清晰的思路:
下载文件流程图

我们先来实现最开始的几个功能看这个 url 是否符合规矩并在我们的数据库中有所记录:

   private void urlIsInDB(Context context, String url) {

        if (url.indexOf("http") == 0
                || url.indexOf("https") == 0) {
            downloadFileInfo = DBDaoImpl.getInstance(context).getDownloadFileInfoWithUrl(url);

            if (downloadFileInfo != null) {
                if (TextUtils.equals(downloadFileInfo.getHadDownloadSize().toString(), downloadFileInfo.getFileSize().toString())) {
                    File file = new File(downloadFileInfo.getFilePath());
                    if (file.exists()) {
                        downloadState = STATE_FINISH_DOWNLOAD;
                        OpenApk.getInstance().openFile(mainActivity, file);
                    } else {
                        downloadFileInfo.setHadDownloadSize((long) 0);
                        DBDaoImpl.getInstance(context).Updata(downloadFileInfo);
                    }

                } else {
                    fileDownload.start(mainActivity, downloadFileInfo);
                    Message message = myWeakHandler.obtainMessage();
                    downloadState = message.what = STATE_START_DOWNLOAD;
                    myWeakHandler.sendMessage(message);
                }
            } else {
                LogUtils.D(TAG, "没有文件信息!");
                fileDownload.start(mainActivity, etUrl.getText().toString());
            }

        } else {
            Toast.makeText(mainActivity, "请输入一个正确的url", Toast.LENGTH_SHORT).show();
        }
    }

然后在FileDownload类中开启线程:

 /** * 开始下载 * * @param downloadurl 文件url地址 */
    public void start(Activity activity, String downloadurl) {
        start(activity, DBDaoImpl.getInstance(activity).initBaseDownloadFileInfo(downloadurl));
    }

    /** * 已经有记录开始下载 * * @param downloadFileInfo 下载信息 */
    public void start(Activity activity, DownloadFileInfo downloadFileInfo) {
        downloadThread = new DownloadThread(downloadFileInfo, mFileDownloadListener, activity);
        downloadThread.start();
    }

最终归并于一个方法,并开启了线程。

在线程中我们初始化一个HttpURLConnection ,并进行一些设置,在这里遇到了关于不少问题,尤其是对于header的一些参数的设置,如获取文件大小为-1,无法下载文件等等,在尝试多遍后目前的设置为:

url = new URL(this.mDownloadUrl);
            httpURLConnection = (HttpURLConnection) url.openConnection();
            httpURLConnection.setUseCaches(false); // 请求时不使用缓存
            httpURLConnection.setConnectTimeout(5 * 1000); // 设置连接超时时间
            httpURLConnection.setRequestMethod("GET");
            httpURLConnection.setRequestProperty("Accept-Language", "zh-cn");
            httpURLConnection.setRequestProperty("UA-CPU", "x86");
            httpURLConnection.setRequestProperty("Accept-Encoding", "gzip");//为什么没有deflate呢
//            httpURLConnection.setRequestProperty("Content-type", "text/html");
            httpURLConnection.setRequestProperty("Connection", "close");


//            httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
            httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)");
            httpURLConnection.setRequestProperty("Accept",
                    "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*");
//            httpURLConnection.setRequestProperty("User-Agent", " Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36");

可以看到我在最开始并没有向header中设置文件从哪开始下载,因为httpURLConnection不能在get某个参数在对某个参数set,如果get代表链接已经发出,就不能在设置了,接下来我们看开断点续传的关键代码:

long downloadSize = mDownloadFileInfo.getHadDownloadSize();

            String fileName = mDownloadFileInfo.getFileName();


            long fileLength;

            if (downloadSize == mDownloadFileInfo.getFileSize()) {
                OpenApk.getInstance().openFile(ctx, new File(mDownloadFileInfo.getFilePath()));
                interrupt();
            }
            if (downloadSize > 0) {
                String tmpFilePath = Environment.getExternalStorageDirectory().getPath() + FILE_DOWNLOAD_TEMP_DIR + fileName;
                LogUtils.D("Tag", "临时文件的路径:" + tmpFilePath);
                fileLength = mDownloadFileInfo.getFileSize();
                httpURLConnection.setRequestProperty("Range",
                        "bytes=" + mDownloadFileInfo.getHadDownloadSize() + "-"
                                + mDownloadFileInfo.getFileSize());
                tmpFile = new File(tmpFilePath);
                if (!tmpFile.exists() || !tmpFile.isFile()) {
                    tmpFile = FileUtils.createTempFile(tmpfileBasePath, fileName);
                    mDownloadFileInfo.setTempFilePath(tmpFile.getAbsolutePath());
                }
            } else {
                fileLength = httpURLConnection.getContentLength(); // 获取文件的大小

                mDownloadFileInfo.setFileSize(fileLength);

                if (fileName == null) {
                    fileName = httpURLConnection.getHeaderField("Content-Disposition"); // 获取文件名
                }
                if (fileName == null) {
                    fileName = mDownloadUrl.substring(mDownloadUrl.lastIndexOf("/") + 1);
                }
                mDownloadFileInfo.setFileName(fileName);
                mDownloadFileInfo.setFilePath(basePath + File.separator + fileName);
                String tmpFilePath = Environment.getExternalStorageDirectory().getPath() + FILE_DOWNLOAD_TEMP_DIR + fileName;
                LogUtils.D("Tag", "临时文件的路径:" + tmpFilePath);
                tmpFile = FileUtils.createTempFile(tmpfileBasePath, fileName);
                mDownloadFileInfo.setTempFilePath(tmpFile.getAbsolutePath());
            }

我们从数据库中获取已下载文件大小,如果为-1的话我们认为这个文件从未下载过,并尝试从请求中获取文件名,与文件大小,并对数据库中的字段一一进行设置。如果下载文件大于0,那么我们认为数据库中已经有了对应的完整的文件信息,并对header中的Range字段进行设置。

接下来自然是获取请求的状态码了:

int code = httpURLConnection.getResponseCode();

这里有个地方需要注意,正常的状态码为200,断点续传的状态码为206,所以如果已下载部分文件,状态码却依然返回200,那么我们认为服务器是不支持断点续传的,需要重新下载:

if (code == HttpURLConnection.HTTP_OK) {
                if (mDownloadFileInfo.getHadDownloadSize() > 0) {
                    // 子线程
                    ToastUtils.showToast(ctx, "文件不支持断点续传,已重新开始下载!");
                    mDownloadFileInfo.setHadDownloadSize((long) -1);
                }
                long currentTime = System.currentTimeMillis();
                int bufferSize = 1024;
                bufferedInputStream = new BufferedInputStream(httpURLConnection.getInputStream(), bufferSize);
                int len; //读取到的数据长度
                byte[] buffer = new byte[bufferSize];
                //写入中间文件
                mOutputStream = new FileOutputStream(tmpFile, true);//true表示向打开的文件末尾追加数据
                mByteOutput = new ByteArrayOutputStream();
                // 开始读取
                while ((len = bufferedInputStream.read(buffer)) != -1) {
                    mByteOutput.write(buffer, 0, len);
                    mDownloadFileInfo = writeCache(mDownloadFileInfo);
                    progress = DownloadUtils.getProgress(mDownloadFileInfo.getHadDownloadSize(), fileLength);
                    long nowTime = System.currentTimeMillis();
                    if (currentTime < nowTime - 500) {
                        currentTime = nowTime;

                        mDownloadFileInfo.setDownloadProgress(progress);
                        if (mFileDownloadListener != null) {
                            if (downloadSize == fileLength) {
                                mFileDownloadListener.onFileDownloadCompleted(mDownloadFileInfo);
                                break;
                            } else {
                                mFileDownloadListener.onFileDownloading(mDownloadFileInfo);
                            }
                        }
                    }
                    if (isStopDownload) {
                        if (mFileDownloadListener != null) {
                            mFileDownloadListener.onFileDownloadPaused(mDownloadFileInfo);
                        }
                        break;
                    }
                }

            } else if (code == HttpURLConnection.HTTP_PARTIAL) {
                long currentTime = System.currentTimeMillis();
                int bufferSize = 1024;
                bufferedInputStream = new BufferedInputStream(httpURLConnection.getInputStream(), bufferSize);
                int len; //读取到的数据长度
                byte[] buffer = new byte[bufferSize];
                //写入中间文件
                mOutputStream = new FileOutputStream(tmpFile, true);//true表示向打开的文件末尾追加数据
                mByteOutput = new ByteArrayOutputStream();
                // 开始读取
                while ((len = bufferedInputStream.read(buffer)) != -1) {
                    mByteOutput.write(buffer, 0, len);
                    mDownloadFileInfo = writeCache(mDownloadFileInfo);
                    progress = DownloadUtils.getProgress(mDownloadFileInfo.getHadDownloadSize(), fileLength);
                    long nowTime = System.currentTimeMillis();
                    if (currentTime < nowTime - 500) {
                        currentTime = nowTime;
                        mDownloadFileInfo.setDownloadProgress(progress);
                        if (mFileDownloadListener != null) {
                            if (downloadSize == fileLength) {
                                mFileDownloadListener.onFileDownloadCompleted(mDownloadFileInfo);
                                break;
                            } else {
                                mFileDownloadListener.onFileDownloading(mDownloadFileInfo);
                            }
                        }
                    }
                    if (isStopDownload) {
                        if (mFileDownloadListener != null) {
                            mFileDownloadListener.onFileDownloadPaused(mDownloadFileInfo);
                        }
                        break;
                    }
                }
            }

最终效果图:
这里写图片描述

上面只是逻辑部分的代码,可以下载我github上面的demo作为参考,欢迎提问,欢迎star。
github项目地址:文件下载demo

上一篇:AR项目实践二:ar直尺 下一篇:iOS 关于UILabel文本的自适应