android AsyncTask实现多线程断点续传

前面一篇博客《AsyncTask实现断点续传》讲解了如何实现单线程下的断点续传,也就是一个文件只有一个线程进行下载。

    对于大文件而言,使用多线程下载就会比单线程下载要快一些。多线程下载相比单线程下载要稍微复杂一点,本博文将详细讲解如何使用AsyncTask来实现多线程的断点续传下载。

一、实现原理

  多线程下载首先要通过每个文件总的下载线程数(我这里设定5个)来确定每个线程所负责下载的起止位置。

复制代码
        long blockLength = mFileLength / DEFAULT_POOL_SIZE;
        for (int i = 0; i < DEFAULT_POOL_SIZE; i++) {
            long beginPosition = i * blockLength;//每条线程下载的开始位置
            long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置
            if (i == (DEFAULT_POOL_SIZE - 1)) {
                endPosition = mFileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度
            }
           ......
        }
复制代码

  这里需要注意的是,文件大小往往不是线程个数的整数倍,所以最后一个线程的结束位置需要设置为文件长度。

  确定好每个线程的下载起止位置之后,需要设置http请求头来下载文件的指定位置:

connection.setRequestProperty("Range", "bytes=" + beginPosition + "-" + endPosition);

 以上是多线程下载的原理,但是还要实现断点续传需要在每次暂停之后记录每个线程已下载的大小,下次继续下载时从上次下载后的位置开始下载。一般项目中都会存数据库中,我这里为了简单起见直接存在了SharedPreferences中,已下载url和线程编号作为key值。

复制代码
 1        @Override
 2         protected void onPostExecute(Long aLong) {
 3             Log.i(TAG, "download success ");
 4             //下载完成移除记录
 5             mSharedPreferences.edit().remove(currentThreadIndex).commit();
 6         }
 7 
 8         @Override
 9         protected void onCancelled() {
10             Log.i(TAG, "download cancelled ");
11             //记录已下载大小current
12             mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
13         }
复制代码

  下载的时候,首先获取已下载位置,如果已经下载过,就从上次下载后的位置开始下载:

复制代码
      //获取之前下载保存的信息,从之前结束的位置继续下载
      //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载
      long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
      if(file.exists() && downedPosition != 0) {
          beginPosition = beginPosition + downedPosition;
          current = downedPosition;
          synchronized (mCurrentLength) {
               mCurrentLength += downedPosition;
          }
      }
复制代码

二、布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
      android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
      android:paddingRight="@dimen/activity_horizontal_margin"
      android:paddingTop="@dimen/activity_vertical_margin"
      android:paddingBottom="@dimen/activity_vertical_margin"
      tools:context="com.test.Main7Activity"
      android:orientation="vertical">

    <Button
        android:id="@+id/begin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始下载"/>

    <Button
        android:id="@+id/end"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="暂停下载"/>

    <ProgressBar
        android:id="@+id/progressbar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="fill_parent"
        android:layout_height="3dp"
        android:layout_marginTop="10dp"
        android:max="100" />
    <TextView
        android:id="@+id/percent_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

