先看一组效果图:
工程目录结构如下:
比较简单,一个MainActivity,一个下载工具类,2个布局文件
activity_main.xml:
<span style="font-family:Courier New;font-size:14px;"><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:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:textSize="18sp"
android:text="请输入下载文件的路径"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!--输入url路径-->
<EditText
android:id="@+id/edt_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="http://" />
<!--输入下载的线程数量-->
<TextView
android:textSize="18sp"
android:text="请设置线程的数量"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/edt_count"
android:inputType="number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="3" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="下载"
android:id="@+id/btn_download" />
<!--动态添加线程下载进度条的父控件-->
<LinearLayout
android:orientation="vertical"
android:id="@+id/ll_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
</LinearLayout>
</span>
进度条布局pb_layout.xml:
<span style="font-family:Courier New;font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ProgressBar>
</span>
MultiThreadDownloadUtil.java工具类:
<span style="font-family:Courier New;font-size:14px;">package com.example.chenys.mutilethreaddownload;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
/**
* 多线程断点下载工具类
*
* @author Chenys
*/
public class MultiThreadDownloadUtil {
/**
* 线程的数量,默认为开启3个线程
*/
private static int threadCount = 3;
/**
* 每个下载区块的大小
*/
private static long blocksize;
/**
* 当前正在工作的线程个数
*/
private static int runningThreadCount;
/**
* 线程下载缓冲区,默认为10M
*/
private static int defaultBuff = 1024 * 1024 * 10;
/**
* 文件保存的文件夹
*/
private static String saveFolder = "download";
private static Context mContext;
/**
* 断点下载文件的方法
*
* @param path
*/
public static void downloadFile(Context context, String path, DownloadResponseHandler downloadResponseHandler) {
HttpURLConnection conn = null;
try {
mContext = context;
if (!path.startsWith("http://")) {
path = "http://" + path;
}
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(3000);
conn.setReadTimeout(1000);
int code = conn.getResponseCode();
if (code == 200) {
// 得到服务器端返回的文件大小,单位byte,字节
long size = conn.getContentLength();
Log.d("chenys", "服务器文件的大小:" + size);
// 计算每个blocksize的大小和记录当前线程的总数
blocksize = size / threadCount;
runningThreadCount = threadCount;
/**
* 1.首先在本地创建一个文件大小跟服务器一模一样的空白文件,RandomAccessFile类的实例支持对随机访问文件的读取和写入
* 参数1:目标文件 参数2:打开该文件的访问模式,"r" 以只读方式打开 ,"rw" 打开以便读取和写入
*/
File file = new File(getFileSavePath(), getFileName(path));
RandomAccessFile raf = new RandomAccessFile(file, "rw");
raf.setLength(size);// 设置文件的大小
// 2.开启若干个子线程,分别下载对应的资源
for (int i = 1; i <= threadCount; i++) {
long startIndex = (i - 1) * blocksize; // 由于服务端下载文件是从0开始的
long endIndex = i * blocksize - 1;
if (i == threadCount) {
// 最后一个线程
endIndex = size - 1;
}
Log.d("chenys", "开启线程:" + i + "下载的位置" + startIndex + "~" + endIndex);
//开启线程
new DownloadThread(path, i, startIndex, endIndex, downloadResponseHandler).start();
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败,请确认你的URL下载地址是否可用");
} catch (ProtocolException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败");
} catch (FileNotFoundException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败,请检查你的sd卡是否存在");
} catch (IOException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败,请检查你的网络是否存在,或者sd卡空间不足");
} finally {
if (null != conn) {
conn.disconnect();
conn = null;
System.gc();
}
}
}
/**
* 自定义下载线程
*
* @author Chenys
*/
private static class DownloadThread extends Thread {
private int threadId; // 线程id
private long startIndex; // 开始下载的位置
private long endIndex; // 结束下载的位置
private String path;
private DownloadResponseHandler downloadResponseHandler;
public DownloadThread(String path, int threadId, long startIndex, long endIndex, DownloadResponseHandler downloadResponseHandler) {
this.path = path;
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.downloadResponseHandler = downloadResponseHandler;
}
/**
* 执行下载任务
*/
public void run() {
HttpURLConnection conn = null;
//计算每个线程的下载总长度
long totalSize = endIndex - startIndex;
try {
//创建记录资源当前下载到什么位置的临时文件
File tempFile = new File(getFileSavePath(), threadId + ".temp");
//记录当前线程下载的大小
int currentSize = 0;
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
//接着从上一次断点的位置继续下载数据
if (tempFile.exists() && tempFile.length() > 0) {
FileInputStream lastDownload = new FileInputStream(tempFile);
BufferedReader br = new BufferedReader(new InputStreamReader(lastDownload));
//获取当前线程上次下载的总大小是多少
String lastSizeStr = br.readLine();
int lastSize = Integer.parseInt(lastSizeStr);
Log.d("chenys", "上次线程" + threadId + "下载的总大小:" + lastSize);
//更新startIndex的位置和每条线程下载的当前大小数
startIndex += lastSize;
currentSize += lastSize;
lastDownload.close();
}
//设置http协议请求头: 指定每条线程从文件的什么位置开始下载,下载到什么位置为止
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
Log.d("chenys", "服务器返回码code=" + code);// 如果是下载一部分资源,那么返回码是206
//获取服务器返回的流
InputStream is = conn.getInputStream();
File file = new File(getFileSavePath(), getFileName(path));
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//指定文件的下载起始位置
raf.seek(startIndex);
Log.d("chenys", "第" + threadId + "个线程写文件的开始位置" + String.valueOf(startIndex));
//开始下载文件
int len = 0;
byte[] buff = new byte[defaultBuff]; //10M的缓冲数组,提高下载速度
while ((len = is.read(buff)) != -1) {
RandomAccessFile rf = new RandomAccessFile(tempFile, "rwd");//注意:这里采用的模式是"rwd",即无缓存模式的读写
//写数据到目标文件
raf.write(buff, 0, len);
//记录当前下载记录的位置到文件中
currentSize += len;
rf.write(String.valueOf(currentSize).getBytes());
rf.close();
//回调显示下载的进度
downloadResponseHandler.onProgress(threadId - 1, totalSize, currentSize);
}
is.close();
raf.close();
} catch (MalformedURLException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败,请确认你的URL下载地址是否可用");
} catch (ProtocolException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败");
} catch (IOException e) {
e.printStackTrace();
downloadResponseHandler.onFailed("下载失败,请检查你的网络是否可用");
} finally {
//以下是处理清理缓存的任务
synchronized (MultiThreadDownloadUtil.class) {
Log.d("chenys", "线程" + threadId + "下载完毕了");
runningThreadCount--;
if (runningThreadCount < 1) {
Log.d("chenys", "所有的线程都工作完毕了,删除临时文件");
for (int i = 1; i <= threadCount; i++) {
File tempFile = new File(getFileSavePath(), i + ".temp");
Log.d("chenys", "删除临时文件成功与否" + tempFile.delete());
downloadResponseHandler.onSuccess("下载完成");
//下载完毕后,恢复线程数量为允许设置的状态
SharedPreferences sp = mContext.getSharedPreferences("threadCount_sp_name", Context.MODE_PRIVATE);
sp.edit().putBoolean("is_count_setted", false).commit();
}
if (null != conn) {
conn.disconnect();
conn = null;
System.gc();
}
}
}
}
}
}
/**
* 设置总线程个数的方法,如果第一次已经设置过了,则不能则设置,只有等当前的任务下载完毕后才能设置
*
* @param threadCount
*/
public static void setThreadCount(Context context,int threadCount) {
SharedPreferences sp = context.getSharedPreferences("threadCount_sp_name", Context.MODE_PRIVATE);
boolean setted = sp.getBoolean("isCountSetted", false);
Log.d("chenys", "是否已经设置过下载线程数" + setted);
if (threadCount > 0 && !setted) {
MultiThreadDownloadUtil.threadCount = threadCount;
SharedPreferences.Editor edit = sp.edit();
edit.putInt("thread_count", threadCount);
edit.putBoolean("is_count_setted", true).commit();
} else {
int lastCountSet = sp.getInt("thread_count", 3);
Toast.makeText(context, "你上次设置的下载线程数是:" + lastCountSet + "当前任务现在完毕后可重新设置线程数", Toast.LENGTH_LONG).show();
MultiThreadDownloadUtil.threadCount = lastCountSet;
}
}
/**
* 默认的线程个数
* @return
*/
public static int getDefaultThreadCount() {
return 3;
}
/**
* 设置保存下载文件的文件夹
*
* @param folderName
*/
public static void setDownloadDir(String folderName) {
if (!TextUtils.isEmpty(folderName)) {
MultiThreadDownloadUtil.saveFolder = folderName;
}
}
/**
* 设置缓存区大小
*/
public static void setCacheSize(int defaultBuff) {
if (defaultBuff < 0) {
MultiThreadDownloadUtil.defaultBuff = defaultBuff;
}
}
/**
* 获取文件的保存路径
*
* @return
*/
private static String getFileSavePath() {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = Environment.getExternalStorageDirectory() + File.separator +
mContext.getPackageName() + File.separator + saveFolder;
File fileDir = new File(path);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
return path;
}
return null;
}
/**
* 获取文件名
*/
private static String getFileName(String path) {
int index = path.lastIndexOf("/");
String fileName = path.substring(index + 1);
return fileName;
}
/**
* 下载成功或失败的回调接口
*/
interface DownloadResponseHandler {
//下载成功时回调
void onSuccess(String resultCode);
//下载失败时回调
void onFailed(String result);
//下载过程中回调,用于显示下载的进度
void onProgress(int threadId, long totalBytes, long currentBytes);
}
}
</span>
MainActivity.java:
<span style="font-family:Courier New;font-size:14px;">package com.example.chenys.mutilethreaddownload;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
/**
* 多线程断点下载
*/
public class MainActivity extends Activity {
private EditText mUrlEdt; //url地址输入框
private EditText mCountEdt; //线程个数输入框
private Button mDowloadBtn; //下载按钮
private LinearLayout mLlContainer; //存放进度条的父控件
private List<ProgressBar> mPbList = new ArrayList<>();//存放进度条的集合
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUrlEdt = (EditText) findViewById(R.id.edt_url);
mCountEdt = (EditText) findViewById(R.id.edt_count);
mDowloadBtn = (Button) findViewById(R.id.btn_download);
mLlContainer = (LinearLayout) findViewById(R.id.ll_container);
mDowloadBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mDowloadBtn.setEnabled(false);//禁用下载按钮
downLoad();
}
});
}
/**
* 消息处理Handler
*/
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Toast.makeText(MainActivity.this, (String) msg.obj, Toast.LENGTH_LONG).show();
mDowloadBtn.setEnabled(true);//恢复下载按钮
super.handleMessage(msg);
}
};
/**
* 下载按钮的点击后的处理逻辑
*/
public void downLoad() {
final String path = mUrlEdt.getText().toString().trim();
final String count = mCountEdt.getText().toString().trim();
if (TextUtils.isEmpty(path)) {
Toast.makeText(this, "对比起,下载路径不能为空", Toast.LENGTH_LONG).show();
return;
}
if (!TextUtils.isEmpty(count)) {
//设置下载的线程个数
int threadCount = Integer.parseInt(count);
Log.d("chenys", "线程个数" + threadCount);
MultiThreadDownloadUtil.setThreadCount(MainActivity.this,threadCount);
//清空旧的的进度条
mLlContainer.removeAllViews();
//根据count的个数添加进度条个数
for (int i = 1; i <= threadCount; i++) {
ProgressBar pb = (ProgressBar) View.inflate(MainActivity.this, R.layout.layout_pb, null);
TextView textView = new TextView(MainActivity.this);
textView.setText("线程" + i + "下载进度:");
mLlContainer.addView(textView);
mLlContainer.addView(pb);
mPbList.add(pb);//将进度条添加到集合中
}
} else {
Toast.makeText(MainActivity.this, "当前未选则线程个数,将开启默认线程进行下载", Toast.LENGTH_LONG).show();
for (int i = 1; i <= MultiThreadDownloadUtil.getDefaultThreadCount(); i++) {
ProgressBar pb = (ProgressBar) View.inflate(MainActivity.this, R.layout.layout_pb, null);
mLlContainer.addView(pb);
mPbList.add(pb);//将进度条添加到集合中
}
}
Toast.makeText(MainActivity.this, "开始下载了", Toast.LENGTH_SHORT).show();
//执行下载任务
new Thread(new Runnable() {
@Override
public void run() {
MultiThreadDownloadUtil.downloadFile(MainActivity.this, path, new MultiThreadDownloadUtil.DownloadResponseHandler() {
@Override
public void onSuccess(String result) {
Log.d("chenys", result);
Message msg = Message.obtain();
msg.obj = result;
handler.sendMessage(msg);
}
@Override
public void onFailed(String result) {
Log.d("chenys", result);
Message msg = Message.obtain();
msg.obj = result;
handler.sendMessage(msg);
}
@Override
public void onProgress(int threadId, long totalBytes, long currentBytes) {
mPbList.get(threadId).setMax((int) totalBytes);
mPbList.get(threadId).setProgress((int) currentBytes);
Log.d("chenys", "threadId=" + threadId + " totalBytes=" + totalBytes + " currentBytes=" + currentBytes);
}
});
}
}).start();
}
@Override
protected void onDestroy() {
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
</span>
下载完毕后的结果:
后台打印的log:
<span style="font-family:Courier New;font-size:14px;">05-16 14:16:58.506 3537-3537/? D/chenys﹕ 线程个数3
05-16 14:16:58.516 3537-3537/? D/chenys﹕ 是否已经设置过下载线程数false
05-16 14:16:58.876 3537-4028/? D/chenys﹕ 服务器文件的大小:23250432
05-16 14:16:59.846 3537-4028/? D/chenys﹕ 开启线程:1下载的位置0~7750143
05-16 14:16:59.856 3537-4028/? D/chenys﹕ 开启线程:2下载的位置7750144~15500287
05-16 14:16:59.876 3537-4028/? D/chenys﹕ 开启线程:3下载的位置15500288~23250431
05-16 14:17:00.196 3537-4035/? D/chenys﹕ 服务器返回码code=206
05-16 14:17:00.216 3537-4036/? D/chenys﹕ 服务器返回码code=206
05-16 14:17:00.246 3537-4036/? D/chenys﹕ 第2个线程写文件的开始位置7750144
05-16 14:17:00.246 3537-4035/? D/chenys﹕ 第1个线程写文件的开始位置0
05-16 14:17:00.336 3537-4039/? D/chenys﹕ 服务器返回码code=206
05-16 14:17:00.386 3537-4039/? D/chenys﹕ 第3个线程写文件的开始位置15500288
05-16 14:17:01.427 3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=78545
05-16 14:17:01.446 3537-4039/? D/chenys﹕ threadId=2 totalBytes=7750143 currentBytes=78537
05-16 14:17:01.476 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=78538
05-16 14:17:01.546 3537-4039/? D/chenys﹕ threadId=2 totalBytes=7750143 currentBytes=125257
05-16 14:17:01.597 3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=125265
05-16 14:17:01.597 3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=160305
05-16 14:17:01.606 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=134018
05-16 14:17:01.626 3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=169065
05-16 14:17:01.626 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=174898
..........
05-16 14:17:01.657 3537-4035/? D/chenys﹕ threadId=0 totalBytes=7750143 currentBytes=186585
05-16 14:17:01.676 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=192418
05-16 14:17:01.718 3537-4039/? D/chenys﹕ threadId=2 totalBytes=7750143 currentBytes=157377
05-16 14:17:01.757 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=209938
05-16 14:17:26.087 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=7734778
05-16 14:17:26.097 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=7749378
05-16 14:17:26.117 3537-4036/? D/chenys﹕ threadId=1 totalBytes=7750143 currentBytes=7750144
05-16 14:17:26.117 3537-4036/? D/chenys﹕ 线程2下载完毕了
05-16 14:17:26.127 3537-4036/? D/chenys﹕ 所有的线程都工作完毕了,删除临时文件
05-16 14:17:26.177 3537-4036/? D/chenys﹕ 删除临时文件成功与否true
05-16 14:17:26.187 3537-4036/? D/chenys﹕ 下载完成
05-16 14:17:26.326 3537-4036/? D/chenys﹕ 删除临时文件成功与否true
05-16 14:17:26.326 3537-4036/? D/chenys﹕ 下载完成
05-16 14:17:26.346 3537-4036/? D/chenys﹕ 删除临时文件成功与否true
05-16 14:17:26.397 3537-4036/? D/chenys﹕ 下载完成</span>