文章目录
这篇文章中记录下单线程的文件断点下载。效果:
项目代码链接: https://github.com/baiyazi/AndroidDownloadUtils,可自取。
同样的这里做一个简单的逻辑分析。
文章目录
1. 相关逻辑
为了实现更新进度条,我们需要知道待下载文件的总长度,故而这里需要一次请求。另外,对于单线程断点下载而言,请求的数据的起始位置为当前存储的文件的长度。然后再次建立连接,请求数据即可。
1.1 获取待下载文件的总长度
HttpURLConnection connection = (HttpURLConnection) url1.openConnection();
...
connection.connect();
// 获取文件总长度
totalLength = connection.getContentLength();
1.2 使用HTTP的Range头部字段
第二次建立连接请求数据的时候,需要使用Range
来指定请求的数据域,比如:
connection.setRequestProperty("Range", "bytes=" + startPoint + "-" );
指定了Range
之后,表示请求文件的部分内容。故而返回的状态码为206
,表示部分数据。
1.3 RandomAccessFile
用RandomAccessFile
来指定写入文件的起始位置,具体使用seek
方法来指定起始位置。然后开始数据的文件写入操作,比如:
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.seek(startPoint);
然后得到请求的数据流对象,并将这个流对象写入到上面的文件中:
InputStream inputStream = connection.getInputStream();
if (connection.getResponseCode() == 206) {
byte[] buffer = new byte[2048];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, len);
startPoint += len;
if(listener != null) listener.onListener(startPoint, totalLength);
if(isPause.getValue()){
Log.d(TAG, "Download paused!");
if(listener != null) listener.onPause(startPoint);
connection.disconnect();
isPause.setValue(false); // 重置
return;
}
}
Log.d(TAG, "Download successful.");
if(listener != null) listener.onSuccess(file);
}
1.4 添加监听接口
在上面的写入数据的时候,我加入了监听判断,比如:listener
的定义为:
public interface IDownLoadListener{
void onStart(long startPos); // 开始
void onPause(long pausePos); // 暂停
void onResume(long resumePos); // 恢复下载
void onSuccess(File file); // 下载成功
void onError(String msg); // 下载失败
void onCancel(); // 取消下载
void onListener(long currentPos, long totalLength); // 监听下载进度
}
isPause
定义为一个枚举类型:
private State isPause = State.ISPAUSE;
public enum State{
ISPAUSE(false),
ISCANCEL(false);
private boolean value;
State(boolean value){
this.value = value;
}
public boolean getValue(){
return this.value;
}
public void setValue(boolean value) {
this.value = value;
}
}
2. 调用示例
String url = "http://vjs.zencdn.net/v/oceans.mp4";
DownLoader build = new DownLoader.Builder(getApplicationContext())
.url(url)
.suffix(DownLoader.FileSuffix.MP4)
.startPoint(0)
.build();
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
build.download(new DownLoader.DownLoadListener(){
@Override
public void onSuccess(File file) {
Log.e("ThreeActivity", "onSuccess");
}
@Override
public void onError(String msg) {
Log.e("ThreeActivity", "onError: " + msg);
}
@Override
public void onListener(long currentPos, long totalLength) {
int val = (int) (currentPos * 1.0 / totalLength * 100);
progressbar.setProgress(val);
}
});
}
});
pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
build.setIsPause(true);
}
});
3. DownLoader完整代码
/**
* 单线程断点下载
*/
public class DownLoader {
private static final String TAG = "DownLoader";
private String url;
private int connectionTimeout;
private String method = "GET";
private Context context;
private String cachePath = "imgs";
private FileSuffix suffix;
private long startPoint, totalLength;
private State isPause = State.ISPAUSE;
private State isCancel = State.ISCANCEL;
private DownLoader(){}
public DownLoader(Context context){
connectionTimeout = 500; // 500毫秒
method = "GET";
this.context = context;
}
public DownLoader url(String url){
this.url = url;
return this;
}
public DownLoader fileSuffix(FileSuffix fileSuffix){
this.suffix = fileSuffix;
return this;
}
public DownLoader cacheDir(String dir){
this.cachePath = dir;
return this;
}
public DownLoader startPoint(long start){
this.startPoint = start;
return this;
}
public DownLoader(Builder builder){
this.url = builder.url;
this.connectionTimeout = builder.connectionTimeout;
this.context = builder.context;
this.cachePath = builder.cachePath;
this.suffix = builder.suffix;
this.startPoint = builder.startPoint;
}
public static class Builder {
private String url;
private int connectionTimeout;
private String cachePath = "imgs";
private Context context;
private FileSuffix suffix;
private long startPoint;
private Builder(){}
public Builder(Context context){
this.context = context;
}
public Builder url(String url){
this.url = url;
return this;
}
public Builder timeout(int ms){
this.connectionTimeout = ms;
return this;
}
public Builder startPoint(long start){
this.startPoint = start;
return this;
}
public Builder suffix(FileSuffix suffix){
this.suffix = suffix;
return this;
}
public Builder cacheDirName(String cacheDirName){
this.cachePath = cacheDirName;
return this;
}
public DownLoader build(){
return new DownLoader(this);
}
}
public enum State{
ISPAUSE(false),
ISCANCEL(false);
private boolean value;
State(boolean value){
this.value = value;
}
public boolean getValue(){
return this.value;
}
public void setValue(boolean value) {
this.value = value;
}
}
/**
* 支持的文件类型后缀
*/
public enum FileSuffix{
EXE("exe"),
ZIP("zip"),
JPEG("jpeg"),
PNG("png"),
GIF("gif"),
MP4("mp4"),
MP3("mp3"),
PDF("pdf");
private String value; // 实际值
FileSuffix(String value) {
this.value = value;
}
public String getValue(){
return this.value;
}
}
public void setIsPause(boolean isPause){
this.isPause.setValue(isPause);
}
public void setIsCancel(boolean isCancel){
this.isCancel.setValue(isCancel);
}
public interface IDownLoadListener{
void onStart(long startPos); // 开始
void onPause(long pausePos); // 暂停
void onResume(long resumePos); // 恢复下载
void onSuccess(File file); // 下载成功
void onError(String msg); // 下载失败
void onCancel(); // 取消下载
void onListener(long currentPos, long totalLength); // 监听下载进度
}
public abstract static class DownLoadListener implements IDownLoadListener{
@Override
public void onStart(long startPos) {
Log.d(TAG, "DownLoadListener ==> onStart");
}
@Override
public void onPause(long pausePos) {
Log.d(TAG, "DownLoadListener ==> onPause");
}
@Override
public void onResume(long resumePos) {
Log.d(TAG, "DownLoadListener ==> onResume");
}
@Override
public void onCancel() {
Log.d(TAG, "DownLoadListener ==> onCancel");
}
}
public void download(DownLoadListener listener){
new Thread(new Runnable() {
@Override
public void run() {
File path = buildPath(cachePath);
HttpURLConnection connection = null;
try {
URL url1 = new URL(url);
connection = (HttpURLConnection) url1.openConnection();
connection.setConnectTimeout(connectionTimeout);
connection.setRequestMethod(method);
connection.setRequestProperty("Charset", "UTF-8");
connection.setRequestProperty("accept", "*/*");
connection.connect();
// 获取文件总长度
totalLength = connection.getContentLength();
if (suffix == null) {
throw new RuntimeException("You must set the download file suffix before.");
}
File file = new File(path, Utils.hashKeyFromUrl(url) + "." + suffix.getValue());
startPoint = file.length();
connection.disconnect();
// 重新请求一次
connection = (HttpURLConnection) url1.openConnection();
connection.setConnectTimeout(connectionTimeout);
connection.setRequestMethod(method);
connection.setRequestProperty("Charset", "UTF-8");
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("Range", "bytes=" + startPoint + "-" );
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.seek(startPoint);
InputStream inputStream = connection.getInputStream();
if(startPoint == 0 && listener != null) {
listener.onStart(0);
}
if (connection.getResponseCode() == 206) {
byte[] buffer = new byte[2048];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, len);
startPoint += len;
if(listener != null) listener.onListener(startPoint, totalLength);
if(isPause.getValue()){
Log.d(TAG, "Download paused!");
if(listener != null) listener.onPause(startPoint);
connection.disconnect();
isPause.setValue(false); // 重置
return;
}
}
Log.d(TAG, "Download successful.");
if(listener != null) listener.onSuccess(file);
}
}catch (IOException e){
Log.e(TAG, "Download bitmap failed.", e);
if(listener != null) listener.onError(e.getLocalizedMessage());
e.printStackTrace();
}finally {
if(connection != null) connection.disconnect();
}
}
}).start();
}
private File buildPath(String filePath) {
// 是否有SD卡
boolean flag = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
// 如果有SD卡就存在外存,否则就位于这个应用的data/package name/cache目录下
final String cachePath;
if(flag) cachePath = context.getExternalCacheDir().getPath();
else cachePath = context.getCacheDir().getPath();
File directory = new File(cachePath + File.separator + filePath);
// 目录不存在就创建
if(!directory.exists()) directory.mkdirs();
return directory;
}
}
4. 后记
后续将继续尝试多线程版本的断点下载。因为单线程确实比较慢。在Android文件多线程下载(二)也测试过下载这个视频文件,很快。这里的单线程就感觉很慢了。具体的时间差我这里没有测试。