基于Retrofit2实现的LycheeHttp-多任务下载的实现

1,052 阅读6分钟

一个请求库,除了请求,上传功能,还需要一个下载的功能。而且下载功能很常用,最常用的就是App的更新了,作为一个下载器,断点续传也是必不可少的。

github地址:github.com/VipMinF/Lyc…

本库其他相关文章

框架引入

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读取就可以了。


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的使用。

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如何区分这是下载还是普通请求,总不能普通请求返回的数据也给我拦截了吧。对于这一点,只需要自定义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
        }
    }
}

之后就是下载时的数据读取和回调速度计算了。

    /**
     *
     *  使用这个方法读取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)
        }

后话:第一次写文章,写的头晕脑涨,写的不太好。如果这篇文章对各位大大有用的话,可以给我点个赞鼓励一下我哦,感谢!