最近因为一些原因,需要用到多文件多线程断点下载文件,所以四处查找资料然后做了一个Demo. 本项目主要参考的是宝塔上的猫-《Android实战:多线程多文件断点续传下载+通知栏控制》
本项目GitHub地址:https://github.com/JonyZeng/JonyDownload
对于多线程下载文件,我们应该首先需要了解单线程下载文件的原理,多线程下载就是把文件分为几份,每一份由一个线程去下载。然后将每一个线程单独下载的文件保存在一起就实现了多线程下载。
断点下载相对于理解比较轻松,每一次暂停线程的时候,将当前下载的进度保存,下次继续从保存的进度进行下载。
而多文件多线程下载的原理是基于多线程单文件下载的基础上,首先确定多文件同时下载需要多少个线程,然后再确定每一个文件多线程同时下载的线程数。
本项目是采用MVP模式进行架构的,因为本人对MVP也是刚了解,所以选择它来进行架构。
- 首先在布局文件中进行主界面activity_main的布局。
然后设置listView条目的布局item.xml
2.布局完成之后,按照国际惯例,我们需要给ListView设置适配器,并优化。由于我们listView是需要显示下载文件的信息。所以我们需要创建一个参数类型是FileBean的list。 这里简单的贴一些FileBean(需要序列化)的参数代码
public class FileBean implements Serializable {
public String fileName;
public Integer fileSize;
public Integer filePause;//下载暂停位置
public Integer DownSize; //finished
public Integer id;
public String Url;
/**
*
* @param fileName 文件名
* @param fileSize 文件大小
* @param downSize 文件下载
* @param id 文件ID
* @param url 文件下载地址
*/
public FileBean(Integer id,String fileName, Integer fileSize, Integer downSize, String url) {
this.fileName = fileName;
this.fileSize = fileSize;
DownSize = downSize;
this.id = id;
Url = url;
}
之后就到了最精彩的地方了。对getView的优化和对控件的赋值。
/**
* 进行数据和View的适配
*
* @param i
* @param view
* @param viewGroup
* @return
*/
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
final FileBean fileBean=list.get(i);
if (view==null){
view=inflater.inflate(R.layout.item,null);
viewHolder = new MyViewHolder();
viewHolder.mTvfileName=(TextView)view.findViewById(R.id.Tv_fileName);
viewHolder.mBarDown=(ProgressBar)view.findViewById(R.id.Pb_down);
viewHolder.mBtnstart=(Button)view.findViewById(R.id.Btn_start);
viewHolder.mBtnstop=(Button) view.findViewById(R.id.Btn_stop);
viewHolder.mTvfileName.setText(list.get(i).getFileName());
viewHolder.mBarDown.setProgress(fileBean.getDownSize());
viewHolder.mBtnstart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//// TODO: 2017/8/23 开始下载
presenter = new DownloadPresenter();
presenter.startDownload(fileBean,context);
Log.d(TAG, "onClick:开始");
Toast.makeText(context, "点击了开始", Toast.LENGTH_SHORT).show();
}
});
viewHolder.mBtnstop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//// TODO: 2017/8/23 暂停下载
presenter.stopDownload(fileBean,context);
Log.d(TAG, "onClick:暂停");
Toast.makeText(context, "点击了暂停", Toast.LENGTH_SHORT).show();
}
});
view.setTag(viewHolder);
}else {
viewHolder= (MyViewHolder) view.getTag();
}
viewHolder.mBarDown.setProgress(fileBean.getDownSize());
return view;
}
根据ListView的优化需求,需要创建一个静态的ViewHolder
/**
* 静态的View ,避免重复加载
*/
static class MyViewHolder {
TextView mTvfileName;
ProgressBar mBarDown;
Button mBtnstart;
Button mBtnstop;
}
两个按钮的点击事件的逻辑代码是通过MVP的实现的。所以需要创建一个Contrast类设置接口,在model层实现代码的逻辑实现。
。
最终在model层里面开启服务。通过intent传递值过去
private static final String TAG = DownloadModel.class.getSimpleName();
private Intent intent;
@Override
public void startDownload(FileBean fileBean, Context context) {
//开始下载
intent = new Intent(context,DownloadService.class);
intent.setAction(Config.ACTION_START);
intent.putExtra("fileBean",fileBean);
Log.d(TAG, "startDownload: 开启下载的服务");
context.startService(intent);
}
@Override
public void stopDownload(FileBean fileBean, Context context) {
//暂停下载
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(Config.ACTION_STOP);
intent.putExtra("fileBean", fileBean);
Log.d(TAG, "stopDownload: 停止下载服务");
context.startService(intent);
}
- 多线程断点下载服务。创建一个类继承Service,在AndroidMainfest.xml中声明。重写onStartCommand方法,在里面根据不同的intent值进行不同的操作。在开始下载的时候,需要通过线程池开启一个线程,所以我们需要自定义一个类继承Thread,在里面实现下载的代码。
自定义线程类所需要的参数
private URL url;
private int responseCode;
private int length = 0; //判断长度
private RandomAccessFile randomAccessFile;
private FileBean fileBean = null;
在重写的run方法中,进行对当前点击下载文件的进行获取和设置下载文件的路径。
url = new URL(fileBean.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(2000);
connection.setRequestMethod("GET");
Log.i(TAG, "run:获取网络请求");
responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
length = connection.getContentLength();
}else{
Toast.makeText(DownloadService.this, "请检查网络情况", Toast.LENGTH_SHORT).show();
}
if (length <= 0) {//说明下载文件不存在
return;
}
//文件存在,开始下载
//判断文件路径是否存在
File dir = new File(Config.DownloadPath);
if (!dir.exists()) {
dir.mkdir();
}
通过RandomAccessFile在内存中根据当前文件大小进行占位。
File file = new File(dir, fileBean.getFileName());
randomAccessFile = new RandomAccessFile(file, "rwd");//随机访问,随时读写
randomAccessFile.setLength(length);
fileBean.setDownSize(length); //设置文件的大小
将通过handler将fileBean传递。
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case Config.MSG_INIT://开始下载
//调用DownloadTask的download下载
fileBean= (FileBean) msg.obj;
DownloadTask task = new DownloadTask(DownloadService.this, fileBean, 3);
task.download();
taskMap.put(fileBean.getId(),task);
Intent intent = new Intent(Config.ACTION_START);
intent.putExtra("fileBean",fileBean);
sendBroadcast(intent);
}
}
};
创建一个下载任务类DownloadTask,在里面执行下载任务。
DownloadTask所需要的一些参数
private static final String TAG = DownloadTask.class.getSimpleName();
public static ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //创建一个线程池,每次开启线程通过线程池调用。
public List urlBeanList; //一个UrlBean的list集合。
private FileBean fileBean; //下载文件的信息类
public DBUtil dbUtil; //数据库操作工具类
private ThreadUtil threadUtil; //下载线程的信息类
public int threadCount=1;
public Context context;
private List downloadLists=null;
private UrlBean urlBean;
public boolean isonPause=false;//判断是否暂停
创建一个数据库线程下载帮助类,里面实现线程对数据库的增删查改。查询数据库线程的方法
/**
* 查询数据库线程
* @param url
* @return
*/
public synchronized ListqueryThread(String url){
Log.i(TAG, "queryThread: url: " + url);
readableDatabase=dbUtil.getReadableDatabase();
List list=new ArrayList<>();
Cursor cursor=readableDatabase.query("download_info", null, "url = ?", new String[] { url }, null, null, null);
while (cursor.moveToNext()){
UrlBean urlBean= new UrlBean();
urlBean.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
urlBean.setUrl(cursor.getString(cursor.getColumnIndex("url")));
urlBean.setStart(cursor.getInt(cursor.getColumnIndex("start")));
urlBean.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
urlBean.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
list.add(urlBean);
}
cursor.close();
readableDatabase.close();
//readableDatabase.query("urlBean",)
return list;
}
创建一个download方法,方便其他地方调用。在这个方法里面实现查询文件的大小和开启多个线程,分块下载。
public void download(){
Log.i(TAG, "download:"+fileBean.getUrl());
urlBeanList =threadUtil.queryThread(fileBean.getUrl());
//获取到数据库里面查询的list
if (urlBeanList.size()==0){ //数据库中不存在,第一次下载
//获取文件大小
int length=fileBean.getDownSize();
//获取需要分的模块
int block=length/threadCount;
for (int i = 0; i < threadCount; i++) {
//确定开始下载的位置
int star=block*i;
//确定结束下载的位置
int end=(i+1)*block-1;
if (i==threadCount-1){ // //最后一个线程下载结束的位置。
end=length-1;
}
//开启线程
urlBean = new UrlBean(fileBean.getUrl(),i,star,end,0);
urlBeanList.add(urlBean);
}
}
Log.d(TAG, "download:");
//下载文件线程的内部类
downloadLists = new ArrayList<>();
for (UrlBean urlBean:urlBeanList) {
DownloadList dowmload=new DownloadList(urlBean);
DownloadTask.cachedThreadPool.execute(dowmload);
downloadLists.add(dowmload);
threadUtil.insertThread(urlBean);
}
}
创建一个内部类,用于将下载下来的文件,写入到内存中去。
部分参数
private HttpURLConnection urlConnection=null;
private RandomAccessFile accessFile=null;
private InputStream inputStream=null;
private File file;
private Integer finished=0;
private Intent intent;
private UrlBean urlBean;
private boolean isFinished=false;
重写run 方法,在里面请求网络,并且将请求下来的数据通过RandomAccessFile写入到内存中去。
@Override
public void run() {//执行下载耗时操作
//获取http对象
try {
URL url= new URL(fileBean.getUrl());
try {
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setConnectTimeout(2000);
urlConnection.setRequestMethod("GET");
//设置下载的头部
int start=urlBean.getStart()+urlBean.getFinished();
//设置下载结束的位置
urlConnection.setRequestProperty("Range", "bytes=" + start + "-" + urlBean.getEnd());
//新建文件对象
file = new File(Config.DownloadPath, fileBean.getFileName());
//随机访问读写对象
accessFile = new RandomAccessFile(file, "rwd");
accessFile.seek(start);
//刷新当前以及下载的大小
finished +=urlBean.getFinished();
intent = new Intent();
intent.setAction(Config.ACTION_UPDATE);
int respCode=urlConnection.getResponseCode();
if (respCode==HttpURLConnection.HTTP_PARTIAL){ //请求成功
//获取输入流对象
inputStream = urlConnection.getInputStream();
//设置一个byte数组,中转数据
byte[] bytes = new byte[1024];
int length=-1;
//定义UI刷新时间
long time=System.currentTimeMillis();
while ((length=inputStream.read(bytes))!=-1){
accessFile.write(bytes,0,length);
//实时获取下载进度,刷新UI
finished+=length;
urlBean.setFinished(urlBean.getFinished()+length);
if (System.currentTimeMillis()-time>500){
time=System.currentTimeMillis();
intent.putExtra("finished",finished);
Log.d(TAG, "finished:"+finished);
intent.putExtra("id",fileBean.getId());
Log.d(TAG, "finished"+fileBean.getId());
context.sendBroadcast(intent);
}
if (isonPause){
threadUtil.updateThread(urlBean);
return;
}
}
}
//当前线程是否下载完成
isFinished = true;
//判断所有线程是否下载完成
checkAllFinished();
判断所有的线是否已经下载完成。
private synchronized void checkAllFinished() {
boolean allFinished=true;
for (DownloadList down:downloadLists) {
if (!down.isFinished)
allFinished=false;
break;
}
if (allFinished==true){
Log.d(TAG, "checkAllFinished: 下载完成");
threadUtil.delThread(fileBean.getUrl());
intent=new Intent(Config.ACTION_FINISHED);
intent.putExtra("urlBean",urlBean);
context.sendBroadcast(intent);
}
}
4.好了,终于到最后MainActivity里面的一些逻辑实现了。
private static final String TAG = MainActivity.class.getSimpleName();
ListView listView;
private FileAdapter mAdapter;
private UrlBean urlBean;
private List list;
private UIRecive mRecive;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
/**
* 初始化
*/
private void init() {
initData();
initView();
mAdapter = new FileAdapter(this, list);
listView.setAdapter(mAdapter);
mRecive=new UIRecive();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Config.ACTION_UPDATE);
intentFilter.addAction(Config.ACTION_FINISHED);
intentFilter.addAction(Config.ACTION_START);
registerReceiver(mRecive, intentFilter);
}
/**
* 初始化控件
*/
private void initView() {
listView = (ListView) findViewById(R.id.LV_down);
}
/**
* 初始化数据
*/
private void initData() {
//文件类集合
list = new ArrayList<>();
list.add(new FileBean(0,getfileName(Config.URLONE),0,0,Config.URLONE)); //(文件ID,文件名,文件大小,已经下载大小,URL)
list.add(new FileBean(1,getfileName(Config.URLTWO),0,0,Config.URLTWO)); //(文件ID,文件名,文件大小,已经下载大小,URL)
list.add(new FileBean(2,getfileName(Config.URLTHREE),0,0,Config.URLTHREE)); //(文件ID,文件名,文件大小,已经下载大小,URL)
list.add(new FileBean(3,getfileName(Config.URLFOUR),0,0,Config.URLFOUR)); //(文件ID,文件名,文件大小,已经下载大小,URL)
}
@Override
public String getfileName(String url) {
return url.substring(url.lastIndexOf("/") + 1);
}
@Override
protected void onDestroy() {
unregisterReceiver(mRecive);
super.onDestroy();
}
/**
* 刷新Ui
*/
private class UIRecive extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) { //接收到传递过来的数据
if (Config.ACTION_UPDATE.equals(intent.getAction())) {
// 更新进度条的时候
int finished = intent.getIntExtra("finished", 0);
Log.d(TAG, "onReceive:finsihed"+finished);
int id = intent.getIntExtra("id", 0);
mAdapter.updataProgress(id, finished);
} else if (Config.ACTION_FINISHED.equals(intent.getAction())){
// 下载结束的时候
FileBean fileBean = (FileBean) intent.getSerializableExtra("fileBean");
mAdapter.updataProgress(fileBean.getId(), 0);
Toast.makeText(MainActivity.this, list.get(fileBean.getId()).getFileName() + "下载完毕", Toast.LENGTH_SHORT).show();
}
}
}