【Kotlin】OkHttp框架实现网络下载


不积跬步,无以至千里;不积小流,无以成江海。要沉下心来,诗和远方的路费真的很贵!

【Kotlin】OkHttp框架实现网络下载

需求

  1. 对网络上的资源进行下载,下载的文件保存在本地mnt/sdcard/Download文件夹。

  2. 显示下载的进度和下载是否成功的提示。

  3. 多线程下载,一个线程下载一张图片或者一个视频。

  4. 只有下载完成后,才可以显示和播放。

思路

  • 总共分为三步:

    1. 检查权限。无权限,则进行权限申请;有权限,进行下一步。

    2. 有权限后,获取到网络资源,形成流文件。

    3. 将流文件写入磁盘,保存到本地。

实现

实现单线程下载功能

  1. Manifest文件中加入权限。
<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  1. 配置网络请求资源的路径。
package com.example.appvideo

class Config {
    object API {
        const val URL_GET_DOWNLOAD_FILE = "https://img-blog.csdnimg.cn/20200614120050920.JPG"
        const val URL_GET_DOWNLOAD_FILE1 = "https://img-blog.csdnimg.cn/20200616204912371.JPG"
        const val URL_GET_DOWNLOAD_FILE2 = "https://img-blog.csdnimg.cn/20200614120120405.JPG"
        const val URL_GET_DOWNLOAD_FILE3 = "https://img-blog.csdnimg.cn/20200614120120401.JPG"
        const val URL_GET_DOWNLOAD_FILE4 = "https://img-blog.csdnimg.cn/2020061412003655.JPG"
        const val URL_GET_DOWNLOAD_FILE5 = "https://img-blog.csdnimg.cn/20200614115943345.JPG"
    }
}
  1. 界面布局文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn_start_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始下载"/>
    <TextView
        android:id="@+id/tv_result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="准备开始下载"/>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:max="100" />
</LinearLayout>
  1. 单线程下载逻辑类。
package com.example.appvideo

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Message
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import kotlinx.android.synthetic.main.activity_download.*
import okhttp3.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream

/**
 * 下载文件界面
 */
class DownloadActivity : AppCompatActivity() {
    companion object {
        //静态常量权限码
        private const val REQUEST_EXTERNAL_STORAGE = 101

        //静态数组,具体权限
        private val PERMISSIONS_STORAGE = arrayOf(
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE"
        )

        //下载状态码
        private const val DO_DOWNLOADING = 0
        private const val DOWNLOAD_SUCCESS = 1
        private const val DOWNLOAD_FAILED = -1
    }

