多线程下载文件(Kotlin版本)

学习Kotlin有一段时间了,但是目前在公司用不上这个,所以打算自己平时写博客的时候多用Kotlin来写Demo。今天是使用Kotlin写了一个多线程下载的demo用来锻炼自己使用Kotlin开发的能力,开始撸代码。

一、效果图

多线程下载文件(Kotlin版本)_第1张图片

二、撸代码

package com.study.download.morecoroutinedownloaddemo

import android.content.pm.PackageManager
import android.os.AsyncTask
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.View
import android.widget.Toast
import com.study.download.morecoroutinedownloaddemo.utlis.PermissionCheckUtils
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.experimental.launch
import java.io.File
import java.io.RandomAccessFile
import java.net.HttpURLConnection
import java.net.URL

class MainActivity : AppCompatActivity() {

    var path = "http://192.168.31.105:8080/HotSpot/tomcat.avi"
    //协程数量
    var coroutineCount = 3
    var coroutineRunningCount = coroutineCount
    //权限数组
    var permissionArray = arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun downloadByCoroutine(view: View) {
        val size = PermissionCheckUtils.checkActivityPermissions(this, permissionArray, 100, null)
        if (size == 0) {
            //runBlocking {  } 以此种方式启动的协程仍旧运行于main线程中,所以联网操作不可以在此处完成
            //用于执行协程任务,通常用于启动最外层的协程
            startDownload()
        }

    }

    /**
     * 开始下载
     */
    private fun startDownload() {
        coroutineRunningCount = coroutineCount
        pb_1.progress = 0
        pb_2.progress = 0
        pb_3.progress = 0

        AsyncTask.THREAD_POOL_EXECUTOR.execute {  }
        //协程
//        async {
//            println("当前async的线程名称为:${Thread.currentThread().name}")
//            beforeDownload()
//        }
        //使用async{}.await()可以返回协程执行的结果
        //线程
//        Thread {
//            beforeDownload()
//        }.start()
        //线程池
        AsyncTask.THREAD_POOL_EXECUTOR.execute {
            beforeDownload()
        }
    }

    private fun beforeDownload() {
        println("当前线程名称为:${Thread.currentThread().name}")
        //创建与文件地址绑定的连接
        var url = URL(path)
        //打开文件连接对象
        val openConnection: HttpURLConnection = url.openConnection() as HttpURLConnection
        //设置请求方式
        openConnection.requestMethod = "GET"
        //设置连接时间
        openConnection.connectTimeout = 5000
        //获取网络连接的响应码
        val responseCode = openConnection.responseCode
        //响应码等于200,说明创建连接成功
        if (responseCode == 200) {
            //获取文件长度
            val contentLength = openConnection.contentLength
            println("获取到的文件长度为:$contentLength")
            //先创建一个跟将要下载文件大小相同的文件
            val sdPath = Environment.getExternalStorageDirectory().absolutePath
            //使用rw模式可以使用缓冲区,往硬盘中写数据
            val filePath = "$sdPath/tomcat.avi"
            var file = File(filePath)
            if (file.exists()) {
                file.delete()
            }
            var raf = RandomAccessFile(filePath, "rw")
            //设置创建文件的大小
            raf.setLength(contentLength.toLong())
            //这句话执行完成之后将会生成一个tomcat.avi文件,该文件的大小跟上一句赋值的长度一样大
            raf.close()

            var blockSize = contentLength / coroutineCount
            for (i in 0 until coroutineCount) {
                //开始下载的文件坐标
                var start = i * blockSize
                //下载截止的文件坐标
                var end = (i + 1) * blockSize - 1
                //最后一个线程下载截止的文件坐标
                if (i == coroutineCount - 1) {
                    end = contentLength - 1
                }
                println("当前协程的id$i,文件下载的起始坐标$start,文件下载的截止坐标$end")
                //启动相应协程,实现多协程下载
                //                    downloadByLaunch(start, end, sdPath, i)
                downloadByThread(start, end, sdPath, i)
            }
        }
    }