三、完整代码

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class Main7Activity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private static final int DEFAULT_POOL_SIZE = 5;
    private static final int GET_LENGTH_SUCCESS = 1;
    //下载路径
    private String downloadPath = Environment.getExternalStorageDirectory() + File.separator + "download";

    //private String mUrl = "http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz";
    private String mUrl = "http://p.gdown.baidu.com/c4cb746699b92c9b6565cc65aa2e086552651f73c5d0e634a51f028e32af6abf3d68079eeb75401c76c9bb301e5fb71c144a704cb1a2f527a2e8ca3d6fe561dc5eaf6538e5b3ab0699308d13fe0b711a817c88b0f85a01a248df82824ace3cd7f2832c7c19173236";
    private ProgressBar mProgressBar;
    private TextView mPercentTV;
    SharedPreferences mSharedPreferences = null;
    long mFileLength = 0;
    Long mCurrentLength = 0L;

    private InnerHandler mHandler = new InnerHandler();

    private List<DownloadAsyncTask> mTaskList = new ArrayList<DownloadAsyncTask>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main7);
        mProgressBar = (ProgressBar) findViewById(R.id.progressbar);
        mPercentTV = (TextView) findViewById(R.id.percent_tv);
        mSharedPreferences = getSharedPreferences("download", Context.MODE_PRIVATE);
        //开始下载
        findViewById(R.id.begin).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread() {
                    @Override
                    public void run() {
                        //创建存储文件夹
                        File dir = new File(downloadPath);
                        if (!dir.exists()) {
                            dir.mkdir();
                        }

                        try {
                            //获取文件大小
                            URL url = new URL(mUrl);
                            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                            mFileLength = connection.getContentLength();
                        } catch (Exception e) {
                            Log.e(TAG, e.getMessage());
                        }
                        Message.obtain(mHandler, GET_LENGTH_SUCCESS).sendToTarget();
                    }
                }.start();
            }
        });

        //暂停下载
        findViewById(R.id.end).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (DownloadAsyncTask task : mTaskList) {
                    if (task != null && (task.getStatus() == AsyncTask.Status.RUNNING || !task.isCancelled())) {
                        task.cancel(true);
                    }
                }
                mTaskList.clear();
            }
        });
    }

    /**
     * 开始下载
     * 根据待下载文件大小计算每个线程下载位置,并创建AsyncTask
     */
    private void beginDownload() {
        mCurrentLength = 0L;
        mPercentTV.setVisibility(View.VISIBLE);
        mProgressBar.setProgress(0);
        long blockLength = mFileLength / DEFAULT_POOL_SIZE;
        for (int i = 0; i < DEFAULT_POOL_SIZE; i++) {
            long beginPosition = i * blockLength;//每条线程下载的开始位置
            long endPosition = (i + 1) * blockLength;//每条线程下载的结束位置
            if (i == (DEFAULT_POOL_SIZE - 1)) {
                endPosition = mFileLength;//如果整个文件的大小不为线程个数的整数倍,则最后一个线程的结束位置即为文件的总长度
            }
            DownloadAsyncTask task = new DownloadAsyncTask(beginPosition, endPosition);
            mTaskList.add(task);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl, String.valueOf(i));
        }
    }

    /**
     * 更新进度条
     */
    synchronized public void updateProgress() {
        int percent = (int) Math.ceil((float)mCurrentLength / (float)mFileLength * 100);
        // Log.i(TAG, "downloading  " + mCurrentLength + "," + mFileLength + "," + percent);
        if(percent > mProgressBar.getProgress()) {
            mProgressBar.setProgress(percent);
            mPercentTV.setText("下载进度:" + percent + "%");
            if (mProgressBar.getProgress() == mProgressBar.getMax()) {
                Toast.makeText(Main7Activity.this, "下载结束", Toast.LENGTH_SHORT).show();
            }
        }
    }

    @Override
    protected void onDestroy() {
        for(DownloadAsyncTask task: mTaskList) {
            if(task != null && task.getStatus() == AsyncTask.Status.RUNNING) {
                task.cancel(true);
            }
            mTaskList.clear();
        }
        super.onDestroy();
    }

    /**
     * 下载的AsyncTask
     */
    private class DownloadAsyncTask extends AsyncTask<String, Integer , Long> {
        private static final String TAG = "DownloadAsyncTask";
        private long beginPosition = 0;
        private long endPosition = 0;

        private long current = 0;

        private String currentThreadIndex;


        public DownloadAsyncTask(long beginPosition, long endPosition) {
            this.beginPosition = beginPosition;
            this.endPosition = endPosition;
        }

        @Override
        protected Long doInBackground(String... params) {
            Log.i(TAG, "downloading");
            String httpUrl = params[0];
            currentThreadIndex = httpUrl + params[1];
            if(httpUrl == null) {
                return null;
            }
            InputStream is = null;
            RandomAccessFile fos = null;
            OutputStream output = null;

            try {
                URL url = new URL(mUrl);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                //本地文件
                File file = new File(downloadPath + File.separator + httpUrl.substring(httpUrl.lastIndexOf("/") + 1));

                //获取之前下载保存的信息,从之前结束的位置继续下载
                //这里加了判断file.exists(),判断是否被用户删除了,如果文件没有下载完,但是已经被用户删除了,则重新下载
                long downedPosition = mSharedPreferences.getLong(currentThreadIndex, 0);
                if(file.exists() && downedPosition != 0) {
                    beginPosition = beginPosition + downedPosition;
                    current = downedPosition;
                    synchronized (mCurrentLength) {
                        mCurrentLength += downedPosition;
                    }
                }

                //设置下载的数据位置beginPosition字节到endPosition字节
                connection.setRequestProperty("Range", "bytes=" + beginPosition + "-" + endPosition);
                //执行请求获取下载输入流
                is = connection.getInputStream();

                //创建文件输出流
                fos = new RandomAccessFile(file, "rw");
                //从文件的size以后的位置开始写入,其实也不用,直接往后写就可以。有时候多线程下载需要用
                fos.seek(beginPosition);

                byte buffer [] = new byte[1024];
                int inputSize = -1;
                while((inputSize = is.read(buffer)) != -1) {
                    fos.write(buffer, 0, inputSize);
                    current += inputSize;
                    synchronized (mCurrentLength) {
                        mCurrentLength += inputSize;
                    }
                    this.publishProgress();
                    if (isCancelled()) {
                        return null;
                    }
                }
            } catch (MalformedURLException e) {
                Log.e(TAG, e.getMessage());
            } catch (IOException e) {
                Log.e(TAG, e.getMessage());
            } finally{
                try{
                /*if(is != null) {
                    is.close();
                }*/
                if(output != null) {
                    output.close();
                }
                if(fos != null) {
                    fos.close();
                }
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onPreExecute() {
            Log.i(TAG, "download begin ");
            super.onPreExecute();
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            //更新界面进度条
            updateProgress();
        }

        @Override
        protected void onPostExecute(Long aLong) {
            Log.i(TAG, "download success ");
            //下载完成移除记录
            mSharedPreferences.edit().remove(currentThreadIndex).commit();
        }

        @Override
        protected void onCancelled() {
            Log.i(TAG, "download cancelled ");
            //记录已下载大小current
            mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
        }

        @Override
        protected void onCancelled(Long aLong) {
            Log.i(TAG, "download cancelled(Long aLong)");
            super.onCancelled(aLong);
            mSharedPreferences.edit().putLong(currentThreadIndex, current).commit();
        }
    }

    private class InnerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case GET_LENGTH_SUCCESS :
                    beginDownload();
                    break;
            }
            super.handleMessage(msg);
        }
    }
}

