Android 对断点续传的理解,文件多线程下载

今天要说的是Android断点续传的理解和具体如何实现利用多个任务去处理多个文件的断点续传

多文件的断点续传其实就是利用了多个任务,每个任务处理一个文件,和单个文件处理断点原理一样,下面是我对断点续传核心的理解

1.对本地文件的操作, 利用了RandomAccessFile的可以对文件任意位置进行读写和修改的操作,RandomAccessFile的seek()可以由指定位置对文件进行写入数据操作
2. 对网络的操作,利用HttpURLConnection中的setRequestProperty()方法我们可以从指定的位置去下载数据

核心代码

		  //设置下载起始位置
          int start = threadBean.getStart() + threadBean.getFinished();
          //传入当前进度给后台
          connection.setRequestProperty("Range", "bytes=" + start + "-" + threadBean.getEnd());
          TLog.log("第一次start" + start);
          //设置写入位置
          File file = new File(ConfigAppPath.downLoadPath, appBean.getAppName());
          raf = new RandomAccessFile(file, "rwd");
          raf.seek(start);

有了上述两个方法,其他的就是对线程和安卓数据库的操作了。下载是由线程去处理的,线程池操作,可以是多线程和单线程。数据库是为了存储我们下载的进度,比如我们点击暂停或下载完成时要记录进度,下次开始时要查询进度,在原进度基础上继续下载。所以实际应用之前我们对线程和数据库要有一定理解。

下面是利用上面知识进行实际应用

效果图

Android 对断点续传的理解,文件多线程下载

  1. 点击下载按钮后,会启动DownloadService的下载服务,刚开始下载时,在初始化线程InitThread中通过HttpURLConnection可以取得下载内容的大小,并初始化下载的文件地址,初始化完成之后创建下载任务进行下载。将该任务加入下载任务集合中,便于之后暂停时移除任务。(我们这个项目数据不能直接拿到内容大小,要经过对文件流和字节处理转换成int才能拿到文件大小,处理较为复杂,这里不用考虑这种情况)
Intent pauseIntent = new Intent(context, DownloadService.class);
                    pauseIntent.setAction(DownloadService.ACTION_PAUSE);
                    pauseIntent.putExtra("PeriodBean", periodBean);
                    context.startService(pauseIntent);