    private fun downloadByThread(start: Int, end: Int, sdPath: String?, coroutineId: Int) {
//        Thread {
//            reallyDownload(start, end, sdPath, coroutineId)
//        }.start()
        AsyncTask.THREAD_POOL_EXECUTOR.execute {
            reallyDownload(start, end, sdPath, coroutineId)
        }
    }

    private fun downloadByLaunch(start: Int, end: Int, sdPath: String?, coroutineId: Int) {
        launch {
            reallyDownload(start, end, sdPath, coroutineId)
        }
    }

    private fun reallyDownload(start: Int, end: Int, sdPath: String?, coroutineId: Int) {
        val name = Thread.currentThread().name
        Log.e("thread", name)
        //重新创建连接,进行指定位置下载
        //创建与文件地址绑定的连接
        var url = URL(path)
        //打开文件连接对象
        val openConnection: HttpURLConnection = url.openConnection() as HttpURLConnection
        //设置请求方式
        openConnection.requestMethod = "GET"
        //设置连接时间
        openConnection.connectTimeout = 5000
        //在请求头中封装客户端所需要的数据
        openConnection.setRequestProperty("range", "bytes=$start-$end")
        //获取网络连接的响应码
        val responseCode = openConnection.responseCode
        //此时返回206的响应码才算是成功的
        if (responseCode == 206) {
            var inputStream = openConnection.inputStream
            //创建文件
            var raf = RandomAccessFile("$sdPath/tomcat.avi", "rw")
            //跳到指定位置
            raf.seek(start.toLong())

            var len = -1
            //设定缓冲区
            var buf = ByteArray(1024)
            var flag = true
            var progress = 0
            while (flag) {
                //读取数据,返回下标
                len = inputStream.read(buf)
                //TODO 可以在此处记录下载文件的坐标,从而实现断点下载
                flag = len != -1
                //写数据
                if (flag) {
                    raf.write(buf, 0, len)
                    progress += len
                }

                when (coroutineId) {
                    0 -> {
                        pb_1.max = end - start
                        pb_1.progress = progress
                        println("当前正在运行的协程号码是$coroutineId,progress是$progress")
                    }
                    1 -> {
                        pb_2.max = end - start
                        pb_2.progress = progress
                        println("当前正在运行的协程号码是$coroutineId,progress是$progress")
                    }
                    2 -> {
                        pb_3.max = end - start
                        pb_3.progress = progress
                        println("当前正在运行的协程号码是$coroutineId,progress是$progress")
                    }
                }

            }
            raf.close()
            //同步锁,因为操作的是同一资源,为了避免操作错乱,所以此处的锁必须是唯一锁和同一锁
//            synchronized(MainActivity::class) {
//                coroutineRunningCount--
//                if (coroutineRunningCount == 0) {
//                    runOnUiThread {
//                        Toast.makeText(this@MainActivity, "下载完成", Toast.LENGTH_SHORT).show()
//                    }
//                } else {
//                    println("当前正在运行的协程号码是$coroutineRunningCount")
//                }
//            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        var flag = true
        for (i in grantResults) {
            //判断是否赋予权限的标记
            flag = if (i == PackageManager.PERMISSION_GRANTED) {
                flag && true
            } else {
                flag && false
            }
        }
        //如果权限赋予成功,那么久开始下载
        if (flag) {
            startDownload()
        }
    }
}


三、注意事项

在Demo开发的时候,经过测试发现会出现死锁的情况。这种情况在协程、线程以及使用线程池的时候都会出现。使用协程其实实质就是线程,因为通过打印日志会发现每个协程所在线程都不一致。至于死锁出现的原因应该是代码中为了弹出Toast设置的同步锁导致的,所以注释了该段代码就好了。

项目源码

你可能感兴趣的:(Kotlin技巧)