http android下载工具,Android实现下载工具的简单代码

下载应该是每个App都必须的一项功能,不采用第三方框架的话,就需要我们自己去实现下载工具了。如果我们自己实现可以怎么做呢?

首先如果服务器文件支持断点续传,则我们需要实现的主要功能点如下:

多线程、断点续传下载

下载管理:开始、暂停、继续、取消、重新开始

如果服务器文件不支持断点续传,则只能进行普通的单线程下载,而且不能暂停、继续。当然一般情况服务器文件都应该支持断点续传吧!

下边分别是单个任务下载、多任务列表下载、以及service下载的效果图:

497bbf2638968079f158a7e5c48695fe.gif

single_task

4e5657482abd77cd5ffdd702a8657a18.gif

task_manage

39e1bcb619ca6697abb91e412162e81e.gif

service_task

基本实现原理:

接下来看看具体的实现原理,由于我们的下载是基于okhttp实现的,首先我们需要一个OkHttpManager类,进行最基本的网络请求封装:

public class OkHttpManager {

............省略..............

/**

* 异步(根据断点请求)

*

* @param url

* @param start

* @param end

* @param callback

* @return

*/

public Call initRequest(String url, long start, long end, final Callback callback) {

Request request = new Request.Builder()

.url(url)

.header("Range", "bytes=" + start + "-" + end)

.build();

Call call = builder.build().newCall(request);

call.enqueue(callback);

return call;

}

/**

* 同步请求

*

* @param url

* @return

* @throws IOException

*/

public Response initRequest(String url) throws IOException {

Request request = new Request.Builder()

.url(url)

.header("Range", "bytes=0-")

.build();

return builder.build().newCall(request).execute();

}

/**

* 文件存在的情况下可判断服务端文件是否已经更改

*

* @param url

* @param lastModify

* @return

* @throws IOException

*/

public Response initRequest(String url, String lastModify) throws IOException {

Request request = new Request.Builder()

.url(url)

.header("Range", "bytes=0-")

.header("If-Range", lastModify)

.build();

return builder.build().newCall(request).execute();

}

/**

* https请求时初始化证书

*

* @param certificates

* @return

*/

public void setCertificates(InputStream... certificates) {

try {

CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

keyStore.load(null);

int index = 0;

for (InputStream certificate : certificates) {

String certificateAlias = Integer.toString(index++);

keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

try {

if (certificate != null)

certificate.close();

} catch (IOException e) {

}

}

SSLContext sslContext = SSLContext.getInstance("TLS");

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

trustManagerFactory.init(keyStore);

sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

builder.sslSocketFactory(sslContext.getSocketFactory());

} catch (Exception e) {

e.printStackTrace();

}

}

}

这个类里包含了基本的超时配置、根据断点信息发起异步请求、校验服务器文件是否有更新、https证书配置等。这样网络请求部分就有了。

接下来,我们还需要数据库的支持,以便记录下载文件的基本信息,这里我们使用SQLite,只有一张表:

/**

* download_info表建表语句

*/

public static final String CREATE_DOWNLOAD_INFO = "create table download_info ("

+ "id integer primary key autoincrement, "

+ "url text, "

+ "path text, "

+ "name text, "

+ "child_task_count integer, "

+ "current_length integer, "

+ "total_length integer, "

+ "percentage real, "

+ "last_modify text, "

+ "date text)";

当然还有对应表的增删改查工具类,具体的可参考源码。

由于需要下载管理,所以线程池也是必不可少的,这样可以避免过多的创建子线程,达到复用的目的,当然线程池的大小可以根据需求进行配置,主要代码如下:

public class ThreadPool {

//可同时下载的任务数(核心线程数)

private int CORE_POOL_SIZE = 3;

//缓存队列的大小(最大线程数)

private int MAX_POOL_SIZE = 20;

//非核心线程闲置的超时时间(秒),如果超时则会被回收

private long KEEP_ALIVE = 10L;

private ThreadPoolExecutor THREAD_POOL_EXECUTOR;

private ThreadFactory sThreadFactory = new ThreadFactory() {

private final AtomicInteger mCount = new AtomicInteger();

@Override

public Thread newThread(@NonNull Runnable runnable) {

return new Thread(runnable, "download_task#" + mCount.getAndIncrement());

}

};

...................省略................

public void setCorePoolSize(int corePoolSize) {

if (corePoolSize == 0) {

return;

}

CORE_POOL_SIZE = corePoolSize;

}

public void setMaxPoolSize(int maxPoolSize) {

if (maxPoolSize == 0) {

return;

}

MAX_POOL_SIZE = maxPoolSize;

}

public int getCorePoolSize() {

return CORE_POOL_SIZE;

}

public int getMaxPoolSize() {

return MAX_POOL_SIZE;

}

public ThreadPoolExecutor getThreadPoolExecutor() {

if (THREAD_POOL_EXECUTOR == null) {

THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(

CORE_POOL_SIZE, MAX_POOL_SIZE,

KEEP_ALIVE, TimeUnit.SECONDS,

new LinkedBlockingDeque(),

sThreadFactory);

}

return THREAD_POOL_EXECUTOR;

}

}

接下来就是我们核心的下载类FileTask了,它实现了Runnable接口,这样就能在线程池中执行,首先看下run()方法的逻辑:

@Override

public void run() {

try {

File saveFile = new File(path, name);

File tempFile = new File(path, name + ".temp");

DownloadData data = Db.getInstance(context).getData(url);

if (Utils.isFileExists(saveFile) && Utils.isFileExists(tempFile) && data != null) {

Response response = OkHttpManager.getInstance().initRequest(url, data.getLastModify());

if (response != null && response.isSuccessful() && Utils.isNotServerFileChanged(response)) {

TEMP_FILE_TOTAL_SIZE = EACH_TEMP_SIZE * data.getChildTaskCount();

onStart(data.getTotalLength(), data.getCurrentLength(), "", true);

} else {

prepareRangeFile(response);

}

saveRangeFile();

} else {

Response response = OkHttpManager.getInstance().initRequest(url);

if (response != null && response.isSuccessful()) {

if (Utils.isSupportRange(response)) {

prepareRangeFile(response);

saveRangeFile();

} else {

saveCommonFile(response);

}

}

}

} catch (IOException e) {

onError(e.toString());

}

}

如果下载的目标文件、记录断点的临时文件、数据库记录都存在,则我们先判断服务器文件是否有更新,如果没有更新则根据之前的记录直接开始下载,否则需要先进行断点下载前的准备。如果记录文件不全部存在则需要先判断是否支持断点续传,如果支持则按照断点续传的流程进行,否则采用普通下载。

首先看下prepareRangeFile()方法,在这里进行断点续传的准备工作:

private void prepareRangeFile(Response response) {

.................省略.................

try {

File saveFile = Utils.createFile(path, name);

File tempFile = Utils.createFile(path, name + ".temp");

long fileLength = response.body().contentLength();

onStart(fileLength, 0, Utils.getLastModify(response), true);

Db.getInstance(context).deleteData(url);

Utils.deleteFile(saveFile, tempFile);

saveRandomAccessFile = new RandomAccessFile(saveFile, "rws");

saveRandomAccessFile.setLength(fileLength);

tempRandomAccessFile = new RandomAccessFile(tempFile, "rws");

tempRandomAccessFile.setLength(TEMP_FILE_TOTAL_SIZE);

tempChannel = tempRandomAccessFile.getChannel();

MappedByteBuffer buffer = tempChannel.map(READ_WRITE, 0, TEMP_FILE_TOTAL_SIZE);

long start;

long end;

int eachSize = (int) (fileLength / childTaskCount);

for (int i = 0; i < childTaskCount; i++) {

if (i == childTaskCount - 1) {

start = i * eachSize;

end = fileLength - 1;

} else {

start = i * eachSize;

end = (i + 1) * eachSize - 1;

}

buffer.putLong(start);

buffer.putLong(end);

}

} catch (Exception e) {

onError(e.toString());

} finally {

.............省略............

}

}

