Kotlin协程的使用与封装

前言:

相信作为一个现代Android开发者,应该都用过或者听过协程了,还不少大佬都已经把协程给扒皮了,其本质就是线程池的封装。源码的解析和性能的对比,都有解说。

协程的性能还线程和进程的区别是什么不如原生线程池,为什么我要用协程,是因为协程可以线程池把碎片化的方法很方便的加入异步处理,发挥Android设备多核的优势。合理的使用协程,应用反而更流畅。

下面我就不涉及太多原理理论了,直接上代码,Android中服务器内存和台式机内存区别如何使用和封装协程。

一. 协程的使用

常用服务器内存条可以用在台式机上吗的几个关键的函数方法

launch ,runB携程电话locking, withContext ,async/await

前两者启动协程,后两者调度线程。

lauch 是http代理非阻塞的 而runBlocking是阻塞的。直接上例子:

   private fun testCoroutine1() {
       //这里只是协程作用域
      // GlobalScope.launch { 
      // lifecycleScope.launch { 
      // viewModelScope.launch { 都可以
        CoroutineScope(Dispatchers.Main).launch {
            delay(500)
            YYLogUtils.w("协程1作用域内部执行")
        }
        YYLogUtils.w("协程1作用域wai部执行")
    }

结果是先执行外部,再执行内部

而runBlocking恰恰相反

  private fun testCoroutine2() {
        runBlocking {
            delay(500)
            YYLogUtils.w("协程2作用域内部执行")
        }
        YYLogUtils.w("协程2作用域wai部执行")
    }

结果是先执行内部,再执行外部,因为阻塞了。

所以一般我们开发绝大多数都是使用launch了。
而我们切换线程appetite一般用wit携程电话hContext和 async/await.区别就是你想顺序执行还是并发执行。

顺序执行:
这里会先等待1秒输入1234,然后调用接口获取Industry,线程安全请求完成之后再调用接口获取School,当前全部完成之后隐藏Loading。
其中网络请求异常的处理已经在内部封装处理了,后面会讲到。

       viewModelScope.launch {
                //开始Loading
                loadStartProgress()
                val startTimeStamp = System.currentTimeMillis()
                val res = withContext(Dispatchers.Default) {
                    //异步执行
                    delay(1000)
                    return@withContext "1234"
                }
                val endTimeStamp = System.currentTimeMillis()
                YYLogUtils.w("res: $res  time: ${endTimeStamp-startTimeStamp}")
                //网络请求获取行业数据
                val industrys = mRepository.getIndustry()
                //返回的数据是封装过的,检查是否成功
                industrys.checkResult({
                    //成功
                    _industryLD.postValue(it)
                }, {
                    //失败
                    toastError(it)
                })
                //上面的请求执行完毕才会执行这个请求
                val schools = mRepository.getSchool()
                //返回的数据是封装过的,检查是否成功
                schools.checkSuccess {
                    _schoollLD.postValue(it)
                }
                //完成Loading
                loadHideProgress()
            }

并发执行:
这里会同线程和进程的区别是什么时调用Industry和School接口,等待两者都完成之后再展示UI。

        viewModelScope.launch {
                //开始Loading
                loadStartProgress()
                val industryResult = async {
                    mRepository.getIndustry()
                }
                val schoolResult = async {
                    mRepository.getSchool()
                }
                val localDBResult = async {
                    //loadDB()
                    YYLogUtils.w("thread:" + CommUtils.isRunOnUIThread())
                    delay(10000)
                }
                //一起处理数据
                val industry = industryResult.await()
                val school = schoolResult.await()
                //如果都成功了才一起返回
                if (industry is OkResult.Success && school is OkResult.Success) {
                    loadHideProgress()
                    _industryLD.postValue(industry.data!!)
                    _schoollLD.postValue(school.data!!)
                }
                YYLogUtils.e(localDBResult.await().toString() + "完成")
            }

大家开发App常用的两种方式都已经掌握了,还有一个不常用但是很重要的点,就是网络请求去重
场景:点击CheckBox调用接口是否开启通知,那么我们就要把用户推送id传给服务器。如果用户狂点CheckBox,那么我怎么请求网络?

常用的两种去重手段。一种是取消上一次的,另一种是队列排队一个一来。

老规矩直接上代码了:

    /**
     * 网络请求去重
     */
    private var controlledRunner = ControlledRunner<OkResult<List<Industry>>>()  //取消之前的
    private val singleRunner = SingleRunner()       //任务队列,排队,单独的
    fun netDuplicate() {
        viewModelScope.launch {
            //比较常用
            //取消上一次的,执行这一次的
            controlledRunner.cancelPreviousThenRun {
                return@cancelPreviousThenRun mRepository.getIndustry()
            }.checkSuccess {
                YYLogUtils.e("请求成功:")
                _industryLD.postValue(it)
            }
            //前一个执行完毕了,再执行下一个
//                singleRunner.afterPrevious {
//                    mMainRepository.getIndustry()
//                }.checkSuccess {
//                    YYLogUtils.e("测试重复的数据:" + it.toString())
//                }
        }
    }

控制器源码如下:

