OkHttp3/EventBus 实现断点续传/下载

cover.png

断点续传/下载,在网络情况不好的时候,可以在断开连接以后,仅继续获取部分内容。假如手机在下载文件的时候下载了80%,某些原因断网了,如果不支持范围请求,那就只有被迫重头开始下载。但是如果有范围请求的加持,就只需要下载最后 5% 的资源,避免重新下载。

记录 App更新的几个主要功能模块,包含

  • Apk文件下载和断点续传
  • Apk安装,需要兼容 android 7.0
  • Android 8.0 未知权限授权
  • 拓展

基本原理

java.io.RandomAccessFile

断点续传/下载需要使用到 java.io.RandomAccessFile 类,RandomAccessFile 的实例支持读取和写入随机访问文件,它也可以 seek(long pos) 设置从此文件的开头开始测量的文件指针偏移量,在该位置进行下一次读取或写入操作。简单点说就是可以通过seek(long pos)方法跳过pos字节开始写入字节。

HTTP 的范围请求

是否支持范围请求

同时还需要,在HTTP/1.1中,可以声明了一个响应头部 Access-Ranges 来标记是否支持范围请求,它只有一个可选参数 bytes,
HTTP/1.1.png
例如这里给了一个下载APK的响应头,可以看到它是有 Accept-Ranges:bytes来标记的,有此 标记标识当前资源支持范围请求。

使用范围请求

如果已经确定双端都支持范围请求,我们就可以在请求资源的时候使用它。

所有的文件最终都是存储在磁盘或者内存中的字节,对于待操作的文件可以将其以字节为单位分割。这样只需要 HTTP 支持请求该文件从 n 到 n+x 这个范围内的资源,就可以实现范围请求了。

HTTP/1.1 中定义了一个 Ranges 的请求头,来指定请求实体的范围。它的范围取值是在 0-Content-Length 之间,使用 - 分割。。

例如已经下载了 1000 bytes 的资源内容,想接着继续下载之后的资源内容,只要在 HTTP 请求头部,增加 Ranges:bytes=1000- 就可以了。
Range 还有几种不同的方式来限定范围,可以根据需要灵活定制:

  • 500-1000:指定开始和结束的范围,一般用于多线程下载。
  • 500- :指定开始区间,一直传递到结束。这个就比较适用于断点续传、或者在线播放等等。
  • -500:无开始区间,只意思是需要最后 500 bytes 的内容实体。
  • 100-300,1000-3000:指定多个范围,这种方式使用的场景很少,了解一下就好了。

来通过一个实例来验证一下

断点续传/下载

请求指定范围数据

几个地方为重点,网络请求添加了一个.addHeader("Range", "bytes=$currentFileLength-"),currentFileLength为当前文件字节大小。比如当前需要下载的文件大小为1000个字节,.addHeader("Range", "bytes=500-")表示只请求服务器500-1000的字节,后续回调中的response.body().contentLength()大小为 500

val request = Request.Builder()
                .addHeader("Range", "bytes=$currentFileLength-")
                .url(apkUrl)
                .build()
// 创建RandomAccessFile实例,并设置跳过currentFileLength个字节
val randomAccessFile = RandomAccessFile(apkFile, "rw")
randomAccessFile.seek(currentFileLength)
// apkFile为下载文件的绝对路径

数据写入

var inputStream: InputStream? = null
var bufferedInputStream: BufferedInputStream? = null
var randomAccessFile: RandomAccessFile? = null
try {
   
    inputStream = responseBody.byteStream()
    bufferedInputStream = BufferedInputStream(inputStream, size)

    // 创建RandomAccessFile实例,并设置跳过currentFileLength个字节
    randomAccessFile = RandomAccessFile(apkFile, "rw")
    randomAccessFile.seek(currentFileLength)

    var len = 0
    while (bufferedInputStream.read(buffer).apply {
    len = this } != -1) {
   
      randomAccessFile.write(buffer, 0, len)
      currentFileLength += len.toLong()
      // 通过EventBus回调下载进度
      callbackProgress(currentFileLength, totalLength)
    }
    // 通过EventBus回调下载进度
    callbackProgress(currentFileLength, totalLength)
    Log.d(TAG, "下载完成: ${
     apkFile.absoluteFile}")
} catch (e: IOException) {
   
    e.printStackTrace()
    Log.e(TAG, e.message, e)
} finally {
   
    bufferedInputStream?.close()
    inputStream?.close()
    randomAccessFile?.close()
    responseBody.close()
    call.cancel()
}

回调数据

/**
 * 通过EventBus回调下载进度
 */
private fun callbackProgress(currentLength: Long, totalLength: Long) {
   
    val progress = (currentLength * 1.0f / totalLength * 100).toInt()

    val hasSubscriberEvent = EventBus.getDefault().hasSubscriberForEvent(Progress::class.java)
    if (hasSubscriberEvent) {
   
      EventBus.getDefault().post(Progress(progress, currentLength, totalLength))
    }
    Log.d(TAG, "--->>> progress = $progress  $currentLength / $totalLength")
}

// 在需要显示进度的地方,通过EventBus注册和反注册
EventBus.getDefault().register(this)
EventBus.getDefault().unregister(this)

// 接收回调
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventDownloadProgress(progress: Progress) {
   
    progress_bar.progress = progress.progress
    progress_text.text = "${
     progress.progress}%"
}

// Progress实体类
data class Progress(val progress: Int, val currentLength: Long, val totalLength: Long)

权限配置

首先一些必要权限需要添加在AndroidManifest.xml中,在Android 6.0之后一些危险权限需要动态申请,Android 8.0还有一个安装未知应用权限需要请求打开。Android 7.0后 SDCard文件访问需要在AndroidManifest.xml配置<provider>......</provider>,需要在res/目录下新创建xml目录

// 分别是 网络、SDCard读、SDCard写、API
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${
     applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

// file_paths.xml,添加到 res/xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="external-path"
        path="." />

    <external-cache-path
        name="external_cache"
        path="." />

    <cache-path
        name="cache"
        path="." />

    <files-path
        name="files_path"
        path="." />

    <external-files-path
        name="external_files_path"
        path="." />

</paths>
interface InstallPermissionCallBack {
   
    // 已授权
    fun onGranted()
    // 未授权
    fun onDenied()
}

/**
 * 检查权限
 */
fun checkInstallPermission(context: Context, callback: InstallPermissionCallBack) {
   
    if (hasInstallPermission
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值