基於Retrofit2實現的LycheeHttp-多任務下載的實現

一個請求庫,除了請求,上傳功能,還須要一個下載的功能。並且下載功能很經常使用,最經常使用的就是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'
}
複製代碼

API的定義

@Download
    @GET("https://xxx/xxx.apk")
    fun download(): Call<DownloadBean>

    @GET
    @Download
    fun download(@Url url: String, @Header(RANGE) range: String): Call<DownloadBean>
複製代碼

API的使用

//普通下載
        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一個鏈。因此應該能夠想像這是一個鏈式處理,一層層深刻,詳細可看RealInterceptorChainide

處理過程

由上圖能夠得出,咱們只要在LoggingInterceptor以前處理Response就能夠了。因此,思路是 自定義一個ResponseBody返回給 LoggingInterceptor,這個ResponseBody裏面定義一個 CallBack,而後在 LoggingInterceptor中實現這個CallBack,等到下載完成就能夠通知 LoggingInterceptor讀取打印日誌,對於下載來講,固然只能打印頭部數據,由於ResponseBody中的數據已經被讀走了,可是下載只是打印頭部數據的日誌已經足夠了。只有一個 自定義一個ResponseBody如何區分這是下載仍是普通請求,總不能普通請求返回的數據也給我攔截了吧。對於這一點,只須要自定義 CoverFactoryresponseBodyConverter中處理。

/** * * 獲取真實的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用於斷點續傳。接下來,開始完成最後的斷點續傳。

斷點續傳的實現

斷點續傳,顧名思義就是記錄上次斷開的點,在下次新的請求的時候告訴服務器從哪裏開始下載。續傳的步驟

  1. 保存下載的進度,也就是上面的currLen
  2. 創建新的請求,在請求頭上設置Range:bytes=123-,123表示已經下載完成,須要跳過的字節。
  3. 服務器收到後會返回Content-Range:bytes 123-299/300的頭部
  4. 使用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)
        }
複製代碼

後話:第一次寫文章,寫的頭暈腦漲,寫的不太好。若是這篇文章對各位大大有用的話,能夠給我點個贊鼓勵一下我哦,感謝!

相關文章
相關標籤/搜索