首先是清除历史记录,创建新的目标文件和临时文件,childTaskCount代表文件需要通过几个子任务去下载,这样就可以得到每个子任务需要下载的任务大小,进而得到具体的断点信息并记录到临时文件中。文件下载我们采用MappedByteBuffer类,相比RandomAccessFile更加的高效。同时执行onStart()方法将代表下载的准备阶段,具体细节后面会说到。

接下来看saveRangeFile()方法:

private void saveRangeFile() {

.................省略..............

for (int i = 0; i < childTaskCount; i++) {

final int tempI = i;

Call call = OkHttpManager.getInstance().initRequest(url, range.start[i], range.end[i], new Callback() {

@Override

public void onFailure(Call call, IOException e) {

onError(e.toString());

}

@Override

public void onResponse(Call call, Response response) throws IOException {

startSaveRangeFile(response, tempI, range, saveFile, tempFile);

}

});

callList.add(call);

}

.................省略..............

}

就是根据临时文件保存的断点信息发起childTaskCount数量的异步请求,如果响应成功则通过startSaveRangeFile()方法分段保存文件:

private void startSaveRangeFile(Response response, int index, Ranges range, File saveFile, File tempFile) {

.................省略..............

try {

saveRandomAccessFile = new RandomAccessFile(saveFile, "rws");

saveChannel = saveRandomAccessFile.getChannel();

MappedByteBuffer saveBuffer = saveChannel.map(READ_WRITE, range.start[index], range.end[index] - range.start[index] + 1);

tempRandomAccessFile = new RandomAccessFile(tempFile, "rws");

tempChannel = tempRandomAccessFile.getChannel();

MappedByteBuffer tempBuffer = tempChannel.map(READ_WRITE, 0, TEMP_FILE_TOTAL_SIZE);

inputStream = response.body().byteStream();

int len;

byte[] buffer = new byte[BUFFER_SIZE];

while ((len = inputStream.read(buffer)) != -1) {

//取消

if (IS_CANCEL) {

handler.sendEmptyMessage(CANCEL);

callList.get(index).cancel();

break;

}

saveBuffer.put(buffer, 0, len);

tempBuffer.putLong(index * EACH_TEMP_SIZE, tempBuffer.getLong(index * EACH_TEMP_SIZE) + len);

onProgress(len);

//退出保存记录

if (IS_DESTROY) {

handler.sendEmptyMessage(DESTROY);

callList.get(index).cancel();

break;

}

//暂停

if (IS_PAUSE) {

handler.sendEmptyMessage(PAUSE);

callList.get(index).cancel();

break;

}

}

addCount();

} catch (Exception e) {

onError(e.toString());

} finally {

.................省略..............

}

在while循环中进行目前文件的写入和将当前下载到的位置保存到临时文件:

saveBuffer.put(buffer, 0, len);

tempBuffer.putLong(index * EACH_TEMP_SIZE, tempBuffer.getLong(index * EACH_TEMP_SIZE) + len);

同时调用onProgress()方法将进度发送出去,其中取消、退出保存记录、暂停需要中断while循环。

因为下载是在子线程进行的,但我们一般需要在UI线程根据下载状态来更新UI,所以我们通过Handler将下载过程的状态数据发送到UI线程:即调用handler.sendEmptyMessage()方法。

最后FileTask类还有一个saveCommonFile()方法,即进行不支持断点续传的普通下载。

前边我们提到了通过Handler将下载过程的状态数据发送到UI线程,接下看下ProgressHandler类基本的处理:

private Handler mHandler = new Handler() {

@Override

public void handleMessage(Message msg) {

super.handleMessage(msg);

switch (mCurrentState) {

case START:

break;

case PROGRESS:

break;

case CANCEL:

break;

case PAUSE:

break;

case FINISH:

break;

case DESTROY:

break;

case ERROR:

break;

}

}

};

在handleMessage()方法中,我们根据当前的下载状态进行相应的操作。

如果是START则需要将下载数据插入数据库,执行初始化回调等;如果是PROGRESS则执行下载进度回调;如果是CANCEL则删除目标文件、临时文件、数据库记录并执行对应回调等;如果是PAUSE则更新数据库文件记录并执行暂停的回调等;如果是FINISH则删除临时文件和数据库记录并执行完成的回调;如果是DESTROY则代表直接在Activity中下载,退出Activity则会更新数据库记录;最后的ERROR则对应出错的情况。具体的细节可参考源码。

最后在DownloadManger类里使用线程池执行下载操作:

ThreadPool.getInstance().getThreadPoolExecutor().execute(fileTask);

//如果正在下载的任务数量等于线程池的核心线程数,则新添加的任务处于等待状态

if (ThreadPool.getInstance().getThreadPoolExecutor().getActiveCount() == ThreadPool.getInstance().getCorePoolSize()) {

downloadCallback.onWait();

}

以及判断新添加的任务是否处于等待的状态,方便在UI层处理。到这里核心的实现原理就完了,更多的细节可以参考源码。

如何使用:

DownloadManger是个单例类,在这里封装在了具体的使用操作,我们可以根据url进行下载的开始、暂停、继续、取消、重新开始、线程池配置、https证书配置、查询数据的记录数据、获得当前某个下载状态的数据:

开始一个下载任务我们可以通过三种方式来进行:

1、通过DownloadManager类的start(DownloadData downloadData, DownloadCallback downloadCallback)方法,data可以设置url、保存路径、文件名、子任务数量:

2、先执行DownloadManager类的setOnDownloadCallback(DownloadData downloadData, DownloadCallback downloadCallback)方法,绑定data和callback,再执行start(String url)方法。

3、链式调用,需要通过DUtil类来进行:例如

DUtil.init(mContext)

.url(url)

.path(Environment.getExternalStorageDirectory() + "/DUtil/")

.name(name.xxx)

.childTaskCount(3)

.build()

.start(callback);

start()方法会返回DownloadManager类的实例,如果你不关心返回值,使用DownloadManger.getInstance(context)同样可以得到DownloadManager类的实例,以便进行后续的暂停、继续、取消等操作。

关于callback可以使用DownloadCallback接口实现完整的回调:

new DownloadCallback() {

//开始

@Override

public void onStart(long currentSize, long totalSize, float progress) {

}

//下载中

@Override

public void onProgress(long currentSize, long totalSize, float progress) {

}

//暂停

@Override

public void onPause() {

}

//取消

@Override

public void onCancel() {

}

//下载完成

@Override

public void onFinish(File file) {

}

//等待

@Override

public void onWait() {

}

//下载出错

@Override

public void onError(String error) {

}

}

也可以使用SimpleDownloadCallback接口只实现需要的回调方法。

暂停下载中的任务:pause(String url)

继续暂停的任务:resume(String url)     ps:不支持断点续传的文件无法进行暂停和继续操作。

取消任务:cancel(String url),可以取消下载中、或暂停的任务。

重新开始下载:restart(String url),暂停、下载中、已取消、已完成的任务均可重新开始下载。

下载数据保存:destroy(String url)、destroy(String... urls),如在Activity中直接下载,直接退出时可在onDestroy()方法中调用,以保存数据。

配置线程池:setTaskPoolSize(int corePoolSize, int maxPoolSize),设置核心线程数以及总线程数。

配置okhttp证书:setCertificates(InputStream... certificates)在数据库查询单个数据DownloadData getDbData(String url),查询全部数据:List getAllDbData()

ps:数据库不保存已下载完成的数据

获得下载队列中的某个文件数据:DownloadData getCurrentData(String url)到这里基本的就介绍完了,更多的细节和具体的使用都在demo中,不合理的地方还请多多指教哦。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值