    //网络请求客户端驱动
    private var okHttpClient: OkHttpClient? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)
        //创建网络连接驱动
        okHttpClient = OkHttpClient()
        //按钮点击事件
        btn_start_download.setOnClickListener {
            //点击按钮开始下载
            startDownLoad()
        }
    }

    /**
     * 用于显示下载状态
     */
    var handler: Handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                //失败或成功
                DOWNLOAD_FAILED, DOWNLOAD_SUCCESS -> {
                    //打印消息,提示用户
                    val result = msg.obj as String
                    tv_result?.text = result
                }
                //下载中
                DO_DOWNLOADING -> {
                    //显示进度信息
                    progressBar.progress = msg.obj as Int
                    val progress = "已下载" + msg.obj + "%"
                    tv_result?.text = progress
                }
            }
        }
    }

    /**
     * 检查是否有读写权限
     * 无则申请权限,有则进行具体操作
     */
    private fun checkPermission() {
        // 检查权限  是否被授权
        // PackageManager.PERMISSION_GRANTED表示已授权过
        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                Toast.makeText(this, "请开通相关权限,否则无法正常使用本应用!", Toast.LENGTH_SHORT).show()
            }
            //申请权限  参数1.context 2.具体权限 3.权限码
            ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE)
        } else {
            Toast.makeText(this, "已授权!", Toast.LENGTH_SHORT).show()
            //有权限再获取资源,否则获取也无法下载到本地
            //第二步,获取资源
            getResourceFromInternet(Config.API.URL_GET_DOWNLOAD_FILE1)
        }
    }

    /**
     * 权限的回调方法
     */
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<out String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            REQUEST_EXTERNAL_STORAGE -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, "授权成功!", Toast.LENGTH_SHORT).show()
                    //授权成功后,再进行下载功能
                    getResourceFromInternet(Config.API.URL_GET_DOWNLOAD_FILE3)
                } else {
                    //权限被拒绝,则提示,不进行任何操作
                    Toast.makeText(this, "授权被拒绝!", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    /**
     * 下载功能具体实现
     */
    private fun startDownLoad() {
        //第一步,检查权限
        checkPermission()
    }

    /**
     * 从网络上获取资源
     */
    private fun getResourceFromInternet(path: String) {
        //创建请求
        val request = Request.Builder()
            .url(path)
            .build()
        //异步执行请求
        okHttpClient?.newCall(request)?.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                //请求失败,提示下载失败信息
                val msg = handler.obtainMessage(DOWNLOAD_FAILED, "下载失败")
                handler.sendMessage(msg)
            }

            override fun onResponse(call: Call, response: Response) {
                //请求成功,获取到资源
                //第三步,保存到本地
                writeToSDCard(response)
            }
        })
    }

    /**
     * 将文件写入SD卡来保存
     */
    private fun writeToSDCard(response: Response) {
        //决定存放路径
        // 1.随着app的消失而消失,外部存储  在mnt/sdcard/Android中
       /* val savePath = getExternalFilesDir(null).toString() + File.separator
        val fileName = "wj.jpg"
        val file = File(save_Path,fileName)*/
        // 2.SD卡 不会随着app消失而消失,内部存储
        val savePath = Environment.getExternalStorageDirectory().absolutePath + "/Download/"
        //文件夹
        val dir = File(savePath)
        //文件夹不存在则创建
        if (!dir.exists()) {
            dir.mkdirs()
        }
        //连接字符串,形成保存的文件名
        val sb = StringBuilder()
        sb.append(System.currentTimeMillis()).append(".jpg")
        val fileName = sb.toString()
        //创建文件
        val file = File(dir, fileName)
        //输入输出流
        var inputStream: InputStream? = null
        var fileOutputStream: FileOutputStream? = null
        //读取到磁盘速度
        val fileReader = ByteArray(4096)
        //文件资源总大小
        val fileSize = response.body()!!.contentLength()
        //当前下载的资源大小
        var sum: Long = 0
        //获取资源
        inputStream = response.body()?.byteStream()
        //文件输出流
        fileOutputStream = FileOutputStream(file)
        //读取的长度
        var read: Int
        while (inputStream?.read(fileReader).also { read = it!! } != -1) {
            //写入本地文件
            fileOutputStream.write(fileReader, 0, read)
            //获取当前进度
            sum += read.toLong()
            Log.e("msg", "downloaded $sum of $fileSize")
            val progress = (sum * 1.0 / fileSize * 100).toInt()
            //发送进度消息
            val msg = handler.obtainMessage(DO_DOWNLOADING, progress)
            handler.sendMessage(msg)
        }
        //结束后,刷新清空文件流
        fileOutputStream.flush()
        //结束后,发送下载成功信息
        val msg = handler.obtainMessage(DOWNLOAD_SUCCESS, "下载成功")
        handler.sendMessage(msg)
        //最后关闭流,防止内存泄露
        inputStream?.close()
        fileOutputStream?.close()
    }
}

实现多线程下载功能

package com.example.appvideo.download

import android.content.Context
import android.os.Environment
import android.os.Looper
import android.util.Log
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicInteger

/**
 * @author lvjunkai
 * @DATE 2022/9/5
 * @TIME 9:51
 * @Description 多线程下载类
 */
class DownloadByManyThread {
    //请求资源网址
    private var url: String? = null

    //连接超时时间
    private var connectionTimeout: Int

