您的位置:首页 > 移动开发 > Android开发

安卓文件下载之断点续传(一)

2017-09-25 18:28 330 查看
最近学了一段时间node js,结果公司暂时没有业务代码用的到,又先转回了安卓这边。因为之前手机应用更新这一块逻辑一直有些问题,在讨论了一番后决定花段时间把这块重做一遍,更新怎么少的了文件下载,文件下载中又有许多需要注意或者说可以优化的地方,断点续传就是其中的一种优化方式,看了许多网上的例子,自己在经过一周的时间后,总算是做了一个过的去的 demo ,目前 demo 比较完善的实现了开启一个线程断点下载的功能,DownloadManager的方式正在实现中,两者的思路是完全相同的,下面就让我们对这个文件下载 demo 制作的过程进行一下分析:

首先想要实现断点续传当然需要记录终端的 在哪,这里的点指的就是文件已经下载的大小,你需要让服务器知道你所已经下载的大小是多少,然后服务器才能返回具体那一段的数据,当然我们还需要记录一些别的内容,如文件名,文件大小,临时文件等等。这里我选用数据库记录这些字段,选择使用了 greedao 这个第三方库,目前项目添加的依赖为:

compile 'de.greenrobot:DaoGenerator:1.3.0'
compile 'de.greenrobot:greendao:1.3.7'


库的版本比较老了,有想使用新版本的可以去百度搜一下,介绍有很多,这里就不再赘述,该 demo 以1.3版本为例。

首先我们创建一个 CreateDBImpl 类用来生成数据库相关文件:

/**
* Created by 冒险者ztn on 2017/9/11.
*/

public class CreateDBImpl {

public static void main(String args[]) throws Exception {
Schema schema = new Schema(1, "com.example.zhangtianning.download.dao");
addDownloadFileSize(schema);
DaoGenerator daoGenerator = new DaoGenerator();
String PATH = "app/src/main/java/";
daoGenerator.generateAll(schema, PATH);
}

private static void addDownloadFileSize(Schema schema) {
Entity downloadInfo = schema.addEntity("DownloadFileInfo");
downloadInfo.addIdProperty().primaryKey();

downloadInfo.addLongProperty("fileSize");
downloadInfo.addStringProperty("filePath");
downloadInfo.addStringProperty("fileName");

downloadInfo.addStringProperty("tempFilePath");

downloadInfo.addStringProperty("downloadUrl");

downloadInfo.addLongProperty("hadDownloadSize");
downloadInfo.addIntProperty("downloadProgress");

downloadInfo.addIntProperty("version");

}
}


我在数据库中记录了这么几个字段,右键run一下生成对应的文件:



写一个接口,看看我们可能会有那些用到的方法:

/**
* 接口
* Created by 冒险者ztn on 2017/9/11.
*/

public interface DBInstances {

/**
* 获取所有下载内容信息
*/
List<DownloadFileInfo> getDownloadFileInfo();

/**
* 根据url获取对应下载内容信息
*/
DownloadFileInfo getDownloadFileInfoWithUrl(String url);

/**
* 保存下载内容信息
*/

void setDownloadFileInfo(DownloadFileInfo downloadFileInfo);

/**
* 清除数据
*/

void clearDownloadFileInfo();

/**
* 是否有完整文件
*
* @param downloadFileInfo
* @return
*/
Boolean hasCompleteFile(DownloadFileInfo downloadFileInfo);

/**
* 初始化一条数据
*
* @param url
* @return
*/
DownloadFileInfo initBaseDownloadFileInfo(String url);

/**
* 更新一条数据
*
* @param downloadFileInfo
*/
void Updata(DownloadFileInfo downloadFileInfo);
}


因为做的是个demo 功能与实际需求还有一定的区别,这里大家可以根据需求实现自己的方法。

然后就是下载的流程了,这里我制作了一张流程图,方便大家能看的更清楚一些,之后我们代码的书写过程也会参考这张流程图来写,让人有更清晰的思路:



我们先来实现最开始的几个功能看这个 url 是否符合规矩并在我们的数据库中有所记录:

private void urlIsInDB(Context context, String url) {

if (url.indexOf("http") == 0
|| url.indexOf("https") == 0) {
downloadFileInfo = DBDaoImpl.getInstance(context).getDownloadFileInfoWithUrl(url);

if (downloadFileInfo != null) {
if (TextUtils.equals(downloadFileInfo.getHadDownloadSize().toString(), downloadFileInfo.getFileSize().toString())) {
File file = new File(downloadFileInfo.getFilePath());
if (file.exists()) {
downloadState = STATE_FINISH_DOWNLOAD;
OpenApk.getInstance().openFile(mainActivity, file);
} else {
downloadFileInfo.setHadDownloadSize((long) 0);
DBDaoImpl.getInstance(context).Updata(downloadFileInfo);
}

} else {
fileDownload.start(mainActivity, downloadFileInfo);
Message message = myWeakHandler.obtainMessage();
downloadState = message.what = STATE_START_DOWNLOAD;
myWeakHandler.sendMessage(message);
}
} else {
LogUtils.D(TAG, "没有文件信息!");
fileDownload.start(mainActivity, etUrl.getText().toString());
}

} else {
Toast.makeText(mainActivity, "请输入一个正确的url", Toast.LENGTH_SHORT).show();
}
}


然后在FileDownload类中开启线程:

/**
* 开始下载
*
* @param downloadurl 文件url地址
*/
public void start(Activity activity, String downloadurl) {
start(activity, DBDaoImpl.getInstance(activity).initBaseDownloadFileInfo(downloadurl));
}

/**
* 已经有记录开始下载
*
* @param downloadFileInfo 下载信息
*/
public void start(Activity activity, DownloadFileInfo downloadFileInfo) {
downloadThread = new DownloadThread(downloadFileInfo, mFileDownloadListener, activity);
downloadThread.start();
}


最终归并于一个方法,并开启了线程。

在线程中我们初始化一个HttpURLConnection ,并进行一些设置,在这里遇到了关于不少问题,尤其是对于header的一些参数的设置,如获取文件大小为-1,无法下载文件等等,在尝试多遍后目前的设置为:

url = new URL(this.mDownloadUrl);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setUseCaches(false);  // 请求时不使用缓存
httpURLConnection.setConnectTimeout(5 * 1000); // 设置连接超时时间
httpURLConnection.setRequestMethod("GET");
httpURLConnection.setRequestProperty("Accept-Language", "zh-cn");
httpURLConnection.setRequestProperty("UA-CPU", "x86");
httpURLConnection.setRequestProperty("Accept-Encoding", "gzip");//为什么没有deflate呢
//            httpURLConnection.setRequestProperty("Content-type", "text/html");
httpURLConnection.setRequestProperty("Connection", "close");

//            httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)");
httpURLConnection.setRequestProperty("Accept",
"image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*");
//            httpURLConnection.setRequestProperty("User-Agent", " Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36");


可以看到我在最开始并没有向header中设置文件从哪开始下载,因为httpURLConnection不能在get某个参数在对某个参数set,如果get代表链接已经发出,就不能在设置了,接下来我们看开断点续传的关键代码:

long downloadSize = mDownloadFileInfo.getHadDownloadSize();

String fileName = mDownloadFileInfo.getFileName();

long fileLength;

if (downloadSize == mDownloadFileInfo.getFileSize()) {
OpenApk.getInstance().openFile(ctx, new File(mDownloadFileInfo.getFilePath()));
interrupt();
}
if (downloadSize > 0) {
String tmpFilePath = Environment.getExternalStorageDirectory().getPath() + FILE_DOWNLOAD_TEMP_DIR + fileName;
LogUtils.D("Tag", "临时文件的路径:" + tmpFilePath);
fileLength = mDownloadFileInfo.getFileSize();
httpURLConnection.setRequestProperty("Range",
"bytes=" + mDownloadFileInfo.getHadDownloadSize() + "-"
+ mDownloadFileInfo.getFileSize());
tmpFile = new File(tmpFilePath);
if (!tmpFile.exists() || !tmpFile.isFile()) {
tmpFile = FileUtils.createTempFile(tmpfileBasePath, fileName);
mDownloadFileInfo.setTempFilePath(tmpFile.getAbsolutePath());
}
} else {
fileLength = httpURLConnection.getContentLength(); // 获取文件的大小

mDownloadFileInfo.setFileSize(fileLength);

if (fileName == null) {
fileName = httpURLConnection.getHeaderField("Content-Disposition"); // 获取文件名
}
if (fileName == null) {
fileName = mDownloadUrl.substring(mDownloadUrl.lastIndexOf("/") + 1);
}
mDownloadFileInfo.setFileName(fileName);
mDownloadFileInfo.setFilePath(basePath + File.separator + fileName);
String tmpFilePath = Environment.getExternalStorageDirectory().getPath() + FILE_DOWNLOAD_TEMP_DIR + fileName;
LogUtils.D("Tag", "临时文件的路径:" + tmpFilePath);
tmpFile = FileUtils.createTempFile(tmpfileBasePath, fileName);
mDownloadFileInfo.setTempFilePath(tmpFile.getAbsolutePath());
}