class SingleRunner {
    private val mutex = Mutex()
    /**
     * 加入到任务队列,前一个任务执行完毕再执行下一个任务
     */
    suspend fun <T> afterPrevious(block: suspend () -> T): T {
        mutex.withLock {
            return block()
        }
    }
}
class ControlledRunner<T> {
    private val activeTask = AtomicReference<Deferred<T>?>(null)
    suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
        activeTask.get()?.cancelAndJoin()
        return coroutineScope {
            val newTask = async(start = LAZY) {
                block()
            }
            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }
            val result: T
            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    activeTask.get()?.cancelAndJoin()
                    yield()
                } else {
                    result = newTask.await()
                    break
                }
            }
            result
        }
    }
    /**
     * 不执行新任务,返回上一个任务的结果
     */
    suspend fun joinPreviousOrRun(block: suspend () -> T): T {
        activeTask.get()?.let {
            return it.await()
        }
        return coroutineScope {
            val newTask = async(start = LAZY) {
                block()
            }
            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }
            val result: T
            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    val currentTask = activeTask.get()
                    if (currentTask != null) {
                        newTask.cancel()
                        result = currentTask.await()
                        break
                    } else {
                        yield()
                    }
                } else {
                    result = newTask.await()
                    break
                }
            }
            result
        }
    }
}

二. 网络请求协程的封装

Retrofit+协程的使用:
原理就是调用Retrofit方法,对它try-catch.得到的是approach网络请求错误信息,可以根据不同的Type类型。然后对R线程池面试题eappletrofit的返回结果再判断如果code不是200,那么就是Api错误(例如Tokhttpwatchen失效)。对错误和成果的结果做统一的封装返回给ViewMo协程del处理。

方式携程网上订票飞机一:
处理BaseRepository:

open class BaseRepository {
    //无异常处理 -> 一般不用这个,一旦报错会App崩溃
    suspend inline fun <T : Any> handleApiCall(call: suspend () -> BaseBean<T>): BaseBean<T> {
        return call.invoke()
    }
    /**
     * 推荐使用拓展函数extRequestHttp
     * 如果要使用Base里面的方法请求网络这么使用
     *   return handleErrorApiCall(call = {
                    handleApiErrorResponse()
                })
     * 都可以实现网络请求
     */
    //处理Http错误-内部再处理Api错误
    suspend fun <T : Any> handleErrorApiCall(call: suspend () -> OkResult<T>, errorMessage: String = ""): OkResult<T> {
        return try {
            call()
        } catch (e: Exception) {
            if (!TextUtils.isEmpty(errorMessage)) {
                OkResult.Error(IOException(errorMessage))
            } else {
                OkResult.Error(handleExceptionMessage(e))
            }
        }
    }
    //处理Api错误,例如403Token过期 把BaseBean的数据转换为自定义的Result数据
    suspend fun <T : Any> handleApiErrorResponse(
        response: BaseBean<T>,
        successBlock: (suspend CoroutineScope.() -> Unit)? = null,
        errorBlock: (suspend CoroutineScope.() -> Unit)? = null
    ): OkResult<T> {
        return coroutineScope {
            //执行挂起函数
            if (response.code == 200) {  //这里根据业务逻辑来 200 -1 等
                successBlock?.let { it() }
                OkResult.Success(response.data)
            } else {
                errorBlock?.let { it() }
                OkResult.Error(IOException(response.message))
            }
        }
    }
    //处理自定义错误消息
    fun handleExceptionMessage(e: Exception): IOException {
        return when (e) {
            is UnknownHostException -> IOException("Unable to access domain name, unknown domain name.")
            is JsonParseException -> IOException("Data parsing exception.")
            is HttpException -> IOException("The server is on business. Please try again later.")
            is ConnectException -> IOException("Network connection exception, please check the network.")
            is SocketException -> IOException("Network connection exception, please check the network.")
            is SocketTimeoutException -> IOException("Network connection timeout.")
            is RuntimeException -> IOException("Error running, please try again.")
            else -> IOException("unknown error.")
        }
    }
}

使用如下:

   suspend fun getServerTime(): OkResult<ServerTimeBean> {
        return handleErrorApiCall({
            handleApiErrorResponse(
                MainRetrofit.apiService.getServerTime(
                    Constants.NETWORK_CONTENT_TYPE,
                    Constants.NETWORK_ACCEPT_V1
                )
            )
        })
    }

方式二:
使用扩展方法的直接一携程网站官网步到位处理:

suspend fun <T : Any> BaseRepository.extRequestHttp(call: suspend () -> BaseBean<T>): OkResult<T> {
    //两种方式都可以,自用下面一种方式
//    runCatching {
//        call.invoke()
//    }.onSuccess { response: BaseBean<T> ->
//        if (response.code == 200) {
//            OkResult.Success(response.data)
//        } else {
//            OkResult.Error(ApiException(response.code, response.message))
//        }
//    }.onFailure { e ->
//        e.printStackTrace()
//        OkResult.Error(handleExceptionMessage(Exception(e.message, e)))
//    }
    return try {
        val response = call()
        if (response.code == 200) {
            OkResult.Success(response.data)
        } else {
            OkResult.Error(ApiException(response.code, response.message))
        }
    } catch (e: Exception) {
        e.printStackTrace()
        OkResult.Error(handleExceptionMessage(e))
    }
}

使用:

   suspend inline fun getIndustry(): OkResult<List<Industry>> {
        return extRequestHttp {
            DemoRetrofit.apiService.getIndustry(
                Constants.NETWORK_CONTENT_TYPE,
                Constants.NETWORK_ACCEPT_V1
            )
        }
    }

调用接口都是固定的模板代码,和之前MVP的方式一样,只需要定义Retrofit-Api的接口定http 500义就行。

源码在此。