1.前言
对于APP项目而言,虽然稀松平常的下载一般不会考虑到所谓的断点下载,但在一旦考虑到文件的大小和对用户的体验和对用户的流量的关心和优化,在处理文件方面,就需要用到断点下载。估计大家都有一定的思路来写,一般也就是把文件下载的数据断点进行保存之后,储存在手机内存或者SQLite小型数据库中,起始也可以通过SharePrefress来存储下载的断点,对速度而然在SharePrefress中不用考虑url和是否原来存储过数据的过程,在维护数据方面中,方便维护数据来说,还是需要对SQLite中的数据进行管来来得实在,单一求速度,个人建议使用SharePrefress就可以了。
2.知识点
(1)断点传递给服务器端,请求需要的从断点开始的数据:
Request request = new Request.Builder()
.addHeader("User-Agent", "android")
.header("Content-Type", "text/html; charset=utf-8;")
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.url(url)
.build();
(2)在写入数据的时候,调节对应的断点进行结合:
方案一:
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.seek(downloadedLength);
方案二:
FileChannel channelOut = randomAccessFile.getChannel()
MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE,response.body().contentLength());
此处注意的是,在写入数据的时候,有人喜欢用数据通道加缓存来写入数据,这样写的结果其实都是可以的,唯一的区别是用缓存加数据通道(方案二),第一次写入就相当于占用了内存空间,但是实际没有那么大的内存。
3.DownloadUtils工具类
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import com.android.utils.DataUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Created by Ice
* on 2017/3/20.
*/
public class DownloadUtils {
private final String DOWNLOAD_FOLDER = "Download";
public final static int UPDATE_PROGRESS = 0x001;
public final static int DOWNLOAD_SUCCEED = 0x002;
public final static int DOWNLOAD_FAILED = 0x003;
private Call call;
private boolean isCancel;
private boolean isPause;
private String url;
private String fileName;
//已经下载文件的大小[下载一部分]
private long downloadedLength = 0;
private OnDownloadListener onDownloadListener;
private File downloadedFile;
private String folderName;
private long totalSize = 0;
private DownloadRecord downloadRecord;
private Context context;
public DownloadUtils(Context context) {
this.context = context;
downloadRecord = new DownloadRecord(context);
}
/**
* 下载工具构造方法
*
* @param url 下载文件的url
* @param fileName 文件名称[文件类型需要]
*/
public DownloadUtils(String url, String fileName) {
this.url = url;
this.fileName = fileName;
}
public boolean isCancel() {
return isCancel;
}
public void setCancel(boolean cancel) {
isCancel = cancel;
}
public boolean isPause() {
return isPause;
}
public void setPause(boolean pause) {
isPause = pause;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public File getDownloadedFile() {
return downloadedFile;
}
public String getFolderName() {
return folderName;
}
public void setFolderName(String folderName) {
this.folderName = folderName;
}
public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
this.onDownloadListener = onDownloadListener;
}
/**
* 开始下载
*/
public void start() {
isPause = false;
download();
}
public void pause() {
if (call == null) {
return;
}
call.cancel();
isPause = true;
sendDownloadErrorMsg("已暂停下载!");
}
/**
* 取消下载
*/
public void cancel() {
if (call == null) {
return;
}
call.cancel();
isCancel = true;
downloadedFile.deleteOnExit();
downloadRecord.deleteDownloadRecord(url);
sendDownloadErrorMsg("已取消下载!");
}
/**
* 创建文件
*
* @return
*/
private File createFile() {
File folder = new File(new FileUtils().createFolder(getFolderName() == null ? DOWNLOAD_FOLDER : getFolderName()));
if (folder.isDirectory() && !folder.exists()) {
folder.mkdirs();
}
return new File(folder.getAbsolutePath() + File.separator + (fileName == null ? createUrlFileName() : fileName));
}
/**
* 创建Url文件名称
*
* @return
*/
private String createUrlFileName() {
if (url.contains("/") && url.contains(".")) {
return url.substring(url.lastIndexOf("/") + 1);
}
SimpleDateFormat format = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss");
return format.format(format) + ".zip";
}
/**
* 下载
*/
private void download() {
if (TextUtils.isEmpty(url)) {
sendDownloadErrorMsg("下载地址为空!");
return;
}
if (!url.contains("http")) {
sendDownloadErrorMsg("下载地址错误!");
return;
}
File file = createFile();
if (file.exists()) {
downloadedLength = file.length();
} else {
downloadedLength = 0;
}
Request request = new Request.Builder()
.addHeader("User-Agent", "android")
.header("Content-Type", "text/html; charset=utf-8;")
.addHeader("RANGE", "bytes=" + downloadedLength + "-")
.url(url)
.build();
OkHttpClient okHttpClient = new OkHttpClient();
call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
sendDownloadErrorMsg(e.toString());
Log.e(this.getClass().getSimpleName(), "IOException:" + e.toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
doResponse(response, createFile());
}
});
}
/**
* 处理服务器返回数据
*
* @param response 返回对象
* @param file 存储文件
*/
private void doResponse(Response response, File file) {
downloadedFile = file;
long downloading = 0;
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
RandomAccessFile randomAccessFile = null;
try {
is = response.body().byteStream();
if (downloadedLength == 0) {
totalSize = response.body().contentLength();
downloadRecord.insertTotalRecord(url, totalSize + "");
} else {
Map<String, String> record = downloadRecord.queryDownloadRecord(url);
if (record != null) {
String total = record.get(DownloadRecord.COLUMN_TOTAL);
totalSize = Long.parseLong(total);
}
}
if (totalSize == downloadedLength) {
//已下载字节和文件总字节相等,说明下载已经完成了
sendDownloadErrorMsg("文件已下载,不需要重新下载!");
return;
}
if (totalSize == 0) {
if (downloadedLength == 0) {
sendDownloadErrorMsg("下载失败,文件长度值为0");
} else {
sendDownloadErrorMsg("文件已下载,不需要重新下载!");
}
return;
}
randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.seek(downloadedLength);
while ((len = is.read(buf)) != -1) {
if (isPause() || isCancel()) {
break;
}
randomAccessFile.write(buf, 0, len);
downloading += len;
long downSum = downloading + downloadedLength;
//传递更新信息
int percentage = (int) ((downloadedLength + downloading) * 100 / totalSize);
sendDownloadProgressMsg(percentage, totalSize, downSum);
}
randomAccessFile.close();
sendDownloadSuccessfulMsg(file);
} catch (Exception e) {
sendDownloadErrorMsg(e.toString());
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
sendDownloadErrorMsg(e.toString());
}
try {
if (randomAccessFile != null)
randomAccessFile.close();
} catch (IOException e) {
sendDownloadErrorMsg(e.toString());
}
}
}
/**
* 发送下载进度信息
*
* @param percentage 百分比
* @param total 文件大小
* @param downloading 正在下载的大小
*/
private void sendDownloadProgressMsg(int percentage, long total, long downloading) {
Message msg = downloadHandler.obtainMessage();
msg.what = UPDATE_PROGRESS;
Bundle bundle = new Bundle();
bundle.putInt("percentage", percentage);
bundle.putLong("total", total);
bundle.putLong("downloading", downloading);
msg.setData(bundle);
downloadHandler.sendMessage(msg);
}
/**
* 发送下载成功信息
*
* @param file
*/
private void sendDownloadSuccessfulMsg(File file) {
Message msg = downloadHandler.obtainMessage();
msg.what = DOWNLOAD_SUCCEED;
msg.obj = file;
downloadHandler.sendMessage(msg);
}
/**
* 发送下载失败信息
*
* @param error
*/
private void sendDownloadErrorMsg(String error) {
Message msg = downloadHandler.obtainMessage();
msg.what = DOWNLOAD_FAILED;
msg.obj = error.toString();
downloadHandler.sendMessage(msg);
}
/**
* 下载Handler处理
*/
private Handler downloadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (onDownloadListener == null) {
return;
}
switch (msg.what) {
case UPDATE_PROGRESS://下载中
Bundle bundle = msg.getData();
onDownloadListener.onDownloading(bundle.getInt("percentage"), bundle.getLong("total"), bundle.getLong("downloading"));
break;
case DOWNLOAD_SUCCEED://成功
onDownloadListener.onDownloadSucceed((File) msg.obj);
break;
case DOWNLOAD_FAILED://失败
onDownloadListener.onDownloadFailure((String) msg.obj);
break;
}
}
};
}
4.DownloadRecord工具类(主要是记录对应的下载断点的工具类BaseDatabase在另外一篇,可以自己找以下。)
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.util.Base64;
import com.android.database.BaseDatabase;
import com.android.utils.ListUtils;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
/**
* Created by Relin on 2018/4/27.
* 下载记录工具
* 方便于断点下载数据文件
*/
public class DownloadRecord extends BaseDatabase {
/**
* 文件记录表名
*/
public static final String TABLE_NAME = "DownloadRecord";
/**
* 文件url
*/
public static final String COLUMN_URL = "url";
/**
* 文件总大小
*/
public static final String COLUMN_TOTAL = "total";
/**
* 文件下载大小
*/
public static final String COLUMN_DOWNLOADED = "downloaded";
public DownloadRecord(Context context) {
super(context);
}
@Override
public void onCreate(SQLiteDatabase db) {
super.onCreate(db);
String sql = "create table if not exists " + TABLE_NAME
+ " (_id integer primary key autoincrement,"
+ COLUMN_URL + " text,"
+ COLUMN_DOWNLOADED + " text,"
+ COLUMN_TOTAL + " text)";
db.execSQL(sql);
}
/**
* 记录文件大小
*
* @param url 文件url地址
* @param total 文件总大小
* @return
*/
public long insertTotalRecord(String url, String total) {
if (queryDownloadRecord(url) == null) {
url = url.hashCode() + "";
ContentValues values = new ContentValues();
values.put(COLUMN_TOTAL, total);
values.put(COLUMN_URL, url);
return insert(TABLE_NAME, values);
} else {
return updateTotalRecord(url, total);
}
}
/**
* 记录文件下载大小
*
* @param url 文件url地址
* @param downloaded 文件下载大小
* @return
*/
public synchronized long insertDownloadedRecord(String url, String downloaded) {
if (queryDownloadRecord(url) == null) {
url = url.hashCode() + "";
ContentValues values = new ContentValues();
values.put(COLUMN_DOWNLOADED, downloaded);
values.put(COLUMN_URL, url);
return insert(TABLE_NAME, values);
} else {
return updateDownloadedRecord(url, downloaded);
}
}
/**
* 更新下载记录
*
* @param url 文件url地址
* @param downloaded 文件下载大小
* @return
*/
private long updateDownloadedRecord(String url, String downloaded) {
url = url.hashCode() + "";
ContentValues values = new ContentValues();
values.put(COLUMN_DOWNLOADED, downloaded);
return update(TABLE_NAME, values, COLUMN_URL + "= ?", new String[]{url});
}
/**
* 更新文件长度记录
*
* @param url 文件url地址
* @param total 文件总大小
* @return
*/
private long updateTotalRecord(String url, String total) {
url = url.hashCode() + "";
ContentValues values = new ContentValues();
values.put(COLUMN_TOTAL, total);
return update(TABLE_NAME, values, COLUMN_URL + " = ?", new String[]{url});
}
/**
* 删除下载记录
*
* @param url
* @return
*/
public long deleteDownloadRecord(String url) {
url = url.hashCode() + "";
return delete(TABLE_NAME, COLUMN_URL + " = ?", new String[]{"'" + url + "'"});
}
/**
* 查询文件记录历史
*
* @param url 文件地址url
* @return
*/
public Map<String, String> queryDownloadRecord(String url) {
url = url.hashCode() + "";
List<Map<String, String>> list = query("select * from " + TABLE_NAME + " where " + COLUMN_URL + " = '" + url + "'");
if (ListUtils.getSize(list) == 0) {
return null;
}
return list.get(0);
}
/**
* 删除下载历史记录缓存
*/
public void deleteDownloadRecord() {
deleteTable(TABLE_NAME);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}