    //请求方法
    private var method: String?
    private var range: Int

    //上下文环境
    private var context: Context

    //文件夹路径
    private val cachePath = "imgs"

    //连接对象
    private var connection: HttpURLConnection? = null

    //文件格式
    private var suffix: String? = null

    /**
     * 构造函数
     * @param context
     * @param suffix
     */
    constructor(context: Context, suffix: String?) {
        connectionTimeout = 500 // 500毫秒
        method = "GET"
        range = 0
        this.context = context
        this.suffix = suffix
    }

    /**
     * 获取网址
     * @param url
     * @return
     */
    fun url(url: String?): DownloadByManyThread {
        this.url = url
        return this
    }

    /**
     * 建造者设计模式
     * @param builder
     */
    constructor(builder: Builder) {
        url = builder.url
        connectionTimeout = builder.connectionTimeout
        method = builder.method
        range = builder.range
        context = builder.context
    }

    /**
     * 建造者对象
     */
    class Builder(val context: Context) {
        var url: String? = null
        var connectionTimeout = 0
        var method: String? = null
        var range = 0

        fun url(url: String?): Builder {
            this.url = url
            return this
        }

        fun timeout(ms: Int): Builder {
            connectionTimeout = ms
            return this
        }

        /**
         * 只能GET或者POST方法
         * @param method
         * @return
         */
        fun method(method: String): Builder {
            if (!(method.toUpperCase() == "GET" || method.toUpperCase() == "POST")) {
                throw AssertionError("Assertion failed")
            }
            this.method = method
            return this
        }

        fun start(range: Int): Builder {
            this.range = range
            return this
        }

        /**
         * 初始化build方法
         * @return
         */
        fun build(): DownloadByManyThread {
            return DownloadByManyThread(this)
        }
    }

    /**
     * 下载结果回调接口
     */
    private interface DownloadListener {
        fun onSuccess(file: File?)
        fun onError(msg: String?)
    }

    /**
     * 静态内部类 下载线程类
     */
    private class DownloadThread(private val url: String?, private val startPos: Long, private val endPos: Long, private val maxFileSize: Long, private val file: File) : Thread() {
        private var randomAccessFile: RandomAccessFile? = null
        private val connectionTimeout = 5 * 1000 // 5秒钟
        private val method = "GET"
        private var listener: DownloadListener? = null

        fun setDownloadListener(listener: DownloadListener?) {
            this.listener = listener
        }

        /**
         * 线程执行调用方法
         */
        override fun run() {
            Log.e(TAG, "=========> " + currentThread().name)
            var connection: HttpURLConnection? = null
            var url_c: URL? = null
            try {
                randomAccessFile = RandomAccessFile(file, "rwd")
                randomAccessFile!!.seek(startPos)
                url_c = URL(url)
                connection = url_c.openConnection() as HttpURLConnection
                connection.connectTimeout = connectionTimeout
                connection.requestMethod = method
                connection.setRequestProperty("Charset", "UTF-8")
                connection.setRequestProperty("accept", "*/*")
                connection.setRequestProperty("Range", "bytes=$startPos-$endPos")
                Log.e(TAG, "Range: bytes=$startPos-$endPos")
                val inputStream = connection.inputStream
                Log.e(TAG, "connection.getContentLength() == " + connection.contentLength)
                val contentLength = connection.contentLength
                if (contentLength < 0) {
                    Log.e(TAG, "Download fail!")
                    return
                }
                try {
                    if (connection.responseCode == 206) {
                        val buffer = ByteArray(2048)
                        var len = -1
                        while (inputStream!!.read(buffer).also { len = it } != -1) {
                            randomAccessFile!!.write(buffer, 0, len)
                        }
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                } finally {
                    try {
                        inputStream?.close()
                    } catch (e: IOException) {
                        e.printStackTrace()
                    }
                }
            } catch (e: IOException) {
                Log.e(TAG, "Download bitmap failed.", e)
                if (listener != null) listener!!.onError(e.localizedMessage)
                e.printStackTrace()
            } finally {
                connection?.disconnect()
                // todo 通知下载完毕
                if (endPos == maxFileSize) {
                    Log.e(TAG, "Download bitmap success.")
                    if (listener != null) listener!!.onSuccess(file)
                }
            }
        }

    }

