当前位置: 首页 > news >正文

泰州免费网站建站模板wordpress判断熊掌号收录

泰州免费网站建站模板,wordpress判断熊掌号收录,wordpress产品图片,天津电力建设公司网站1 简介 Kotlin 中的协程提供了一种全新处理并发的方式#xff0c;您可以在 Android 平台上使用它来简化异步执行的代码。协程是从 Kotlin 1.3 版本开始引入#xff0c;但这一概念在编程世界诞生的黎明之际就有了#xff0c;最早使用协程的编程语言可以追溯到 1967 年的 Sim…1 简介 Kotlin 中的协程提供了一种全新处理并发的方式您可以在 Android 平台上使用它来简化异步执行的代码。协程是从 Kotlin 1.3 版本开始引入但这一概念在编程世界诞生的黎明之际就有了最早使用协程的编程语言可以追溯到 1967 年的 Simula 语言。 在过去几年间协程这个概念发展势头迅猛现已经被诸多主流编程语言采用比如 Javascript、C#、Python、Ruby 以及 Go 等。Kotlin 的协程是基于来自其他语言的既定概念。 在 Android 平台上协程主要用来解决两个问题: 处理耗时任务 (Long running tasks)这种任务常常会阻塞住主线程保证主线程安全 (Main-safety) 即确保安全地从主线程调用任何 suspend 函数。 特点一句话总结协程能更加安全实现异步代码同步化实质是对线程切换的封装 2 Kotlin协程创建 下面我们来看看创建协程的三种方式 2.1 使用 runBlocking 顶层函数创建 fun runBlockingTest(){runBlocking {KyLog.i(yvan,runBlocking)}}2.2 使用 GlobalScope 单例对象创建 fun globalScopeTest(){GlobalScope.launch {Log.i(yvan,GlobalScope launch)}}2.3 自行通过 CoroutineContext 创建一个 CoroutineScope 对象 fun coroutineScopeTest(){val coroutineScope CoroutineScope(Dispatchers.IO)coroutineScope.launch {KyLog.i(yvan,CoroutineScope launch)}}2.4 使用总结 方法一runBlocking通常适用于单元测试的场景而业务开发中不会用到这种方法因为它是线程阻塞的不推荐。方法二GlobalScope和使用方法一runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法因为它的生命周期会只受整个应用程序的生命周期限制且不能取消。方法三CoroutineContext是比较推荐的使用方法我们可以通过 context 参数去管理和控制协程的生命周期这里的 context和 Android 里的不是一个东西是一个更通用的概念会有一个 Android 平台的封装来配合使用。 3 Kotlin协程取消 与线程类比Java 线程其实没有提供任何机制来安全地终止线程。 Thread 类提供了一个方法 interrupt() 方法用于中断线程的执行。调用interrupt()方法并不意味着立即停止目标线程正在进行的工作而只是传递了请求中断的消息然后由线程在下一个合适的时机中断自己。协程Job 接口有一个 cancel() 方法用于取消它调用它会触发以下效果 协程会在第一个挂起点结束 job 下面例子中的 delay如果一个 job 有几个子 job它们也会被取消但是它的父 job 不受影响一旦一个 job 被取消它就不能被用作任何新 job 的父 job。它首先处于 “Cancelling” 状态然后处于 “Cancelled” 状态 3.1 job的cancel()、join()、cancelAndJoin()方法 3.1.1 job的cancel() 取消之后我们通常会调用 join() 方法程序必须要等到“取消”执行完才能继续。如果没有这个函数我们可能就会有一些别的竞争。 下面代码展示了一个示例在IO线程没有调用 join() 的情况下我们将会看到 “repeat end 0” 在 “Cancelled” 后面 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)onlyCancel() }private suspend fun onlyCancel() coroutineScope {val job launch {repeat(200) { i -Log.i(yvan, repeat start $i thread:${Thread.currentThread().name})delay(100)Log.i(yvan, repeat doing $i)Thread.sleep(100) // 我们模拟一些耗时操作Log.i(yvan, repeat end $i)}}delay(200)job.cancel()Log.i(yvan, Cancelled) }上面的打印结果 yvan: CoroutineScope launch yvan: repeat start 0 thread:DefaultDispatcher-worker-3 yvan: repeat doing 0 yvan: Cancelled yvan: repeat end 0 yvan: repeat start 1 thread:DefaultDispatcher-worker-1 3.1.2 job的join() cancel()之后先往后执行Cancelled后还能继续执行repeat()方法内的逻辑加上 job.join() 将会改变这一点 因为它会挂起直到一个协程完成取消。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)cancelAndJoin() }private suspend fun cancelAndJoin() coroutineScope {val job launch {repeat(200) { i -Log.i(yvan, repeat start $i thread:${Thread.currentThread().name})delay(100)Log.i(yvan, repeat doing $i)Thread.sleep(100) // 我们模拟一些耗时操作Log.i(yvan, repeat end $i)}}delay(200)job.cancel()job.join()// 为了更容易地同时调用 cancel() 和 join() kotlinx.coroutines 提供了更方便的扩展函数 cancelAndJoin()。// job.cancelAndJoin()Log.i(yvan, Cancelled) }加上 job.join() 的打印结果 yvan: CoroutineScope launch yvan: repeat start 0 thread:DefaultDispatcher-worker-3 yvan: repeat doing 0 yvan: repeat end 0 yvan: repeat start 1 thread:DefaultDispatcher-worker-3 yvan: Cancelled 加上job.join()的打印结果是执行完repeat()内所有逻辑才往后执行Cancelled。 需要注意上面是IO线程的情况如果在Main线程则不管是否有job.join()打印结果都跟IO线程加上job.join()的顺序一致。 因为取消发生在挂起点上如果没有挂起点就不会发生。为了模拟这种情况我们使用了 Thread.sleep 而不是 delay 这种做法不太好所以请不要在任何现实项目中这么做。我们只是试图模拟一种情况在这种情况下我们广泛的使用我们的协程但没有挂起它们。在实践中如果我们有一些更复杂的计算比如神经网络学习是的为了简化处理并行化我们也会使用协程或者当我们需要做一些阻塞调用例如读取文件时就会发生这种情况。 3.1.3 job一次性取消多个协程 使用 Job() 工厂函数创建的 job 可以以同样的方式被取消。这通常用于一次性取消多个协程。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)jobFactory()}private suspend fun jobFactory(): Unit coroutineScope {val job Job()launch(job) {repeat(400) { i -delay(200)Log.i(yvan, job1 repeat $i thread:${Thread.currentThread().name})}}launch(job) {repeat(400) { i -delay(200)Log.i(yvan, job2 repeat $i thread:${Thread.currentThread().name})}}delay(400)job.cancelAndJoin()Log.i(yvan, Cancelled)}打印结果 yvan: CoroutineScope launch yvan: job2 repeat 0 thread:DefaultDispatcher-worker-2 yvan: job1 repeat 0 thread:DefaultDispatcher-worker-3 yvan: Cancelled Job() 一次性取消多个协程这个能力比较重要。我们经常需要取消一组并发任务。例如在 Android 中当用户离开一个视图时我们需要取消此视图启动的多个协程。 3.2 CancellationException 异常 3.2.1 异常捕获及finally块 当一个 job 被取消时它的状态变成 Cancelling然后在第一个挂起点抛出一个 CancellationException 异常。可以使用 try-catch 来捕获这个异常。 private suspend fun tryCatchCancelAndJoin(): Unit coroutineScope {val job Job()launch(job) {try {repeat(400) { i -delay(200)Log.i(yvan, job repeat $i thread:${Thread.currentThread().name})}} catch (e: CancellationException) {Log.i(yvan, job repeat error $e)} finally {Log.i(yvan, job repeat finally deal)}}delay(400)job.cancelAndJoin()Log.i(yvan, Cancelled)}打印结果 yvan: CoroutineScope launch yvan: job repeat 0 thread:DefaultDispatcher-worker-3 yvan: job repeat error kotlinx.coroutines.JobCancellationException: Job was cancelled; jobJobImpl{Cancelling}ccbaa8c yvan: job repeat finally deal yvan: Cancelled 一个被取消的协程不是仅仅的停止它是使用一个异常在内部取消的。因此我们可以自由地在 finlay 块清理所有的东西。例如我们可以使用 finally 块来关闭文件或数据库连接等。 3.2.2 finally块中再次使用协程 由于我们可以捕获 CancellationException 在协程真正结束之前可以执行一些操作你可能想知道有没有什么限制。只要需要清理所有资源协程就可以运行。然而挂起是不允许的。 job 已经处于 “Cancelling” 状态在这种状态下挂起或启动另一个协程是不可能的。如果我们启动另一个协程它将被忽略如果我们尝试挂起它将会抛出 CancellationException。 private suspend fun tryCatchCancelAndJoin(): Unit coroutineScope {val job Job()launch(job) {try {repeat(400) { i -delay(200)Log.i(yvan, job repeat $i thread:${Thread.currentThread().name})}} catch (e: CancellationException) {Log.i(yvan, job repeat error $e)} finally {Log.i(yvan, job repeat finally deal)launch { // 这个launch内部会被忽略不执行Log.i(yvan, job repeat finally launch)}try {delay(400) // 会抛出异常} catch (e: Exception) {Log.i(yvan, job repeat error2 $e)}Log.i(yvan, job repeat finally end)}}delay(400)job.cancelAndJoin()Log.i(yvan, Cancelled)}打印结果 yvan: CoroutineScope launch yvan: job repeat 0 thread:DefaultDispatcher-worker-3 yvan: job repeat error kotlinx.coroutines.JobCancellationException: Job was cancelled; jobJobImpl{Cancelling}ccbaa8c yvan: job repeat finally deal yvan: job repeat error2 kotlinx.coroutines.JobCancellationException: Job was cancelled; jobJobImpl{Cancelling}ccbaa8c yvan: job repeat finally end yvan: Cancelled job 已经处于 “Cancelling” 状态下finally中的再次使用协程launch内不会再执行。 3.2.3 不能被取消的 job 有时当协程已经取消时我们确实需要使用挂起函数。在这种情况下首选的方法是使用 withContext(NonCancellable) 函数来包装这个调用。在 withContext 中我们使用了 NonCancelable 对象这是一个不能被取消的 job。因此在 block 代码块中job 处于活跃状态我们可以调用任何我们想要的挂起函数。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)tryCatchCancelAndJoinNonCancellable()}private suspend fun tryCatchCancelAndJoinNonCancellable(): Unit coroutineScope {val job Job()launch(job) {try {delay(200)Log.i(yvan, job finished)} catch (e: CancellationException) {Log.i(yvan, job catch $e)} finally {Log.i(yvan, job finally)withContext(NonCancellable) {delay(200)Log.i(yvan, job cleanup done)}}}delay(100)job.cancelAndJoin()Log.i(yvan, job done)}打印结果 yvan: CoroutineScope launch yvan: job catch kotlinx.coroutines.JobCancellationException: Job was cancelled; jobJobImpl{Cancelling}ccbaa8c yvan: job finally yvan: job cleanup done yvan: job done 3.3 invokeOnCompletion Job 中提供了释放资源机制的 invokeOnCompletion 函数。它用于设置当 job 到达最终状态时即 “Completed” 或 “Cancelled”回调的代码。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)invokeOnCompletion()}private suspend fun invokeOnCompletion(): Unit coroutineScope {val job launch {delay(400)Log.i(yvan, job launch start)delay(100)Log.i(yvan, job launch end)}job.invokeOnCompletion { exception: Throwable? -Log.i(yvan, Finished exception:$exception)}delay(400)job.cancelAndJoin()Log.i(yvan, job done)}打印结果 yvan: CoroutineScope launch yvan: job launch start yvan: Finished exception:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; jobStandaloneCoroutine{Cancelled}ccbaa8c yvan: job done 这个回调函数的参数exception是一个异常 协程完成或没有异常为null协程被取消为CancellationException如果协程异常则为对应的Exception job 在调用 invokeOnCompletion 之前已经完成那么回调函数将立即被调用。 下面的例子展示了一种情况协程不能取消因为它里面没有挂起点我们使用 Thread.sleep 而不是 delay。即便它应该在400毫秒后取消但实际上执行超过了1分钟。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)nonCancel()}private suspend fun nonCancel(): Unit coroutineScope {val job Job()launch(job) {repeat(400) { i -Thread.sleep(200)// 这里我们可能有一些复杂的操作例如读取文件Log.i(yvan, repeat $i thread:${Thread.currentThread().name})}}delay(400)job.cancelAndJoin()Log.i(yvan, Cancelled)delay(400)}打印结果 yvan: CoroutineScope launch yvan: repeat 0 thread:DefaultDispatcher-worker-3 yvan: repeat 1 thread:DefaultDispatcher-worker-3 … yvan: repeat 399 thread:DefaultDispatcher-worker-3 yvan: Cancelled repeat()中times为400ms即执行了400次每次sleep200ms执行所以总的时间为80000ms。 3.4 isActive 我们可以使用 isActive 属性来检查 job 是否仍然处于活跃状态并在 job 处于非活跃状态时停止计算。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)nonCancelActive()}private suspend fun nonCancelActive(): Unit coroutineScope {val job Job()launch(job) {var count 0do {Thread.sleep(200)countLog.i(yvan, while $count thread:${Thread.currentThread().name})// 通过isActive限制继续执行} while (isActive)}delay(500)job.cancelAndJoin()Log.i(yvan, Cancelled)}打印结果 yvan: CoroutineScope launch yvan: while 1 thread:DefaultDispatcher-worker-3 yvan: while 2 thread:DefaultDispatcher-worker-3 yvan: while 3 thread:DefaultDispatcher-worker-3 yvan: Cancelled 3.5 ensureActive 我们也可以使用 ensureActive() 函数它会在 Job 不活跃时候抛出 CancelllationException。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)nonCancelEnsureActive()}private suspend fun nonCancelEnsureActive(): Unit coroutineScope {val job Job()launch(job) {try {repeat(400) { num -Thread.sleep(200)// 协程被取消后会导致抛出CancelllationException异常ensureActive()Log.i(yvan, repeat $num thread:${Thread.currentThread().name})}} catch (e: Exception) {Log.i(yvan, repeat catch $e)}}delay(500)job.cancelAndJoin()Log.i(yvan, Cancelled)}打印结果 yvan: CoroutineScope launch yvan: repeat 0 thread:DefaultDispatcher-worker-3 yvan: repeat 1 thread:DefaultDispatcher-worker-3 yvan: repeat catch kotlinx.coroutines.JobCancellationException: Job was cancelled; jobJobImpl{Cancelling}ccbaa8c yvan: Cancelled yield() 和 ensureActive 使用方式一样。 yield 会进行的第一个工作就是检查任务是否完成如果 Job 已经完成的话就会抛出 CancellationException 来结束协程。yield 应该在定时检查中最先被调用。 ensureActive() 和 yield() 的结果看起来十分相似但它们有很大的不同。函数 ensureActive() 需要在 CoroutinScope、CoroutineContext、Job作用域内调用。它所做的事情只是在 job 不再活跃时抛出异常。它更轻量所以通常它应该是首选。函数 yield 是一个常规的顶层挂起函数。它不需要任何作用域因此可以在任意常规挂起函数中使用。由于它执行挂起和恢复操作因此可能会产生其它影响例如如果我们使用带有线程池的分发器则会导致线程更改。 yield 通常只用于挂起 CPU 密集型或阻塞线程的函数。 3.6 suspendCancellableCoroutine suspendCancellableCoroutine它的行为类似于 suspendCoroutine但是它的 continuation 被包装到了提供了额外方法的 CancellableContinuationT 中。最重要的一个方法是 invokeOnCancellation我们使用它来定义取消协程时应该发生什么。我们通常使用它来取消库中的进程或者释放一些资源。 suspend fun someTask() suspendCancellableCoroutine { cont -cont.invokeOnCancellation {// do cleanup}// rest of the implementation }CancellableContinuationT 也允许我们检查 job 的状态通过使用 isActiveisCompleted、 isCancelled 属性并使用可选的取消原因异常取消这个 continuation。 3.7 协程取消总结 取消是一个强大的功能。它通常很容易使用但有时会很棘手。所以了解它的工作原理很重要。 正确使用取消操作意味着更少的资源浪费和更少的内存泄漏这对我们的应用程序的性能很重要。 4 等待协程执行 无返回值的协程使用 launch 函数创建需要返回值则通过 async 函数创建。 使用 async 方法启动 Deferred 也是一种 job 可以调用它的 await() 方法获取执行的结果。 private suspend fun asyncTest(): Unit coroutineScope {val asyncDeferred async {// do some}// 等待结果返回val result asyncDeferred.await()}deferred 也是可以取消的对于已经取消的 deferred 调用 await() 方法会抛出JobCancellationException 异常。在 deferred.await 之后调用 deferred.cancel()那么什么都不会发生因为任务已经结束了。 5 协程异常处理 上面协程取消中已经提到过挂起函数包裹在 try/catch 代码块中这样就可以在 finally 代码块中进行资源清理等操作了具体请看3.2 6 协程超时 绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动使用 withTimeout 函数。 CoroutineScope(Dispatchers.IO).launch {Log.i(yvan, CoroutineScope launch)withTimeoutTest()}private suspend fun withTimeoutTest(): Unit coroutineScope {val result withTimeout(300) {try {Log.i(yvan, start)delay(100)Log.i(yvan, 1)delay(100)Log.i(yvan, 2)delay(100)Log.i(yvan, 3)delay(100)Log.i(yvan, 4)delay(100)Log.i(yvan, 5)Log.i(yvan, end)} catch (e: Exception) {Log.i(yvan, e:$e)}}Log.i(yvan, result:$result)}打印结果 yvan: CoroutineScope launch yvan: start yvan: 1 yvan: 2 yvan: e:kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms withTimeout 抛出了 TimeoutCancellationException它是 CancellationException 的子类。 当然还有另一种方式 使用 withTimeoutOrNull两个函数正常执行完后都有返回值但两者的区别在于 withTimeout 超时则无返回值直接抛出一个超时异常 TimeoutCancellationExceptionwithTimeoutOrNull 函数会在超时也会有返回一个 null 值 7 协程并发与挂起 7.1 async实现并发 考虑一个场景 开启多个任务并发执行所有任务执行完之后返回结果再汇总结果继续往下执行。 针对这种场景解决方案有很多比如 Java 的 FeatureTask concurrent 包里面的 CountDownLatch、Semaphore、 Rxjava 提供的 Zip 变换操作等。 前面提到有返回值的协程我们通常使用 async 函数来启动。 private fun asyncTime() runBlocking {val time measureTimeMillis {val a async(Dispatchers.IO) {Log.i(yvan, async1 thread:${Thread.currentThread().name})delay(1000) // 模拟耗时操作1}val b async(Dispatchers.IO) {Log.i(yvan, async2 thread:${Thread.currentThread().name})delay(2000) // 模拟耗时操作2}Log.i(yvan, ab${a.await() b.await()})Log.i(yvan, end)}Log.i(yvan, time: $time)}打印结果 15:38:17.260 6043-6083/com.example.kotlin I/yvan: CoroutineScope launch 15:38:17.261 6043-6085/com.example.kotlin I/yvan: async1 thread:DefaultDispatcher-worker-3 15:38:17.262 6043-6070/com.example.kotlin I/yvan: async2 thread:DefaultDispatcher-worker-1 15:38:19.266 6043-6083/com.example.kotlin I/yvan: ab3 15:38:19.266 6043-6083/com.example.kotlin I/yvan: end 15:38:19.266 6043-6083/com.example.kotlin I/yvan: time: 2006 async 启动一个协程后调用 await 方法后会阻塞等待结果的返回同样能达到效果。 7.2 async实现惰性启动 async 可以通过将 start 参数设置为 CoroutineStart.LAZY 变成惰性的。在这个模式下调用 await 获取协程执行结果的时候或者调用 Job 的 start 方法时协程才会启动。 private fun asyncTime2() runBlocking {val time measureTimeMillis {val a async(Dispatchers.IO, CoroutineStart.LAZY) {Log.i(yvan, async1 thread:${Thread.currentThread().name})delay(1000) // 模拟耗时操作1}val b async(Dispatchers.IO, CoroutineStart.LAZY) {Log.i(yvan, async2 thread:${Thread.currentThread().name})delay(2000) // 模拟耗时操作2}a.start()b.start()Log.i(yvan, ab${a.await() b.await()})Log.i(yvan, end)}Log.i(yvan, time: $time)}打印结果 15:42:48.796 6460-6489/com.example.kotlin I/yvan: CoroutineScope launch 15:42:48.799 6460-6491/com.example.kotlin I/yvan: async1 thread:DefaultDispatcher-worker-3 15:42:48.799 6460-6490/com.example.kotlin I/yvan: async2 thread:DefaultDispatcher-worker-2 15:42:50.803 6460-6489/com.example.kotlin I/yvan: ab3 15:42:50.804 6460-6489/com.example.kotlin I/yvan: end 15:42:50.804 6460-6489/com.example.kotlin I/yvan: time: 2007 如果上面的start不调用依靠await方法启动则需要等到a.await后1000ms才能执行b.awaitb再执行2000ms后才能输出。 打印结果 15:42:58.760 6542-6569/com.example.kotlin I/yvan: CoroutineScope launch 15:42:58.762 6542-6571/com.example.kotlin I/yvan: async1 thread:DefaultDispatcher-worker-3 15:42:59.766 6542-6571/com.example.kotlin I/yvan: async2 thread:DefaultDispatcher-worker-3 15:43:01.770 6542-6569/com.example.kotlin I/yvan: ab3 15:43:01.770 6542-6569/com.example.kotlin I/yvan: end 15:43:01.770 6542-6569/com.example.kotlin I/yvan: time: 3010 7.3 挂起函数 我们先来看一段代码其中delay方法是否能正常编译通过呢 fun delayTest(){delay(1000)}以上代码会报错Suspend function ‘delay’ should be called only from a coroutine or another suspend function 为什么呢我们来看挂起函数的delay源码 public suspend fun delay(timeMillis: Long) {if (timeMillis 0) return // dont delayreturn suspendCancellableCoroutine sc { cont: CancellableContinuationUnit -// if timeMillis Long.MAX_VALUE then just wait forever like awaitCancellation, dont schedule.if (timeMillis Long.MAX_VALUE) {cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)}} }可以看到方法签名用 suspend 修饰表示该函数是一个挂起函数。解决这个异常只需要将我们定义的方法也用 suspend 修饰使其变成一个挂起函数。 使用 suspend 关键字修饰的函数成为挂起函数挂起函数只能在另一个挂起函数或者协程中被调用。在挂起函数中可以调用普通函数非挂起函数。 7.4 协程和挂起的本质 本质上协程是轻量级的线程kotlin 协程的实现是借助线程可以理解为对线程的一个封装框架。启动一个协程使用 launch 或者 async 函数启动的是函数中闭包代码块好比启动一个线程实现上是执行 run 方法中的代码所以协程可以理解为是这个代码块。协程的核心点就是函数或者一段程序能够被挂起稍后再在挂起的位置恢复。 suspend 翻译过来是中断、暂停的意思。当线程执行到协程的 suspend 函数的时候暂时不继续执行协程代码了。这个挂起是针对当前线程来说的从当前线程挂起就是这个协程从执行它的线程上脱离并不是说协程停下来了而是当前线程不再管这个协程要去做什么了。 当协程执行到挂起函数时从当前线程脱离然后继续执行这个时候在哪个线程执行由协程调度器所指定挂起函数执行完之后又会重新切回到它原先的线程来这个就是协程的优势所在。 理解一下协程和线程的区别 线程一旦开始执行就不会暂停直到任务结束这个过程是连续的线程是抢占式的调度不存在协作的问题协程程序能够自己挂起和恢复程序自己处理挂起恢复实现程序执行流程的协作式调度。 Kotlin 中所谓的挂起就是一个稍后会被自动切回来的线程调度操作这个 resume 功能是协程的如果不在协程里面调用那它就没法恢复。所以挂起函数必须在协程或者另一个挂起函数里面被调用总是直接或者间接地在协程里被调用。 实现挂起的的目的是让程序脱离当前的线程也就是要切线程kotlin 协程提供了一个 withContext() 方法来实现线程切换。 private suspend fun withContextTest() {withContext(Dispatchers.IO) {Log.i(yvan, withContextTest)}}withContext() 本身也是一个挂起函数它接收一个 Dispatcher参数依赖这个参数协程被挂起再切到别的线程。所以想要自己写一个挂起函数除了加上 suspend 关键字以外还需要函数内部直接或者间接的调用 Kotlin 协程框架自带的挂起函数才行。比如前面调用的 delay 函数框架内部实际上进行了切线程的操作。 suspend 并不能切换线程。切线程依赖的是挂起函数里面的实际代码这个关键字只是一个提醒作用。如果我创建一个 suspend 函数内部不包含其它挂起函数编译器同样会提示这个修饰符是多余的。 suspend 表明这个函数时挂起函数限制了它只能在协程或者其它挂起函数里面调用。 其它语言比如 C#使用的 async 关键字。 如果一个函数比较耗时那么就可以把它定义成挂起函数。耗时一般有两种情况 I/O 操作和CPU 计算工作。 另外还有延时操作也可以把它定义成挂起函数代码本身执行不耗时但是需要延时一段时间。 写法 给函数加上 suspend 关键字后 如果是耗时操作在 withContext 把函数的内容操作就可以了如果是延时操作调用 delay 函数即可。 延时操作 suspend fun testA() {...delay(1000)... }耗时操作 suspend fun testB() {withContext(Dispatchers.IO) {...} }// 也可以写成 suspend fun testB() withContext(Dispatchers.IO) {... }8 协程上下文和作用域 两个概念 CoroutineContext 协程的上下文CoroutineScope 协程的作用域 8.1 协程上下文 CoroutineContext 协程总是运行在一些以 CoroutineContext 类型为代表的上下文中。协程上下文是各种不同元素的集合。其中主元素是协程中的 Job 以及它的调度器。 协程上下文包含当前协程scope的信息 比如的Job, ContinuationInterceptor, CoroutineName 和CoroutineId。在CoroutineContext中是用map来存这些信息的 map的键是这些类的伴生对象值是这些类的一个实例你可以这样子取得context的信息: val job context[Job] val continuationInterceptor context[ContinuationInterceptor]Job继承了CoroutineContext.ElementCoroutineContext.Element继承了 CoroutineContext。 他是协程上下文的一部分。 Job 一个重要的子类 ———— AbstractCoroutine即协程。使用launch 或者async方法都会实例化出一个AbstractCoroutine 的协程对象。一个协程的协程上下文的Job值就是他本身。 val job mScope.launch {printWithThreadInfo(job: ${this.coroutineContext[Job]})}printWithThreadInfo(job2: $job)printWithThreadInfo(job3: ${job[Job]})输出 thread id: 1, thread name: main — job2: StandaloneCoroutine{Active}1ee0005 thread id: 12, thread name: test_dispatcher — job: StandaloneCoroutine{Active}1ee0005 thread id: 1, thread name: main — job3: StandaloneCoroutine{Active}1ee0005 协程上下文包含一个 协程调度器 CoroutineDispatcher它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行或将它分派到一个线程池亦或是让它不受限地运行。 所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext 参数它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。 当调用 launch { …… } 时不传参数它从启动了它的 CoroutineScope 中承袭了上下文以及调度器。 CoroutineContext最重要的两个信息是 Dispatcher 和 Job, 而 Dispatcher 和 Job 本身又实现了 CoroutineContext 的接口。是其子类。 这个设计就很有意思了。 有时我们需要在协程上下文中定义多个元素。我们可以使用 操作符来实现。 比如说我们可以显式指定一个调度器来启动协程并且同时显式指定一个命名 launch(Dispatchers.Default CoroutineName(test)) {println(Im working in thread ${Thread.currentThread().name}) }这得益于 CoroutineContext 重载了操作符 。 8.2 协程作用域 CoroutineScope CoroutineScope 即协程运行的作用域,它的源码如下 public interface CoroutineScope {public val coroutineContext: CoroutineContext }可以看出CoroutineScope的代码很简单主要作用是提供 CoroutineContext 启动协程需要 CoroutineContext。 作用域可以管理其域内的所有协程。一个CoroutineScope可以有许多的子scope。协程内部是通过 CoroutineScope.coroutineContext 自动继承自父协程的上下文。而 CoroutineContext 就是在作用域内为协程进行线程切换的快捷方式。 当使用 GlobalScope 来启动一个协程时则新协程的作业没有父作业。 因此它与这个启动的作用域无关且独立运作。GlobalScope 包含的是 EmptyCoroutineContext。 一个父协程总是等待所有的子协程执行结束。父协程并不显式的跟踪所有子协程的启动并且不必使用 Job.join 在最后的时候等待它们。 取消父协程会取消所有的子协程。所以使用 Scope 来管理协程的生命周期。 默认情况下协程内某个子协程抛出一个非 CancellationException 异常未被捕获会传递到父协程任何一个子协程异常退出那么整体都将退出 8.3 创建 CoroutineScope 创建一个 CoroutineScope, 只需调用 public fun CoroutineScope(context: CoroutineContext) 方法传入一个 CoroutineContext 对象。 在协程作用域内启动一个子协程默认自动继承父协程的上下文但在启动时我们可以指定传入上下文。 val dispatcher Executors.newFixedThreadPool(1).asCoroutineDispatcher() val myScope CoroutineScope(dispatcher) myScope.launch {... }8.4 SupervisorJob 启动一个协程默认是实例化的是 Job 类型。该类型下协程内某个子协程抛出一个非 CancellationException 异常未被捕获会传递到父协程任何一个子协程异常退出那么整体都将退出。 为了解决上述问题可以使用SupervisorJob替代JobSupervisorJob与Job基本类似区别在于SupervisorJob不会被子协程的异常所影响。 private val svJob SupervisorJob() private val mDispatcher newSingleThreadContext(test_dispatcher)private val exceptionHandler CoroutineExceptionHandler { _, throwable -printWithThreadInfo(exceptionHandler: throwable: $throwable) }private val svScope CoroutineScope(svJob mDispatcher exceptionHandler) private val mScope CoroutineScope(Job() mDispatcher exceptionHandler)svScope.launch {... }// 或者 supervisorScope { launch { ...} }8.5 如何在 Android 中使用协程 8.5.1. 自定义 coroutineScope 不要使用 GlobalScope 去启动协程因为 GlobalScope 启动的协程生命周期与应用程序的生命周期一致无法取消。官方建议在 Android 中自定义协程作用域。当然Kotlin 给我们提供了 MainScope我们可以直接使用。 public fun MainScope(): CoroutineScope ContextScope(SupervisorJob() Dispatchers.Main)然后让 Activity 实现该作用域 class BasicCorotineActivity : AppCompatActivity(), CoroutineScope by MainScope() {... }然后再通过 launch 或者 async 启动协程 private fun loadAndShow() {launch {val task async(Dispatchers.IO) {// load 过程delay(3000)...hello, kotlin}tvShow.setText(task.await())} }最后别忘了在 Activity onDestory 时取消协程。 override fun onDestroy() {cancel()super.onDestroy() }8.5.2 ViewModelScope 如果你使用了 ViewModel LiveData 实现 MVVM 架构根本就不会在 Activity 上书写任何逻辑代码更别说启动协程了。这个时候大部分工作就要交给 ViewModel 了。那么如何在 ViewModel 中定义协程作用域呢直接把上面的 MainScope() 搬过来就可以了。 class ViewModelOne : ViewModel() {private val viewModelJob SupervisorJob()private val uiScope CoroutineScope(Dispatchers.Main viewModelJob)val mMessage: MutableLiveDataString MutableLiveData()fun getMessage(message: String) {uiScope.launch {val deferred async(Dispatchers.IO) {delay(2000)post $message}mMessage.value deferred.await()}}override fun onCleared() {super.onCleared()viewModelJob.cancel()} }这里的 uiScope 其实就等同于 MainScope。调用 getMessage() 方法和之前的 loadAndShow() 效果也是一样的记得在 ViewModel 的 onCleared() 回调里取消协程。 你可以定义一个 BaseViewModel 来处理这些逻辑避免重复书写模板代码。 然而Kotlin 提供了 viewmodel-ktx 来了。引入下面的依赖 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha03然后直接使用协程作用域 viewModelScope 就可以了。viewModelScope 是 ViewModel 的一个扩展属性定义如下 val ViewModel.viewModelScope: CoroutineScopeget() {val scope: CoroutineScope? this.getTag(JOB_KEY)if (scope ! null) {return scope}return setTagIfAbsent(JOB_KEY,CloseableCoroutineScope(SupervisorJob() Dispatchers.Main))}所以直接使用 viewModelScope 就是最好的选择。 8.5.3 LifecycleScope 与 viewModelScope 配套的 还有 LifecycleScope, 引入依赖 implementation androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03lifecycle-runtime-ktx 给每个 LifeCycle 对象通过扩展属性定义了协程作用域 lifecycleScope 。可以通过 lifecycle.coroutineScope 或者 lifecycleOwner.lifecycleScope 进行访问。示例代码如下 lifecycleOwner.lifecycleScope.launch {val deferred async(Dispatchers.IO) { getMessage(LifeCycle Ktx)}mMessage.value deferred.await() }当 LifeCycle 回调 onDestroy() 时协程作用域 lifecycleScope 会自动取消。 9 协程并发数据同步 9.1 线程中数据安全问题 在多线程同时操作修改一个数据时可能会出现数据异常的情况我们称之为线程数据不安全给数据加上 volatile 关键修饰: Volatile var data 1没有用 volatile 修饰 data 之前改变了不具有可见性一个线程将它的值改变后另一个线程却 “不知道”所以程序没有退出。 当把变量声明为 volatile 类型后编译器与运行时都会注意到这个变量是共享的因此不会将该变量上的操作与其他内存操作一起重排序。 volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方因此在读取volatile类型的变量时总会返回最新写入的值。 在访问volatile变量时不会执行加锁操作因此也就不会使执行线程阻塞因此volatile变量是一种比sychronized关键字更轻量级的同步机制。 当对非 volatile 变量进行读写的时候每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU每个线程可能在不同的CPU上被处理这意味着每个线程可以拷贝到不同的CPU缓存中。 而声明变量是 volatile 的JVM 保证了每次读变量都从内存中读跳过CPU缓存这一步。 volatile 修饰的遍历具有如下特性 保证此变量对所有的线程的可见性当一个线程修改了这个变量的值volatile 保证了新值能立即同步到主内存以及每次使用前立即从主内存刷新。但普通变量做不到这点普通变量的值在线程间传递均需要通过主内存来完成。禁止指令重排序优化。不会阻塞线程。 synchronized 只会保证该同步块中的变量的可见性发生变化后立即同步到主存JVM对于现代的机器做了最大程度的优化也就是说最大程度的保障了线程和主存之间的及时的同步也就是相当于虚拟机尽可能的帮我们加了个volatile但是当CPU被一直占用的时候同步就会出现不及时的情况。 9.2 协程中数据同步问题 看如下例子 CoroutineScope(Dispatchers.IO).launch {concurrencyTest()}private var count 0private suspend fun concurrencyTest() withContext(Dispatchers.IO) {repeat(100) {launch {repeat(1000) {count}}}launch {delay(3000)Log.i(yvan, end count: $count thread:${Thread.currentThread().name})}}打印结果 yvan: end count: 96137 thread:DefaultDispatcher-worker-5 并不是我们期待的 100000。很明显协程并发过程中数据不同步造成。 9.2.1 volatile 无效 很显然有人肯定也想着使用 volatile 修饰变量就可以解决真的是这样吗其实不然。我们给 count 变量用 volatile 修饰也依然得不到期望的结果。 volatile 在并发中保证可见性但是不保证原子性。 count 该运算包含读、写操作并非一次原子操作。这样并发情况下自然得不到期望的结果。 9.2.2 使用线程安全的数据结构 一种解决办法是使用线程安全地数据结构。们可以使用具有 incrementAndGet 原子操作的 AtomicInteger 类 CoroutineScope(Dispatchers.IO).launch {concurrencyTest()}private var count AtomicInteger()private suspend fun concurrencyTest() withContext(Dispatchers.IO) {repeat(100) {launch {repeat(1000) {count.incrementAndGet()}}}launch {delay(3000)Log.i(yvan, end count: ${count.get()} thread:${Thread.currentThread().name})}}打印结果 yvan: end count: 100000 thread:DefaultDispatcher-worker-7 9.2.3 同步操作 对数据的增加进行同步操作可以同步计数自增的代码块 private val obj Any()private var count 0private suspend fun concurrencyTest() withContext(Dispatchers.IO) {repeat(100) {launch {repeat(1000) {synchronized(obj) { // 同步代码块count}}}}launch {delay(3000)Log.i(yvan, end count: $count thread:${Thread.currentThread().name})}}或者使用 ReentrantLock 操作。 runBlockingUnit {val cos measureTimeMillis {concurrencyTest()}Log.i(yvan, cos time: $cos)}private val mLock ReentrantLock()private var count 0private suspend fun concurrencyTest() withContext(Dispatchers.IO) {repeat(100) {launch {repeat(1000) {mLock.lock()try{count} finally {mLock.unlock()}}}}launch {delay(3000)Log.i(yvan, end count: $count thread:${Thread.currentThread().name})}} 打印结果 yvan: end count: 100000 thread:DefaultDispatcher-worker-53 yvan: cos time: 3275 加锁在协程中的替代品叫做 Mutex 它具有 lock 和 unlock 方法关键的区别在于 Mutex.lock() 是一个挂起函数它不会阻塞当前线程。还有 withLock 扩展函数可以方便的替代常用的 mutex.lock(); 、try { …… } finally { mutex.unlock() } 模式 runBlockingUnit {val cos measureTimeMillis {concurrencyTest()}Log.i(yvan, cos time: $cos)}private val mutex Mutex()private var count 0private suspend fun concurrencyTest() withContext(Dispatchers.IO) {repeat(100) {launch {repeat(1000) {mutex.withLock {count}}}}launch {delay(3000)Log.i(yvan, end count: $count thread:${Thread.currentThread().name})}}打印结果 yvan: end count: 100000 thread:DefaultDispatcher-worker-45 yvan: cos time: 3040 9.2.4 限制线程 在同一个线程中进行计数自增就不会存在数据同步问题。每次进行自增操作时切换到单一线程。如同 AndroidUI 刷新必须切换到主线程一般。 runBlockingUnit {val cos measureTimeMillis {singleThreadLimit()}Log.i(yvan, cos time: $cos)}private val countContext newSingleThreadContext(CountContext)private var count 0suspend fun singleThreadLimit() withContext(countContext) {repeat(100) {launch {repeat(1000) {count}}}launch {delay(3000)Log.i(yvan, end count: $count thread:${Thread.currentThread().name})}}打印结果 yvan: end count: 100000 thread:CountContext yvan: cos time: 3014 9.2.5 使用 Actors 一个 actor 是由协程、 被限制并封装到该协程中的状态以及一个与其它协程通信的通道 组合而成的一个实体。一个简单的 actor 可以简单的写成一个函数 但是一个拥有复杂状态的 actor 更适合由类来表示。 有一个 actor 协程构建器它可以方便地将 actor 的邮箱通道组合到其作用域中用来接收消息、组合发送 channel 与结果集对象这样对 actor 的单个引用就可以作为其句柄持有。 使用 actor 步骤 是定义一个 actor 要处理的消息类Kotlin 的密封类很适合这种场景。 我们使用 IncCounter 消息用来递增计数器和 GetCounter 消息用来获取值来定义 CounterMsg 密封类。 后者需要发送回复。CompletableDeferred 通信原语表示未来可知可传达的单个值 这里被用于此目的。 // 计数器 Actor 的各种类型 sealed class CounterMsg // 递增计数器的单向消息 object IncCounter : CounterMsg() // 携带回复的请求 class GetCounter(val response: CompletableDeferredInt) : CounterMsg() 接下来定义一个函数使用 actor 协程构建器来启动一个 actor // 这个函数启动一个新的计数器 actor fun CoroutineScope.counterActor() actorCounterMsg {// actor 状态var counter 0 // 即将到来消息的迭代器for (msg in channel) { when (msg) {is IncCounter - counteris GetCounter - msg.response.complete(counter)}} }主要代码 suspend fun counterActorTest() withContext(Dispatchers.IO) {// 创建该 actorval counterActor counterActor() repeat(100) {launch {repeat(1000) {counterActor.send(IncCounter)}}}launch {delay(3000)// 发送一条消息以用来从一个 actor 中获取计数值val response CompletableDeferredInt()counterActor.send(GetCounter(response))Log.i(yvan, Counter ${response.await()})// 关闭该actorcounterActor.close() }}actor 本身执行时所处上下文就正确性而言无关紧要。一个 actor 是一个协程而一个协程是按顺序执行的因此将状态限制到特定协程可以解决共享可变状态的问题。实际上actor 可以修改自己的私有状态 但只能通过消息互相影响避免任何锁定。 actor 在高负载下比锁更有效因为在这种情况下它总是有工作要做而且根本不需要切换到不同的上下文。 实际上 CoroutineScope.actor()方法返回的是一个 SendChannel对象。Channel 也是 Kotlin 协程中的一部分。 10 协程总结 10.1 CoroutineContext 协程的上下文它包含用户定义的一些数据集合这些数据与协程密切相关。它类似于map集合可以通过key来获取不同类型的数据。同时CoroutineContext的灵活性很强如果其需要改变只需使用当前的CoroutineContext来创建一个新的CoroutineContext即可。 10.2 CoroutineScope 我们可以认为CoroutineScope是提供CoroutineContext的容器保证CoroutineContext能在整个协程运行中传递下去约束CoroutineContext的作用边界。 10.3 GlobalScope GlobalScope(object关键词修饰,其实就是个单例)不受job任何边界限制。GlobalScope用于启动顶级协程,在整个应用程序生命周期内运行且不会过早取消。GlobalScope的另一种用法是在Dispatchers.Unconfined中运行的操作符它与job无任何关联。应用程序代码通常应使用应用程序定义的CoroutineScope。不建议GlobalScope在应用中使用。 lifecycleScope lifecycleScope.launch(Dispatchers.IO) {}10.4 ViewModelScope ViewModelScope是为 ViewModel应用程序中的每个定义的。如果清除在此范围内启动的任何协程都会自动取消ViewModel。当您只有在活动时才需要完成工作时协程非常有用ViewModel。例如如果您正在计算布局的一些数据则应将工作范围限制在 ViewModel以便在 ViewModel清除 时工作会自动取消以避免消耗资源。 ViewModel中使用的协程。 它是ViewModel的扩展属性。自动取消不会造成内存泄漏如果是CoroutineScope就需要在onCleared()方法中手动取消了否则可能会造成内存泄漏。 10.5 CoroutineStart-协程启动模式 suspend fun main() { println(1) val job GlobalScope.launch { println(2) }println(3)//等待协程执行完毕job.join()println(4) }// print 1 3 2 4 suspend fun main() { println(1) val job GlobalScope.launch(start CoroutineStart.LAZY) {println(2) }println(3)//等待协程执行完毕job.join()println(4) }// 1 3 4 2 10.6 Dispatchers 协程上下文包含一个 协程调度器 参见 CoroutineDispatcher它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行或将它分派到一个线程池亦或是让它不受限地运行。 所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext 参数它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。 ContinuationInterceptor 初看起来这种写法有点奇怪但习惯了以后还是不得不承认这个是很优雅的设计相当于一个协变类型的 map。 10.7 CPS — Continuation Passing Style CPS其实就是将直接返回值的函数变换为通过回调传递结果的函数 很简单吧这就是CPS风格函数的结果通过回调来传递, 协程里通过在CPS的Continuation回调里结合状态机流转来实现协程挂起-恢复的功能. Kotlin 中被 suspend 修饰符修饰的函数在编译期间会被编译器做特殊处理。而这个特殊处理的第一道工序就是CPS续体传递风格变换它会改变挂起函数的函数签名。 我们直接展示一个例子 挂起函数 await 的函数签名如下所示 suspend fun T CompletableFutureT.await(): T在编译期发生 CPS 变换之后 fun T CompletableFutureT.await(continuation: ContinuationT): Any?编译器对挂起函数的第一个改变就是对函数签名的改变这种改变被称为 CPS续体传递风格变换。 我们看到发生 CPS 变换后的函数多了一个 ContinuationT 类型的参数Continuation 这个单词翻译成中文就是续体它的声明如下 interface Continuationin T {val context: CoroutineContextfun resumeWith(result: ResultT) }续体是一个较为抽象的概念简单来说它包装了协程在挂起之后应该继续执行的代码在编译的过程中一个完整的协程被分割切块成一个又一个续体。在 await 函数的挂起结束以后它会调用 continuation 参数的 resumeWith 函数来恢复执行 await 函数后面的代码。 11 协程面试题 1、什么是 Kotlin 协程 Kotlin 协程是一种轻量级的并发框架用于简化异步编程。它允许开发者使用顺序的方式来编写异步的、非阻塞的代码提供了一种能够挂起和恢复执行的机制。 2、Kotlin 协程与线程的区别是什么 Kotlin 协程是基于线程的但是它们更轻量级、更高效。线程是操作系统调度的最小执行单位而协程是在运行时进行调度的可以允许更多的协程在较少的线程上执行。 3、如何创建一个协程 可以使用 launch 函数或async函数来创建一个协程。例如launch { ... } 可以创建一个顶层协程它将在协程作用域中运行。 4、协程的取消机制是什么 协程的取消可以通过调用 cancel 方法或者取消相关的协程作用域来实现。协程会在取消后立即停止执行并调用相应的取消回调。 5、如何处理协程中的异常 可以使用 try/catch 块来捕获协程中的异常。可以使用 CoroutineExceptionHandler 来设置一个统一的异常处理程序。 6、什么是挂起函数 挂起函数是指在执行期间可能会暂停执行的函数。它们通过使用 suspend 修饰符来定义可以被其他协程挂起和恢复执行。 7、协程的调度器是什么 协程的调度器是负责决定协程在哪个线程上执行的组件。Kotlin 协程的调度器可以通过 launch、async 等函数的参数来指定也可以使用 withContext 函数在协程内部切换调度器。 8、协程的上下文是什么 协程的上下文是一组键值对包含了协程的调度器、异常处理器等信息。可以使用 CoroutineScope 或者 coroutineScope函数来创建具有特定上下文的协程作用域。 9、协程的并发与并行有何区别 协程的并发是指在同一个线程上进行交替执行的能力通过使用协程挂起和恢复执行的机制来实现。而并行是指在不同的线程上同时执行多个任务。 10、什么是协程的父子关系 协程可以嵌套在其他协程中形成父子关系。父协程在执行时会等待其所有子协程执行完毕这样可以实现更好的结构化并发。 11、协程的优势有哪些 协程具有以下优势 简化异步编程用顺序代码编写异步逻辑更轻量级可以在较少的线程上运行大量的协程提供异常处理机制使得错误处理更加灵活支持结构化并发提高代码的可读性和可维护性
http://www.laogonggong.com/news/118459.html

