今天来看看多线程断点续传(数据库保存进度)
效果图
原理图
基本流程
点击下载按钮后启动Service,去获取文件信息,获取文件信息成功后启动下载任务并把文件信息传过去,由于是多线程断点下载,在开始下载前需要根据当前文件的大小以及线程的数量将任务进行分配,并根据每个线程需要下载的信息创建不同的线程实体,同时把线程信息保存到数据库中。利用Range请求头去请求不同部分的数据,如果点击了暂停任务,则将当前任务暂停的标记设置为true,并且保存进度到数据库,下次继续下载的时候先从数据库中获取线程信息,根据当前的信息继续下载。
代码
FileListAdapter
public class FileListAdapter extends BaseAdapter {
private static final String TAG = "FileListAdapter";
private Context mContext;
private List<FileInfo> mListInfo;
public FileListAdapter(Context context,List<FileInfo> infos){
mContext = context;
mListInfo = infos;
}
@Override
public int getCount() {
return mListInfo.size();
}
@Override
public Object getItem(int position) {
return mListInfo.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// //获取对应位置的文件信息
final FileInfo fileInfo = mListInfo.get(position);
ViewHolder holder;
if(convertView == null){
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item,null);
holder = new ViewHolder();
holder.tvFile = (TextView) convertView.findViewById(R.id.textName);
holder.btnStart = (Button) convertView.findViewById(R.id.btn_start);
holder.btnStop = (Button) convertView.findViewById(R.id.btn_stop);
holder.pbProgress = (ProgressBar) convertView.findViewById(R.id.progressBar);
holder.tvFile.setText(fileInfo.fileName);
holder.pbProgress.setMax(100);
holder.btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//启动服务,把文件信息传过去
Intent intent = new Intent(mContext, DownloadService.class);
intent.setAction(DownloadService.ACTION_START);
intent.putExtra("fileinfo",fileInfo);
mContext.startService(intent);
}
});
holder.btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//停止服务
Intent intent = new Intent(mContext, DownloadService.class);
intent.setAction(DownloadService.ACTION_STOP);
intent.putExtra("fileinfo",fileInfo);
mContext.startService(intent);
}
});
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
holder.pbProgress.setProgress(fileInfo.finished);
return convertView;
}
public void updateProgressBar(int id, int progress){
FileInfo fileInfo = mListInfo.get(id);
fileInfo.finished = progress;
notifyDataSetChanged();
}
//为什莫要定义为静态的,因为静态内部类只会被加载一次
static class ViewHolder{
TextView tvFile;
Button btnStart;
Button btnStop;
ProgressBar pbProgress;
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
//url必须带前缀,也就是协议,否则会报错
public static final String url1 = "http://www.imooc.com/mobile/imooc.apk";
// public static final String url2 = "http://www.imooc.com/download/Activator.exe";
// public static final String url3 = "http://www.imooc.com/download/iTunes64Setup.exe";
private List<FileInfo> fileList;
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(DownloadService.ACTION_UPDATE.equals(action)){
int finished = intent.getIntExtra("finished", 0);
int id = intent.getIntExtra("id", 0);
adapter.updateProgressBar(id,finished);//根据id更新进度
}else if(DownloadService.ACTION_FINISHED.equals(action)){
Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo");
int id = fileInfo.id;
adapter.updateProgressBar(id,0);//下载完成将进度条置为0
}
}
};
private ListView listFile;
private FileListAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
//注册广播
IntentFilter filter = new IntentFilter();
filter.addAction(DownloadService.ACTION_UPDATE);
filter.addAction(DownloadService.ACTION_FINISHED);
registerReceiver(receiver, filter);
}
public void initData(){
listFile = (ListView) findViewById(R.id.listFile);
fileList = new ArrayList<>();
FileInfo fileInfo1 = new FileInfo(0,url1,"imooc1.apk",0,0);
FileInfo fileInfo2 = new FileInfo(1,url1,"imooc2.apk",0,0);
FileInfo fileInfo3 = new FileInfo(2,url1,"imooc3.apk",0,0);
fileList.add(fileInfo1);
fileList.add(fileInfo2);
fileList.add(fileInfo3);
adapter = new FileListAdapter(this,fileList);
listFile.setAdapter(adapter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
}
DownloadService
public class DownloadService extends Service {
private static final String TAG = "DownloanService";
public static final String ACTION_START = "ACTION_START";
public static final String ACTION_STOP = "ACTION_STOP";
public static final String ACTION_UPDATE = "ACTION_UPDATE";
public static final String ACTION_FINISHED = "ACTION_FINISHED";
public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/downloads/";
public static final int MSG_INIT_THREAD = 0;
//保存当前任务
private Map<Integer, DownloadTask> map = new LinkedHashMap<>();
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_INIT_THREAD:
FileInfo fileInfo = (FileInfo) msg.obj;
//将文件信息传给下载任务,主要是文件的url,文件大小
DownloadTask task = new DownloadTask(DownloadService.this, fileInfo, 3);
task.download();
map.put(fileInfo.id, task);//将当前任务保存到集合中
break;
}
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getAction() != null) {
if (ACTION_START.equals(intent.getAction())) {
//获取文件信息
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo");
// new InitThread(fileInfo).start();
DownloadTask.sExcutorService.execute(new InitThread(fileInfo));//利用线程池启动任务
} else if (ACTION_STOP.equals(intent.getAction())) {
Toast.makeText(this, "停止", Toast.LENGTH_SHORT).show();
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileinfo");
DownloadTask task = map.get(fileInfo.id);//根据id取出当前下载的任务
if (task != null) {
task.isPause = true;
}
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
class InitThread extends Thread {
private FileInfo fileInfo;
private RandomAccessFile raf;
private HttpURLConnection conn;
public InitThread(FileInfo fileInfo) {
this.fileInfo = fileInfo;
}
@Override
public void run() {
super.run();
try {
URL url = new URL(fileInfo.url);//www.imooc.com/mobile/imooc.apk
conn = (HttpURLConnection) url.openConnection();//打开链接
conn.setConnectTimeout(3000);
conn.setReadTimeout(3000);
int len = -1;
if (conn.getResponseCode() == 200) {
len = conn.getContentLength();//获取服务器文件长度
}
if (len < 0) {
return;
}
File dir = new File(DOWNLOAD_PATH);
if (!dir.exists()) {
dir.mkdir();//目录不存在创建目录
}
File file = new File(dir, fileInfo.fileName);
//在指定路径下创建一个个服务器文件大小一样的文件
raf = new RandomAccessFile(file, "rwd");
raf.setLength(len);//设置临时文件的长度为服务器文件长度
fileInfo.length = len;//设置文件信息
handler.obtainMessage(MSG_INIT_THREAD, fileInfo).sendToTarget();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null)
conn.disconnect();
try {
if (raf != null)
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
DownloadTask
public class DownloadTask {
private static final String TAG = "DownloadTask";
private Context mContext;
//要下载的文件信息,
private FileInfo fileInfo;
private ThreadDao dao;
//是否暂停
public boolean isPause = false;
//完成的进度
private int finished;
//线程数量
private int mThreadCount = 1;
//静态线程池,方便管理线程
public static ExecutorService sExcutorService = Executors.newCachedThreadPool();
private List<DownloadThread> mThreadList;
public DownloadTask(Context mContext, FileInfo fileInfo, int threadCount) {
this.mContext = mContext;
this.fileInfo = fileInfo;
this.mThreadCount = threadCount;
dao = new ThreadDaoImpl(mContext);
}
public void download() {
//每次下载任务前,根据url查询线程信息
List<ThreadInfo> threadInfos = dao.queryThread(fileInfo.url);
ThreadInfo threadinfo;
if (threadInfos.size() == 0) {
//每个线程需要下载的长度
int length = fileInfo.length / mThreadCount;
for (int i = 0; i < mThreadCount; i++) {
//结束位置比开始位置多length,减一是为了下载位置补充和
threadinfo = new ThreadInfo(i, i * length, (i + 1) * length - 1, fileInfo.url, 0);
if (i == mThreadCount - 1) {
threadinfo.end = fileInfo.length;//防止除不尽的情况
}
threadInfos.add(threadinfo);//将线程信息保存到集合中
//第一次数据库中不存在信息,向数据库写入信息
dao.insertThread(threadinfo);
}
}
mThreadList = new ArrayList<>();
for (ThreadInfo info : threadInfos) {
DownloadThread downloadthread = new DownloadThread(info);
// downloadthread.start();
sExcutorService.execute(downloadthread);
mThreadList.add(downloadthread);
}
}
/**
* 判断下载任务里面的所有线程是否执行完毕
*/
public synchronized void checkAllThreadFinished() {
boolean allFinished = true;
for (DownloadThread thread : mThreadList) {
if (!thread.isFinished) {
allFinished = false;
break;
}
}
if (allFinished) {
//下载完成删除删除下载信息
dao.deleteThread(fileInfo.url);
Intent intent = new Intent(DownloadService.ACTION_FINISHED);
intent.putExtra("fileinfo", fileInfo);
Log.d(TAG, "checkAllThreadFinished: "+fileInfo);
mContext.sendBroadcast(intent);
}
}
class DownloadThread extends Thread {
private ThreadInfo mThreadInfo;
private RandomAccessFile raf;
private HttpURLConnection conn;
public boolean isFinished = false;
public DownloadThread(ThreadInfo threadInfo) {
this.mThreadInfo = threadInfo;
}
@Override
public void run() {
super.run();
//设置下载位置
try {
URL url = new URL(mThreadInfo.url);//下载链接
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(5000);
conn.setConnectTimeout(4000);
//当前下载位置等于起始位置加上已经下载的进度
int start = mThreadInfo.start + mThreadInfo.finished;
//下载的范围为起始位置到文件长度,因为是单线程下载
conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.end);
File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.fileName);
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);//指定从某个位置起
Intent intent = new Intent(DownloadService.ACTION_UPDATE);
finished += mThreadInfo.finished;//更新完成的进度,必须要加上数据库中查询到的文件信息
//开始下载
Log.d(TAG, "getResponseCode: "+conn.getResponseCode());
if (conn.getResponseCode() == 206) {
//读取数据
int len = -1;
long time = System.currentTimeMillis();
InputStream stream = conn.getInputStream();
byte[] buffer = new byte[1024 << 2];//每次赌徒多少个字节
while ((len = stream.read(buffer)) != -1) {
//写入文件
raf.write(buffer, 0, len);
finished += len;//累加整个文件完成进度
mThreadInfo.finished += len;
if(System.currentTimeMillis() - time >1500) {
time = System.currentTimeMillis();
//通知Activity更新进度条
intent.putExtra("finished", finished * 100 / fileInfo.length);
intent.putExtra("id", fileInfo.id);
mContext.sendBroadcast(intent);
}
//下载暂停,保存进度到数据库
if (isPause) {
//将当前的信息保存到数据库
dao.updateThread(mThreadInfo.url, mThreadInfo.id + "", mThreadInfo.finished);
return;
}
}
//表示下载完成
isFinished = true;
Log.d(TAG, "run: 完成");
checkAllThreadFinished();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
{
conn.disconnect();
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
其他相关的类和之前的一篇单线程断点续传中的相同,这里就不在给出了。
最后在给出一篇多线程断点续传的文章
多线程多任务断点续传