    private var executor: Executor? = null
    fun download() {
        val path = buildPath(cachePath)
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw RuntimeException("Can't visit network from UI thread.")
        }
        try {
            val url1 = URL(url)
            connection = url1.openConnection() as HttpURLConnection
            connection!!.connectTimeout = connectionTimeout
            connection!!.requestMethod = method
            connection!!.setRequestProperty("Charset", "UTF-8")
            connection!!.setRequestProperty("accept", "*/*")
            connection!!.connect()
            val contentLength = connection!!.contentLength
            if (contentLength < 0) {
                Log.e(TAG, "Download fail!")
                return
            }

            // TODO 分为多个线程,进行执行
            val step = contentLength / maximumPoolSize
            Log.e(TAG, "maximumPoolSize: $maximumPoolSize , step:$step")
            Log.e(TAG, "contentLength: $contentLength")
            val sb = StringBuilder()
            sb.append(System.currentTimeMillis()).append(suffix)
            val file = File(path, sb.toString())
            if (contentLength.toLong() == file.length()) {
                Log.e(TAG, "Nothing changed!") // already downlaod.
                return
            }
            // 否则就下载
            for (i in 0 until maximumPoolSize) {
                if (i != maximumPoolSize - 1) {
                    val downloadThread = DownloadThread(url, (i * step).toLong(), ((i + 1) * step - 1).toLong(), contentLength.toLong(), file)
                    executor!!.execute(downloadThread)
                } else {
                    val downloadThread = DownloadThread(url, (i * step).toLong(), contentLength.toLong(), contentLength.toLong(), file)
                    downloadThread.setDownloadListener(object : DownloadListener {
                        override fun onSuccess(file: File?) {
                            Log.e(TAG, "onSuccess: ")
                        }

                        override fun onError(msg: String?) {
                            Log.e(TAG, "onError: ")
                        }
                    })
                    executor!!.execute(downloadThread)
                }
            }
        } catch (e: IOException) {
            Log.e(TAG, "Download bitmap failed.", e)
            e.printStackTrace()
        } finally {
            if (connection != null) connection!!.disconnect()
        }
    }

    private fun buildPath(filePath: String): File {
        // 是否有SD卡
        val flag = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
        // 如果有SD卡就存在外存,否则就位于这个应用的data/package name/cache目录下
        val cachePath: String
        cachePath = if (flag) context.externalCacheDir!!.path else context.cacheDir.path
        val directory = File(cachePath + File.separator + filePath)
        // 目录不存在就创建
        if (!directory.exists()) directory.mkdirs()
        return directory
    }

    companion object {
        //标志
        private const val TAG = "Downloader"

        //运行线程数量
        private val corePoolSize = Runtime.getRuntime().availableProcessors() + 1

        //同时运行的最大线程数量
        private val maximumPoolSize = Runtime.getRuntime().availableProcessors() * 2 + 1
        private val mThreadFactory: ThreadFactory = object : ThreadFactory {
            private val mCount = AtomicInteger(1)
            override fun newThread(r: Runnable): Thread {
                return Thread(r, "Thread#" + mCount.getAndIncrement())
            }
        }
    }

    init {
        executor = ThreadPoolExecutor(corePoolSize, maximumPoolSize, 10L,
                TimeUnit.SECONDS,
                LinkedBlockingDeque(),
                mThreadFactory)
    }
}
 //多线程下载文件
            val url2 = "https://img-blog.csdnimg.cn/20200614120050920.JPG"

            Thread(Runnable {
                val downloader = DownloadByManyThread(this@DownloadActivity, ".jpg")
                downloader.url(url2).download()
            }).start()

版权声明:本文为qq_46546793原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。