相关文章:

  • 网站权重排行网站技术策划
  • 网站运营计划书网站导航栏下拉菜单
  • 成都网站建设与推广wordpress开发导航菜单
  • 有哪些网站可以做外贸凡科送审平台
  • 公司无网站无平台怎么做外贸360收录提交
  • 广东做网站优化公司报价如何制作个人网页主题是周末愉快
  • 做饮食找工作哪个网站好比较好的网络推广平台
  • 唯样商城网站西安网站维护
  • 做财经比较好的网站有哪些网站取源用iapp做软件
  • 网站一元空间有哪些呀佛山免费建站
  • 网站建设 中国联盟网app网站开发流程图
  • 空调维修技术支持深圳网站建设制作页培训
  • 东莞松山湖网站建设北京知名的网站建设公司排名
  • 黑河建设网站小广告制作模板
  • 做网站代理需要办什么执照wordpress编辑器选择
  • 网站搭建代理如何创建二级域名
  • 网站建设标书模板中山哪里网站建设
  • 上海中心设计公司是谁佛山seo优化
  • 东营seo网站推广费用荆州网站建设费用
  • 定制网站开发哪里好wap多用户网站
  • 什么网站可以做私房菜外卖关于建设俄语网站的稿子
  • 旅游网站建设规划书企业管理培训课程有哪些内容
  • 武进网站建设价格房子装修改造
  • 荆门做网站公众号的公司wordpress 页面 评论链接 新窗口打开
  • 地方宣传网站建设的必要性谁做网站
  • 网站建站开发网站注册短信验证怎么做
  • 铁岭市网站建设公司网站建设运营外包
  • 创意经济型网站建设中资咨询管理有限公司
  • 美团外卖网站开发家装公司装修
  • 视频类html网站模板锡林郭勒盟建设工程管理网站