一個請求庫,除了請求,上傳功能,還須要一個下載的功能。並且下載功能很經常使用,最經常使用的就是App的更新了,做爲一個下載器,斷點續傳也是必不可少的。java
github地址:github.com/VipMinF/Lyc…git
本庫其餘相關文章github
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'
}
複製代碼
@Download
@GET("https://xxx/xxx.apk")
fun download(): Call<DownloadBean>
@GET
@Download
fun download(@Url url: String, @Header(RANGE) range: String): Call<DownloadBean>
複製代碼
//普通下載
getService<API>().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<API>().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讀取就能夠了。app
open class ResponseCallBack<T>(private val handler: IResponseHandler<T>) : Callback<T> {
var call: Call<T>? = 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<T>, val file: RandomAccessFile, val handler: IResponseHandler<T>) :
ResponseCallBack<T>(handler) {
override fun onHandler(data: T) {
//這裏將data讀取出來 存進file文件中
super.onHandler(data)
}
}
複製代碼
看起來很簡單,實際上又是一把淚。發現等了很久纔將開始讀取,並且下載速度飛快,經調試發現,又是日誌那邊下載了,由於數據已經下載了,因此後面就沒下載的事,都是從緩存中讀取,因此速度飛快。若是把日誌去掉了,感受很不方便,並且也會致使普通的請求沒有日誌打印。想到第一個方法是,下載和普通請求從getService就開始區分開來,下載的去掉日誌,但這個方法不符合我封裝的框架,只好另外想辦法。框架
最終想到的辦法是,本身來實現日誌的功能。固然不是本身寫,先是把LoggingInterceptor
的代碼複製過來,而後在chain.proceed以後進行處理,爲啥要在這以後處理,先看看下OKHttp的Interceptor的使用。dom
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
。ide
自定義一個ResponseBody
返回給
LoggingInterceptor
,這個ResponseBody裏面定義一個
CallBack
,而後在
LoggingInterceptor
中實現這個CallBack,等到下載完成就能夠通知
LoggingInterceptor
讀取打印日誌,對於下載來講,固然只能打印頭部數據,由於ResponseBody中的數據已經被讀走了,可是下載只是打印頭部數據的日誌已經足夠了。只有一個
自定義一個ResponseBody
如何區分這是下載仍是普通請求,總不能普通請求返回的數據也給我攔截了吧。對於這一點,只須要自定義
CoverFactory
在
responseBodyConverter
中處理。
/** * * 獲取真實的ResponseCover,處理非下載狀況返回值的轉換 * 若是是Download註解的方法,則認爲這是一個下載方法 * */
override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
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<ResponseBody, Any>)
}
/** * 全部Response的body都通過這裏 * */
class CoreResponseCover(private val isDownloadMethodCover: Boolean, private val delegateResponseCover: Converter<ResponseBody, Any>) :
Converter<ResponseBody, Any> {
override fun convert(value: ResponseBody): Any {
.......
//非下載的狀況
if (!isDownloadMethodCover) {
(responseBody as? CoreResponseBody).also {
// 通知日誌讀取
it?.startRead()
it?.notifyRead()
}
return delegateResponseCover.convert(value)
} else {
//下載的狀況
return responseBody ?: value
}
}
}
複製代碼
以後就是下載時的數據讀取和回調速度計算了。post
/** * * 使用這個方法讀取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<DownloadBean>
複製代碼
封裝的思路是定義一個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<API>().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<TextView>(R.id.taskName).text = "任務:${item.id}"
helper.getView<TextView>(R.id.speed).text = "${item.getSpeed()}"
helper.getView<TextView>(R.id.progress).text = "${item.progress}%"
helper.getView<ProgressBar>(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)
}
複製代碼
後話:第一次寫文章,寫的頭暈腦漲,寫的不太好。若是這篇文章對各位大大有用的話,能夠給我點個贊鼓勵一下我哦,感謝!