以上代码亲测可用,几百M大文件也没问题。

四、遇到的坑

  问题描述:在使用上面代码下载http://ftp.neu.edu.cn/mirrors/eclipse/technology/epp/downloads/release/juno/SR2/eclipse-java-juno-SR2-linux-gtk-x86_64.tar.gz文件的时候,不知道为什么暂停时候执行AsyncTask.cancel(true)来取消下载任务,不执行onCancel()函数,也就没有记录该线程下载的位置。并且再次点击下载的时候,5个Task都只执行了onPreEexcute()方法,压根就不执行doInBackground()方法。而下载其他文件没有这个问题。

  这个问题折腾了我好久,它又没有报任何异常,调试又调试不出来。看AsyncTask的源码、上stackoverflow也没有找到原因。看到这个网站(https://groups.google.com/forum/#!topic/android-developers/B-oBiS7npfQ)时,我还真以为是AsyncTask的一个bug。

  百番周折,问题居然出现在上面代码239行(这里已注释)。不知道为什么,执行这一句的时候,线程就阻塞在那里了,所以doInBackground()方法一直没有结束,onCancel()方法当然也不会执行了。同时,因为使用的是线程池Executor,线程数为5个,点击取消之后5个线程都阻塞了,所以再次点击下载的时候只执行了onPreEexcute()方法,没有空闲的线程去执行doInBackground()方法。真是巨坑无比有木有。。。

  虽然问题解决了,但是为什么有的文件下载执行到is.close()的时候线程会阻塞而有的不会?这还是个谜。如果哪位大神知道是什么原因,还望指点指点!

 

源码下载:https://github.com/liuling07/MultiTaskAndThreadDownload


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值