我们从数据库中获取已下载文件大小,如果为-1的话我们认为这个文件从未下载过,并尝试从请求中获取文件名,与文件大小,并对数据库中的字段一一进行设置。如果下载文件大于0,那么我们认为数据库中已经有了对应的完整的文件信息,并对header中的Range字段进行设置。

接下来自然是获取请求的状态码了:

int code = httpURLConnection.getResponseCode();


这里有个地方需要注意,正常的状态码为200,断点续传的状态码为206,所以如果已下载部分文件,状态码却依然返回200,那么我们认为服务器是不支持断点续传的,需要重新下载:

if (code == HttpURLConnection.HTTP_OK) {
if (mDownloadFileInfo.getHadDownloadSize() > 0) {
// 子线程
ToastUtils.showToast(ctx, "文件不支持断点续传,已重新开始下载!");
mDownloadFileInfo.setHadDownloadSize((long) -1);
}
long currentTime = System.currentTimeMillis();
int bufferSize = 1024;
bufferedInputStream = new BufferedInputStream(httpURLConnection.getInputStream(), bufferSize);
int len; //读取到的数据长度
byte[] buffer = new byte[bufferSize];
//写入中间文件
mOutputStream = new FileOutputStream(tmpFile, true);//true表示向打开的文件末尾追加数据
mByteOutput = new ByteArrayOutputStream();
// 开始读取
while ((len = bufferedInputStream.read(buffer)) != -1) {
mByteOutput.write(buffer, 0, len);
mDownloadFileInfo = writeCache(mDownloadFileInfo);
progress = DownloadUtils.getProgress(mDownloadFileInfo.getHadDownloadSize(), fileLength);
long nowTime = System.currentTimeMillis();
if (currentTime < nowTime - 500) {
currentTime = nowTime;

mDownloadFileInfo.setDownloadProgress(progress);
if (mFileDownloadListener != null) {
if (downloadSize == fileLength) {
mFileDownloadListener.onFileDownloadCompleted(mDownloadFileInfo);
break;
} else {
mFileDownloadListener.onFileDownloading(mDownloadFileInfo);
}
}
}
if (isStopDownload) {
if (mFileDownloadListener != null) {
mFileDownloadListener.onFileDownloadPaused(mDownloadFileInfo);
}
break;
}
}

} else if (code == HttpURLConnection.HTTP_PARTIAL) {
long currentTime = System.currentTimeMillis();
int bufferSize = 1024;
bufferedInputStream = new BufferedInputStream(httpURLConnection.getInputStream(), bufferSize);
int len; //读取到的数据长度
byte[] buffer = new byte[bufferSize];
//写入中间文件
mOutputStream = new FileOutputStream(tmpFile, true);//true表示向打开的文件末尾追加数据
mByteOutput = new ByteArrayOutputStream();
// 开始读取
while ((len = bufferedInputStream.read(buffer)) != -1) {
mByteOutput.write(buffer, 0, len);
mDownloadFileInfo = writeCache(mDownloadFileInfo);
progress = DownloadUtils.getProgress(mDownloadFileInfo.getHadDownloadSize(), fileLength);
long nowTime = System.currentTimeMillis();
if (currentTime < nowTime - 500) {
currentTime = nowTime;
mDownloadFileInfo.setDownloadProgress(progress);
if (mFileDownloadListener != null) {
if (downloadSize == fileLength) {
mFileDownloadListener.onFileDownloadCompleted(mDownloadFileInfo);
break;
} else {
mFileDownloadListener.onFileDownloading(mDownloadFileInfo);
}
}
}
if (isStopDownload) {
if (mFileDownloadListener != null) {
mFileDownloadListener.onFileDownloadPaused(mDownloadFileInfo);
}
break;
}
}
}


最终效果图:



上面只是逻辑部分的代码,可以下载我github上面的demo作为参考,欢迎提问,欢迎star。

github项目地址:文件下载demo
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息