多线程下载器(不含数据库部分)
Posted DennisJu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程下载器(不含数据库部分)相关的知识,希望对你有一定的参考价值。
多线程下载器(不含数据库部分)
1、写在前面:
虽然demo中程序框架已搭建完成,但是由于笔者时间原因,暂时只完成了核心部分:多线程下载的部分,其他数据库、服务通知、暂停部分还未添加到项目中。
2、相关知识点:
(1)Java线程及停止线程的方式
(2)Java RandomAccessFile文件操作
(3)HttpURLConnection相关range字段的配置
(4)Sqlite同步操作
2、核心思想:
(1)通过HttpURLConnection判断服务器是否支持断电续传:
<1>否->直接开启普通的多线程下载(遇到断网等情况便会重新下载)
<2>是->开启普通的多线程下载,但是每个线程都含有自己的下载进度信息,以便断网或用户暂停开始重新下载重新开启下载。笔者在针对不同的下载尺寸智能的分配不同的线程数量去下载资源,通过设置缓冲区大小来提高下载速度。
3、核心技术:
(1)HttpURLConnection的配置
(2)RandomAccessFile随机文件的读取以及缓冲区的设置
(3)线程的暂停与启动
4、分析结果:
(1)将功能划分为三大部分:下载器(统一的外部接口)、存储器(内部的存储实现)、通知服务(用户交互部分)。
(2)项目结构:
5、核心代码:
package com.jx.downloader;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import android.content.Context;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import com.jx.dbhelper.DownloadRecordDB;
import com.jx.model.DownloadModel;
/**
* 自定义下载器: 1、根据即将下载的内容大小智能的分配下载线程数量,每个线程通过Downloader携带自身线程的下载信息(下载起点、终点、线程名称)
* 2、如果服务器支持断电续传则开启数据库
* ,在下载处于暂停的状态时(导致下载暂停的原因可能是手动暂停或者网络不佳),自动保存下载信息到数据库,在取消下载的时候自动清空数据库信息,
* 重新开始下载的时候,读取内容重新下载(一般情况本地存储变量还未被回收,不必从数据库重新读取) 2、使用RandomAccessFile存储下载的内容
*
* @author J_X 2016年3月19日09:58:00
*/
public class JX_Downloader {
/**
* 0~3M的下载范围默认开启1个线程
*/
private final static long BELOW_Three_M_SIZE = 5 * 1024 * 1024;
/**
* 3~6M的下载范围默认开启2个线程
*/
private final static long BELOW_SIX_M_SIZE = 6 * 1024 * 1024;
/**
* 10~18M的下载范围默认开启4个线程
*/
private final static long BELOW_EIGHTEEN_M_SIZE = 18 * 1024 * 1024;
/**
* 线程是否暂停的标示
*/
private static volatile boolean isStopDownloading = false;
/**
* 下载器的标示id,以为可以创建多个下载器
*/
private long downloaderID;
/**
* 需要下载的资源链接
*/
private String resourceUrl;
/**
* 下载链接的遵守URL协议的对象
*/
private URL resourceURL;
/**
* 需要下载的资源的总字节数
*/
private long resourceSize;
/**
* 需要的下载线程的总数量
*/
private int totalTreadNum;
/**
* 资源所在服务器是否支持断点续传功能
*/
private boolean isSupportLoadingAndSaving;
/**
* 下载资源存储的数据库
*/
private DownloadRecordDB downlaodDB;
/**
* 文件存储目录
*/
private String savaFileName;
/**
* 构造器暂无子类所以没有提供默认构造函数
*
* @param context
* 上下文
* @param downloadUrl
* 将要被下载的链接
* @param saveFileName
* 带后缀的下载的文件存储名称
*/
public JX_Downloader(Context context, String downloadUrl,
String saveFileName) throws MalformedURLException {
// TODO Auto-generated constructor stub
resourceUrl = downloadUrl;
this.resourceURL = new URL(resourceUrl);
this.downlaodDB = new DownloadRecordDB(context);
// 如果用户没有设置下载后缀,提供默认存储文件夹
if (TextUtils.isEmpty(saveFileName)) {
this.savaFileName = "JX_DownLoader.txt";
} else {
this.savaFileName = saveFileName;
}
}
/**
* 开启下载任务,对外提供方便下载方法
*/
public void startDownload() {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
readyDownload();
} catch (IOException e) {
// TODO Auto-generated catch block
Log.e("Debug", "downloading fail!!!");
}
}
}).start();
}
/**
* 核心下载程序(采用类似门面模式方法处理,方法有严格的执行顺序要求)
*
* @throws IOException
*
*/
private void readyDownload() throws IOException {
// 判断当前连接所在服务器是否支持断点续传下载
// this.isSupportLoadingAndSaving = false;
// /** 不支持的给出友好提示,只进行多线程下载的任务 */
// if (!isSupportLoadingAndSaving) {
// Log.e("Debug", "Server don't support pause_save download!");
// } else {
// /** 支持的情况:1、开启数据库存储各个线程进度 */
// }
/* 多线程下载流程 */
// 1、计算需要下载的资源大小
HttpURLConnection httpURLConnection = settingRequestHttp(null);
if (httpURLConnection.getResponseCode() == 200) {
// TODO: 需要进一步优化网络
Log.e("Debug", "Coonected sucessfully");
}
resourceSize = httpURLConnection.getContentLength();
if (resourceSize <= 0) {
Log.e("Debug", "unkown file Length and return");
return;
} else {
Log.e("Debug", "file length: " + resourceSize + "bytes");
}
// 2、按照用户设置或者资源大小智能的设置下载线程总数量
int tempThreadNum = setTotalTreadNum(resourceSize);
// 3、根据分配的线程数量,来将资源“等分”,并设置header
ArrayList<DownloadModel> downloadArray = new ArrayList<DownloadModel>();
if (isSupportLoadingAndSaving) {
// 从数据库读取
downloadArray = downlaodDB.getAllInfo();
} else {
// 从本地方法读取
downloadArray = initResoureSize(tempThreadNum, resourceSize);
}
// 4、划分线程进行下载
for (int i = 0; i < downloadArray.size(); ++i) {
DownloadModel tempModel = downloadArray.get(i);
MyRunnable runnable = new MyRunnable(tempModel);
Thread thread = new Thread(runnable);
thread.start();
}
}
/**
* 配置Http信息,准备开始下载数据
*
* @throws IOException
*/
private HttpURLConnection settingRequestHttp(DownloadModel model)
throws IOException {
HttpURLConnection coon = (HttpURLConnection) this.resourceURL
.openConnection();
coon.setConnectTimeout(3 * 1000);
coon.setRequestMethod("GET");
coon.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
coon.setRequestProperty("Accept-Language", "zh-CN");
coon.setRequestProperty("Referer", resourceUrl);
coon.setRequestProperty("Charset", "UTF-8");
coon.setRequestProperty(
"User-Agent",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
coon.setRequestProperty("Connection", "Keep-Alive");
// 断点续传的核心代码,设置下载区间,注意是bytes=在这吃过亏
if (model != null) {
coon.setRequestProperty(
"Range",
"bytes=" + model.getDownloadedLength() + "-"
+ model.getDownloadLengthSum());
}
return coon;
}
/**
* 多线程下载共享的run方法
*
* @author Administrator
*
*/
private class MyRunnable implements Runnable {
private DownloadModel rModel;
public MyRunnable(DownloadModel model) {
this.rModel = model;
}
@Override
public void run() {
try {
Log.i("Debug", rModel.getThreadName());
HttpURLConnection coon = settingRequestHttp(rModel);
if (coon.getResponseCode() == 200) {
Log.e("Debug", "Coonected sucessfully");
}
resourceSize = coon.getContentLength();
if (resourceSize <= 0) {
Log.e("Debug", "unkown file Length and return");
return;
} else {
readResourceAndSave(coon, rModel);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 将网络数据从网上读取下来
*
* @throws IOException
*/
private void readResourceAndSave(HttpURLConnection coon, DownloadModel model)
throws IOException {
//创建文件夹
File savDir = Environment.getExternalStorageDirectory();
File file = null;
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
if (!savDir.exists()) {
savDir.mkdirs();
}
file = new File(savDir, this.savaFileName);
}
RandomAccessFile rFile = new RandomAccessFile(file, "rwd");
// 跳转到当前线程对文件的读写起始位置
rFile.seek(model.getDownloadedLength());
InputStream finput = coon.getInputStream();
if (finput != null) {
Log.e("Debug", "finput=>" + finput.toString());
} else {
Log.e("Debug", "finput is null and return");
}
Log.e("Debug", "rFile.getFilePointer=>" + rFile.getFilePointer());
BufferedInputStream bInput = new BufferedInputStream(finput);
// 设置3KB的缓冲区,加快下载速度,这里后期改成按字符或者整行读取方式优化
int size = 3 * 1024;
byte[] readBytes = new byte[size];
int readNum = 0;
long sum = 0;
// 当还有数据可读且线程没有暂停
long needReadNum = model.getDownloadLengthSum()
- model.getDownloadedLength();
while ((readNum = bInput.read(readBytes, 0, size)) > 0
&& sum < needReadNum && !isStopDownloading) {
sum += readNum;
// 如果请求的尺寸下一次读取即将超过总需求数量时,修正读取内容的大小保证不多读取
if ((sum + size) > needReadNum) {
size = (int) (needReadNum - sum);
}
// 写入到文件中
rFile.write(readBytes, 0, readNum);
}
Log.e("Debug",
model.getThreadName() + "finish downloading:" + rFile.length()
+ "-bytes");
rFile.close();
finput.close();
}
/**
* 计算需要下载的资源大小
*
* @param url
* 待下载的资源内容链接
* @return 返回资源所占的字节数
*/
private ArrayList<DownloadModel> initResoureSize(int tNum,
long contentLength) {
long tempLastSize = contentLength;// 剩余的下载内容大小
ArrayList<DownloadModel> downloadArray = new ArrayList<DownloadModel>();
long commonSize = contentLength / tNum;
for (int i = 0; i < tNum; ++i) {
DownloadModel downloadModel = new DownloadModel();
// 还未开始下载,所以已下载的大小为0byte,他的长度代表下一个线程的下载的起点
downloadModel.setDownloadedLength(i * commonSize);
long loadEnding = tempLastSize;
// 剩下的最后一个下载线程的下载大小=总线程-其他线程下载的大小和
if (i == tNum - 1) {
loadEnding = contentLength;
} else {
loadEnding = commonSize * i + commonSize;
}
tempLastSize = contentLength - commonSize;
downloadModel.setDownloadLengthSum(loadEnding);
downloadModel.setThreadName("JX_Download_Thread" + i);
// 如果支持断点续传,就存储到数据库中,否则暂时存储到ArrayList中
if (isSupportLoadingAndSaving) {
downlaodDB.insert(downloadModel);
} else {
downloadArray.add(downloadModel);
}
}
return downloadArray;
}
/**
* 根据资源大小智能的设置下载线程总数量
*
* @param totalTreadNum
* 设置的下载数据量
*/
private int setTotalTreadNum(long contentLength) {
// 如果用户没有设置下载的线程数量则根据大小智能设置
if (0 == this.totalTreadNum) {
if (contentLength <= 0) {
Log.i("Debug", "下载的内容太小!");
return 0;
} else if (contentLength < BELOW_Three_M_SIZE) {
this.totalTreadNum = 1;
} else if (contentLength < BELOW_SIX_M_SIZE) {
this.totalTreadNum = 2;
} else if (contentLength < BELOW_EIGHTEEN_M_SIZE) {
this.totalTreadNum = 4;
} else {
this.totalTreadNum = 5;
}
}
return this.totalTreadNum;
}
/**
* 判断当前连接所在服务器是否支持断点续传下载
*
* @return 默认不支持断电续传
*/
public boolean isSupportLoadingAndSaving() {
return isSupportLoadingAndSaving;
}
public long getDownloaderID() {
return downloaderID;
}
public void setDownloaderID(long downloaderID) {
this.downloaderID = downloaderID;
}
public int getTotalTreadNum() {
return totalTreadNum;
}
public String getResourceUrl() {
return resourceUrl;
}
public long getResourceSize() {
return resourceSize;
}
}
下载器使用:
package com.jx.main;
import java.net.MalformedURLException;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.example.mutilthreaddownloader.R;
import com.jx.downloader.JX_Downloader;
/**
* 2016年3月19日09:41:00
*
* @author J_X 多线程测试类
*/
public class MainActivity extends Activity {
JX_Downloader downloader;
Button btnDownlaod;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnDownlaod = (Button)findViewById(R.id.btn_start_downlaoding);
btnDownlaod.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
try {
downloader = new JX_Downloader(MainActivity.this,
"****.apk""****.apk); downloader.startDownload();
} catch (MalformedURLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
}
}
小结:
下载器的速度,出来外部因素网速,服务器传输速度,内部因素主要是线程的数量和缓冲区能够影响下载速度,但是笔者在魅族酷派小米虚拟机上同一wifi相同线程数量以及同样大小缓冲区下,下载速度小米的下载速度奇慢,还有待考证具体原因。
以上是关于多线程下载器(不含数据库部分)的主要内容,如果未能解决你的问题,请参考以下文章