一个请求库,除了请求,上传功能,还需要一个下载的功能。而且下载功能很常用,最常用的就是App的更新了,作为一个下载器,断点续传也是必不可少的。
github地址:github.com/VipMinF/Lyc…
本库其他相关文章
- 基于Retrofit2实现的LycheeHttp
- 基于Retrofit2实现的LycheeHttp-使用动态代理实现上传
框架引入
dependencies {
implementation 'com.vecharm:lycheehttp:1.0.2'
}
复制代码
如果你喜欢用RxJava 还需要加入
dependencies {
//RxJava
implementation 'com.vecharm.lycheehttp:lychee_rxjava:1.0.2'
//或者 RxJava2
implementation 'com.vecharm.lycheehttp:lychee_rxjava2:1.0.2'
}
复制代码
API的定义
@Download
@GET("https://xxx/xxx.apk")
fun download(): Call
@GET
@Download
fun download(@Url url: String, @Header(RANGE) range: String): Call
复制代码
API的使用
//普通下载
getService().download().request(File(App.app.externalCacheDir, "xx.apk")) {
onUpdateProgress ={fileName, currLen, size, speed, progress -> /*进度更新*/}
onSuccess = { Toast.makeText(App.app, "${it.downloadInfo?.fileName} 下载完成", Toast.LENGTH_SHORT).show() }
onErrorMessage={}
onCompleted={}
}
//断点续传
getService().download(url, range.bytesRange()).request(file) {
onUpdateProgress ={fileName, currLen, size, speed, progress -> /*进度更新*/}
onSuccess = { Toast.makeText(App.app, "${id}下载完成", Toast.LENGTH_LONG).show() }
onErrorMessage={}
onCompleted={}
}
复制代码
对与下载的API需要使用Download
进行注解,断点续传的需要添加@Header(RANGE)
参数。
实现流程
第一步,先实现最基本的下载功能,再去考虑多任务,断点续传。下载功能,比较容易实现,retrofit2.Callback::onResponse
中返回的ResponseBody读取就可以了。
open class ResponseCallBack<T>(private val handler: IResponseHandler) : Callback {
var call: Call? = null
override fun onFailure(call: Call<T>, t: Throwable) {
this.call = call
handler.onError(t)
handler.onCompleted()
}
override fun onResponse(call: Call<T>, response: Response<T>) {
this.call = call
try {
val data = response.body()
if (response.isSuccessful) {
if (data == null) handler.onError(HttpException(response)) else onHandler(data)
} else handler.onError(HttpException(response))
} catch (t: Throwable) {
handler.onError(t)
}
handler.onCompleted()
}
open fun onHandler(data: T) {
if (call?.isCanceled == true) return
if (handler.isSucceeded(data)) handler.onSuccess(data)
else handler.onError(data)
}
}
class DownloadResponseCallBack<T>(val tClass: Class, val file: RandomAccessFile, val handler: IResponseHandler) :
ResponseCallBack(handler) {
override fun onHandler(data: T) {
//这里将data读取出来 存进file文件中
super.onHandler(data)
}
}
复制代码
看起来很简单,实际上又是一把泪。发现等了好久才将开始读取,而且下载速度飞快,经调试发现,又是日志那边下载了,因为数据已经下载了,所以后面就没下载的事,都是从缓存中读取,所以速度飞快。如果把日志去掉了,感觉很不方便,而且也会导致普通的请求没有日志打印。想到第一个方法是,下载和普通请求从getService就开始区分开来,下载的去掉日志,但这个方法不符合我封装的框架,只好另外想办法。
最终想到的办法是,自己来实现日志的功能。当然不是自己写,先是把LoggingInterceptor
的代码复制过来,然后在chain.proceed之后进行处理,为啥要在这之后处理,先看看下OKHttp的Interceptor的使用。
class CoreInterceptor(private val requestConfig: IRequestConfig) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
·····
val response = chain.proceed(request)
···
return response
}
}
复制代码
像这样的Interceptor 我们可以添加很多个,参数是Interceptor.Chain
一个链。所以应该可以想像这是一个链式处理,一层层深入,详细可看RealInterceptorChain
。
处理过程
由上图可以得出,我们只要在LoggingInterceptor之前处理Response就可以了。所以,思路是自定义一个ResponseBody
返回给
LoggingInterceptor
,这个ResponseBody里面定义一个
CallBack
,然后在
LoggingInterceptor
中实现这个CallBack,等到下载完成就可以通知
LoggingInterceptor
读取打印日志,对于下载来说,当然只能打印头部数据,因为ResponseBody中的数据已经被读走了,但是下载只是打印头部数据的日志已经足够了。只有一个
自定义一个ResponseBody
如何区分这是下载还是普通请求,总不能普通请求返回的数据也给我拦截了吧。对于这一点,只需要自定义
CoverFactory
在
responseBodyConverter
中处理。
/**
*
* 获取真实的ResponseCover,处理非下载情况返回值的转换
* 如果是Download注解的方法,则认为这是一个下载方法
* */
override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter? {
var delegateResponseCover: Converter<*, *>? = null
retrofit.converterFactories().filter { it != this }.find {
delegateResponseCover = it.responseBodyConverter(type, annotations, retrofit); delegateResponseCover != null
}
return CoreResponseCover(annotations.find { it is Download } != null, delegateResponseCover as Converter)
}
/**
* 所有Response的body都经过这里
* */
class CoreResponseCover(private val isDownloadMethodCover: Boolean, private val delegateResponseCover: Converter) :
Converter {
override fun convert(value: ResponseBody): Any {
.......
//非下载的情况
if (!isDownloadMethodCover) {
(responseBody as? CoreResponseBody).also {
// 通知日志读取
it?.startRead()
it?.notifyRead()
}
return delegateResponseCover.convert(value)
} else {
//下载的情况
return responseBody ?: value
}
}
}
复制代码
之后就是下载时的数据读取和回调速度计算了。
/**
*
* 使用这个方法读取ResponseBody的数据
* */
fun read(callback: ProgressHelper.ProgressListener?, dataOutput: IBytesReader? = null) {
var currLen = rangeStart
try {
val fileName = downloadInfo?.fileName ?: ""
progressCallBack = object : CoreResponseBody.ProgressCallBack {
val speedComputer = ProgressHelper.downloadSpeedComputer?.newInstance()
override fun onUpdate(isExhausted: Boolean, currLen: Long, size: Long) {
speedComputer ?: return
if (speedComputer.isUpdate(currLen, size)) {
callback?.onUpdate(fileName, currLen, size, speedComputer.computer(currLen, size), speedComputer.progress(currLen, size))
}
}
}
startRead()
val source = source()
val sink = ByteArray(1024 * 4)
var len = 0
while (source.read(sink).also { len = it } != -1) {
currLen += dataOutput?.onUpdate(sink, len) ?: 0
//返回当前range用于断点续传
progressCallBack?.onUpdate(false, currLen, rangeEnd)
}
progressCallBack?.onUpdate(true, currLen, rangeEnd)
//通知日志读取,由于日志已经在上面消费完了,所以在只能获取头部信息
notifyRead()
} catch (e: java.lang.Exception) {
e.printStackTrace()
} finally {
Util.closeQuietly(source())
dataOutput?.onClose()
}
}
复制代码
在上面的回调中,返回了currLen
也就是range
用于断点续传。接下来,开始完成最后的断点续传。
断点续传的实现
断点续传,顾名思义就是记录上次断开的点,在下次新的请求的时候告诉服务器从哪里开始下载。续传的步骤
- 保存下载的进度,也就是上面的
currLen
- 建立新的请求,在请求头上设置
Range:bytes=123-
,123表示已经下载完成,需要跳过的字节。 - 服务器收到后会返回
Content-Range:bytes 123-299/300
的头部 - 使用
RandomAccessFile.seek(123)
的方式追加后面的数据
前面已经写完了基础的下载方式,断点续传只需要在进行一层封装。对于请求头加入range这个比较简单,在API定义的时候就可以做了。
@GET
@Download
fun download(@Url url: String, @Header(RANGE) range: String): Call
复制代码
封装的思路是定义一个Task
类用来保存下载的信息,比如下载路径
,文件名称
,文件大小
,已经下载的大小
,下载时间
,本次请求的ID
。
open class Task : Serializable {
val id = UUID.randomUUID()
var createTime = System.currentTimeMillis()
var range = 0L
var progress = 0
var fileName: String? = null
var fileSize = 0L
var url: String? = null
var filePath: String? = null
var onUpdate = { fileName: String, currLen: Long, size: Long, speed: Long, progress: Int ->
//保存进度信息
//保存文件信息
//通知UI更新
this.updateUI?.invoke() ?: Unit
}
open var updateUI: (() -> Unit)? = null
set(value) {
field = value
value?.invoke()
}
var service: Call<*>? = null
var isCancel = false
private set
fun cancel() {
isCancel = true
service?.cancel()
}
fun resume() {
if (!isCancel) return
url ?: return
filePath ?: return
isCancel = false
download(url!!, File(filePath))
}
fun cache() {
//todo 将任务信息保存到本地
}
fun download(url: String, saveFile: File) {
this.url = url
this.filePath = saveFile.absolutePath
if (range == 0L) saveFile.delete()
val file = RandomAccessFile(saveFile, "rwd").also { it.seek(range) }
service = getService().download(url, range.bytesRange()).request(file) {
onUpdateProgress = onUpdate
onSuccess = { Toast.makeText(App.app, "${id}下载完成", Toast.LENGTH_LONG).show() }
}
}
}
复制代码
UI更新的回调,item是上面定义的Task
item.updateUI = {
helper.getView(R.id.taskName).text = "任务:${item.id}"
helper.getView(R.id.speed).text = "${item.getSpeed()}"
helper.getView(R.id.progress).text = "${item.progress}%"
helper.getView(R.id.progressBar).also {
it.max = 100
it.progress = item.progress
}
}
复制代码
任务的请求
addDownloadTaskButton.setOnClickListener {
val downloadTask = DownloadTask()
val file = File(App.app.externalCacheDir, "xx${adapter.data.size + 1}.apk")
downloadTask.download("https://xxx.xxx.apk", file)
adapter.addData(downloadTask)
}
复制代码
后话:第一次写文章,写的头晕脑涨,写的不太好。如果这篇文章对各位大大有用的话,可以给我点个赞鼓励一下我哦,感谢!