HttpURLConnection connection = null;
        RandomAccessFile randomAccessFile = null;
        InputStream inputStream = null;
        try {
            URL url = new URL(appBean.getUrl());
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(10000);
            connection.setRequestMethod("GET");
            connection.connect();
            int fileLength = -1;
            if(connection.getResponseCode() == HttpURLConnection.HTTP_OK){
                fileLength = connection.getContentLength();
            }
            if(fileLength<=0) return;
            if (Environment.getExternalStorageState().equals(
                    Environment.MEDIA_MOUNTED)) {
                File dir0 = new File(ConfigPath.demsPath);
                if (!dir0.exists()) {
                    dir0.mkdir();
                }
                File dir = new File(ConfigAppPath.downLoadPath);
                if (!dir.exists()) {
                    dir.mkdir();
                }
                File file = new File(dir, appBean.getAppName());
                randomAccessFile = new RandomAccessFile(file, "rwd");
                randomAccessFile.setLength(fileLength);
            }
//下载线程初始化完毕
                AppInfoBean appBean = (AppInfoBean) eventMessage.getObject();
                Log.w("AAA", "总length:" + appBean.getLength());
                //开始下载
                DownloadTask downloadTask = new DownloadTask(this, appBean, 3);
                downloadTasks.add(downloadTask);
  1. 一个下载任务对应一个下载文件,在一个下载任务中我们可以创建多条线程进行下载,并用线程池管理,增加了下载的效率。假设我们这里创建了三条线程,我们先要查询数据库是否有对应的ThreadBean,每条线下下载的进度信息放入ThreadBean中,并存储到数据库中。

  2. 数据库中没有拿到ThreadBean信息,说明是刚开始下载,我们创建三个ThreadBean类分别供三条线程使用,每条线程下载整个文件的1/3大小,每个ThreadBean包含id,URL、开始、结束、完成位置,具体这几个字段会在DownloadThread中使用,初始化玩ThreadBean之后,将ThreadBean插入数据库。然后创建三条DownloadThread下载,用线程池管理

private void initDownThreads(int downloadThreadCount) {
        //查询数据库中的下载线程信息
        threads = dao.getThreads(periodBean.getUrl(), mPhone);
        if (threads.size() == 0) {//如果列表没有数据 则为第一次下载
            Log.w("AAA", "第一次下载");
            //根据下载的线程总数平分各自下载的文件长度
            int length = periodBean.getLength() / downloadThreadCount;
            for (int i = 0; i < downloadThreadCount; i++) {
                ThreadBean thread = new ThreadBean(i, periodBean.getUrl(), i * length,
                        (i + 1) * length - 1, 0, mPhone);
                if (i == downloadThreadCount - 1) {
                    thread.setEnd(periodBean.getLength());
                }
                //将下载线程保存到数据库
                dao.insertThread(thread);
                threads.add(thread);
            }
            dao.insertPeriod(periodBean);
        }
        //创建下载线程开始下载
        for (ThreadBean thread : threads) {
            finishedProgress += thread.getFinished();
            DownloadThread downloadThread = new DownloadThread(periodBean, thread, this);
            DownloadService.executorService.execute(downloadThread);
            downloadThreads.add(downloadThread);
        }
        Log.w("AAA", " 开始下载:" + finishedProgress);
    }
  1. 这里开始就会用到本文开头介绍核心的地方,每次暂停时我们都会记录ThreadBean中finish位置(这个finish位置在进度条显示,下次开始下载的开始进度,下次写入文件的开始进度都会用到),我们开始下载时的start是初始化ThreadBean的start加上上次finish位置,例如如果是刚开始下载则finish为0,如果之前下载过一段,则finish有大小,下次开始的位置则需要加上上一次finish的大小,同理写入本地文件start同样操作。

  2. 下载通过while循环,获取的文件流不断读取字节方式进行,读取完成则表示该线程已下载完成,当点击暂停时,该任务会暂停所有线程,并保存当前进度,然后通过广播提示页面进度条显示。下次下载时会取出数据库中的ThreadBean重复步骤4。

//暂停下载
            else if (intent.getAction().equals(ACTION_PAUSE)) {
                AppInfoBean appBean = (AppInfoBean) intent.getSerializableExtra("appBean");
                DownloadTask pauseTask = null;
                for (DownloadTask downloadTask : downloadTasks) {
                    if (downloadTask.getFileBean().getId().equals(appBean.getId())) {
                        downloadTask.pauseDownload();
                        pauseTask = downloadTask;
                        break;
                    }
                }
                //将下载任务移除
                downloadTasks.remove(pauseTask);
            }
HttpURLConnection connection = null;
        RandomAccessFile raf = null;
        InputStream inputStream = null;
        try {
            URL url = new URL(threadBean.getUrl());
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(10000);
            connection.setRequestMethod("GET");
            //设置下载起始位置
            int start = threadBean.getStart() + threadBean.getFinished();
            //传入当前进度给后台
            connection.setRequestProperty("Range", "bytes=" + start + "-" + threadBean.getEnd());
            TLog.log("第一次start" + start);
            //设置写入位置
            File file = new File(ConfigAppPath.downLoadPath, appBean.getAppName());
            raf = new RandomAccessFile(file, "rwd");
            raf.seek(start);
            //开始下载
            if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                //获得文件流
                inputStream = connection.getInputStream();
                byte[] bytes = new byte[512];
                int len = -1;
                int count = 0;
                int i = 0;
                while ((len = inputStream.read(bytes)) != -1) {
                    //i++;
                    //TLog.log("count相加" + (count += len));
                    //写入文件
                    raf.write(bytes, 0, len);
                    //将加载的进度回调出去
                    callback.progressCallBack(len);
                    //TLog.log("len" + "i=" + i + "-" +(count += len));
                    //保存进度
                    threadBean.setFinished(threadBean.getFinished() + len);
                    //TLog.log("及时进度" + (threadBean.getFinished() + len));
                    //在下载暂停的时候将下载进度保存到数据库
                    if (isPause) {
                        callback.pauseCallBack(threadBean);
                        return;
                    }
                }
                //下载完成
                callback.threadDownLoadFinished(threadBean);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } 
  1. 这一步来讲进度条是如何显示的,刚开始我们该下载任务中几条线程的下载finish进度进行相加为进度条的初始进度,如果没有ThreadBean则为初始进度为0,while循环里每次下载一定字节数之后会返回大小length给下载任务,之后的进度条总长度则为初始长度加length,但是不能频繁发广播去更新进度条,所以我这里控制的是每100毫秒发一次去刷新进度条
@Override
    public void progressCallBack(int length) {
        //i++;
        //TLog.log("length" +"i=" + i + "-" + length);
        finishedProgress += length;
        //每100毫秒发送刷新进度事件
        if (System.currentTimeMillis() - curTime > 100 || finishedProgress == periodBean.getLength()) {
            TLog.log("finishedprogress", finishedProgress + "");
            periodBean.setFinished(finishedProgress);
            EventMessage message = new EventMessage(3, periodBean);
            EventBus.getDefault().post(message);
            curTime = System.currentTimeMillis();
        }
    }
  1. 页面会有四种广播回调,每种回调页面UI也随之变换,每隔100毫秒时进度条刷新,暂停时按钮变化,完成时刷新,异常时刷新,现在回到最开始打开页面时,拿到网络数据,我们要先将其本地存储的数据合并,进行页面显示
@Subscribe(threadMode = ThreadMode.MAIN)
    public void getEventMessage(EventMessage eventMessage) {
        switch (eventMessage.getType()) {
            case 2://下载完成
                break;
            case 3://下载进度刷新
                break;
            case 4://点击暂停
                break;
            case 6://下载失败异常
                break;
        }
    }
//合并mPeriods
private void refreshData() {
        //获得数据库中的items
        List<OfflinePeriodInfo> periodsData = mDao.getPeriods(SysCode.OFFLINE_DOWNLOAD_URL, mPhone);
        if (periodsData != null && periodsData.size() != 0) {
            for (int j = 0; j < mPeriods.size(); j++) {
                for (int k = 0; k < periodsData.size(); k++) {
                    if (mPeriods.get(j).getId().equals(periodsData.get(k).getId())) {
                        mPeriods.get(j).setFinished(periodsData.get(k).getFinished());
                        mPeriods.get(j).setLength(periodsData.get(k).getLength());
                    }
                }
            }
        }
    }
  1. 下面是adpter的主要代码
final ViewHolder viewHolder;
        //if (convertView == null || convertView.getTag() == null) {
        convertView = getLayoutInflater(parent.getContext()).inflate(
                R.layout.item_offline_download, null);
        viewHolder = new ViewHolder(convertView);
        /*} else {
            viewHolder = (ViewHolder) convertView.getTag();
        }*/
        if (_data != null && _data.size() > 0) {
            final OfflinePeriodInfo periodBean = (OfflinePeriodInfo) _data.get(position);
            viewHolder.tvProject.setText(periodBean.getPeriodName());
            int progress = (int) (periodBean.getFinished() * 1.0f / periodBean.getLength() * 100);
            if (periodBean.getLength() != 0) {
                viewHolder.progressBarDownload.setProgress(progress);
            }
            if (progress > 0 && progress < 100) {
                viewHolder.tvDownload.setVisibility(View.VISIBLE);
                viewHolder.tvPauseUpload.setVisibility(View.GONE);
                viewHolder.tvDownload.setText("继续");
                viewHolder.tvDownload.setTextColor(context.getResources().getColor(R.color.off_line_pause));
            }
            if (progress == 100) {
                viewHolder.tvDownload.setVisibility(View.VISIBLE);
                viewHolder.tvPauseUpload.setVisibility(View.GONE);
                viewHolder.tvDownload.setText("完成");
                viewHolder.progressBarDownload.setProgress(progress);
                viewHolder.tvDownload.setTextColor(context.getResources().getColor(R.color.common_blue_color));
            }
            //点击下载,
            viewHolder.tvDownload.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //判断网络
                    if (judgeInternet()) return;
                        periodBean.setPosition(position);
                        Intent startIntent = new Intent(context, DownloadService.class);
                        startIntent.setAction(DownloadService.ACTION_START);
                        startIntent.putExtra("PeriodBean", periodBean);
                        context.startService(startIntent);
                }
            });
            //点击暂停
            viewHolder.tvPauseDownload.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (judgeInternet()) return;
                    Intent pauseIntent = new Intent(context, DownloadService.class);
                    pauseIntent.setAction(DownloadService.ACTION_PAUSE);
                    pauseIntent.putExtra("PeriodBean", periodBean);
                    context.startService(pauseIntent);
                }
            });
        }
  1. 总结:界面功能主要通过四个类服务(Download)、下载任务(DownloadTask)、初始线程(InitThread)、下载线程(DownloadThread)协调页面完成,数据库辅助,只要知道流程和原理实现起来还是比较简单的。实际开发中遇到的情况可能要复杂很多,比如可能会遇到开头介绍拿contentLength的过程,可能会实现我这种折叠view的显示,可能下载完成的UI会变成上传等。不过一般不需要考虑,只要理解上述讲解的几个过程,这类问题都会迎刃而解,有不明白的可以在下面评论区提出问题