协程的上下文

我们使用构建器Launch去启动协程的时候,都需要指定协程上下文(没有显示指定会使用默认值)。
协程上下文(CoroutineContext)是一组用于定义协程的行为元素。它由如下几项构成
job(控制协程的生命周期),CoroutineDispatcher(向合适的线程发任务,协程调度器),CoroutineName(协程的名称,调试的时候很有用),CoroutineExceptionHandler(处理未被捕获的异常)
1,组合上下文中的元素
有时候我们需要在协程上下文中定义多个元素,我们可以使用+操作符来实现。比如说我们可以显式指定一个调度器来启动协程并且同时显示指定一个命名。

   @Testfun `test CoroutineContext`() = runBlocking<Unit> {//launch是协程构建器,需要传递协程上下文//而协程上下文是由job,CoroutineDispatcher,CoroutineName,CoroutineExceptionHandler构成,//Dispatchers.Default是调度器,CoroutineName("test")是名字launch(Dispatchers.Default + CoroutineName("test")) {println("I'm working in thread ${Thread.currentThread().name}")}}

打印输出:I‘m working in thread DefaultDiapatcher-worker-1 @test#2,打印当前线程的时候会打印线程名字。#2是协程的编号
之所以可以相加是CoroutineContex进行了运算符的重载。这里加了两个,当我们还可以加更多。

2,协程上下文的继承
对于新创建的线程,它的CoroutineContext包含了一个全新的job实例。它会帮助我们控制协程的生命周期。而剩下的元素会从CoroutineContext的父类继承,该父类可能是另外一个协程,或者创建协程的CoroutineScope(协程作用域)。

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {//必须指定泛型类型为<Unit>,不然报错//创建一个协程作用域,协程作用域中也是需要指定协程上下文的//有时候需要在协程上下文中定义多个元素,我们可以使用+操作符来实现val scope = CoroutineScope(Job()+Dispatchers.IO+CoroutineName("test"))//launch是通过协程作用域启动的//得到新的Job//launch会继承在协程作用域CoroutineScope中定义的协程上下文val job = scope.launch {//打印出launch启动的协程的Job对象,println("1111111${coroutineContext[Job]}   ${Thread.currentThread().name}")val result = async {//launch的子协程,继承launch的协程上下文println("22222222${coroutineContext[Job]}   ${Thread.currentThread().name}")"OK"}.await()}job.join()}
}

打印输出
1111111"test#2":StandaloneCoroutine{Active}@3b1ca9c4 DefaultDispatcher-worker-1 @test#2
22222222"test#3":DeferredCoroutine{Active}@40608051 DefaultDispatcher-worker-3 @test#3
Job对象不一样(@后的哈希值不一样),是一个全新的实例,其它的从父类继承。打印线程的时候自动会把协程的名字(协程名字都是test,只是编号不同,DefaultDispatcher-worker-3与DefaultDispatcher-worker-1都是IO调度器)打印出来

3,协程上下文的继承公式

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {//线程的异常处理器//使用_替代CoroutineContext对象val exceptionHandler = CoroutineExceptionHandler { _, exception ->println("caught $exception")//其实是回掉函数}//job()每次创建都会启用一个新的协程实例val scope = CoroutineScope(Job() + Dispatchers.Main+exceptionHandler)//线程的调度器是IO不是main,main被覆盖了。//协程的名字默认是coroutineval job = scope.launch(Dispatchers.IO) {//通过launch启动的新协程}}
}

协程的异常处理


协程有多种启动方式,启动方式不同,异常的处理方式也不同。协程有子协成,协程嵌套异常的传播也比较复杂。

代码理解(根协程)

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {val job1 = GlobalScope.launch {throw IndexOutOfBoundsException()//这个异常在这里抛出,要在这里catch,注意与下面协程的不同}job1.join()val job2 = GlobalScope.async {throw ArithmeticException()}//这句代码抛出异常,不消费就不会引发异常。job2.await()//不调用这句代码,job2就不会引发异常}
}

这里会抛出两个异常,注意这两个异常的catch地方不一样

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.Exception
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {val job1 = GlobalScope.launch {try {throw IndexOutOfBoundsException()} catch (e: Exception) {e.printStackTrace()}}job1.join()val job2 = GlobalScope.async {throw ArithmeticException()}try {job2.await()} catch (e: Exception) {e.printStackTrace()}}
}

非根协程所创建的协程中,产生的异常总是会被传播(代码演示如下)

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.Exception
import java.lang.IllegalArgumentException
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {val scope = CoroutineScope(Job())val job = scope.launch {async {//非根协程异常会直接抛出,而不需要调用.await(),//在根协程中,需要调用await才会抛出异常//该异常会被抛给它的父协程launchthrow IllegalArgumentException()}}job.join()}
}


一个child失败了,其它的child也取消了。
如果要打破异常的传播,可以使用SupervisorJob

代码体会这一点(举个例子,UI的组件有三种动画,我们其中的一种抛了异常,但还想另外两种动画执行)

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.Exception
import java.lang.IllegalArgumentException
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {//如果不使用SupervisorJob(),而是使用Job(),那就会job1,抛出异常,job2也会结束val supervisor = CoroutineScope(SupervisorJob())//job1泡了异常,job2还会执行val job1 = supervisor.launch {delay(100)println("111111111111111")throw IllegalArgumentException()}val job2 = supervisor.launch {try {delay(100000000)}finally {println("2222222222222")}}joinAll(job1,job2)//两个都join}
}

打印输出如下

111111111111111
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IllegalArgumentExceptionat com.example.testkotlin1030.ExampleUnitTest$my test$1$job1$1.invokeSuspend(ExampleUnitTest.kt:27)at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

如果把SupervisorJob换成Job,打印输出如下

111111111111111
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IllegalArgumentExceptionat com.example.testkotlin1030.ExampleUnitTest$my test$1$job1$1.invokeSuspend(ExampleUnitTest.kt:27)at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2222222222222

也可以使用supervisor.cancel()取消所有的协程(举例如下,一个UI组件销毁了,我们就销毁它的所有的动画)

  @Testfun `my test`() = runBlocking<Unit> {//如果不使用SupervisorJob(),而是使用Job(),那就会job1,抛出异常,job2也会结束val supervisor = CoroutineScope(SupervisorJob())//job1泡了异常,job2还会执行val job1 = supervisor.launch {delay(100)println("111111111111111")throw IllegalArgumentException()}val job2 = supervisor.launch {try {delay(100000000)}finally {println("2222222222222")}}delay(200)supervisor.cancel()//停掉所有的协程。joinAll(job1,job2)//两个都join}

运行结果如下。

111111111111111
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IllegalArgumentExceptionat com.example.testkotlin1030.ExampleUnitTest$my test$1$job1$1.invokeSuspend(ExampleUnitTest.kt:27)at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2222222222222

与SupervisorJob相匹配的还有SupervisorScope,使用直接使用作用域构建器SupervisorScope也可以达到相同的效果。
代码如下

@Testfun `test supervisorScope`() = runBlocking<Unit> {supervisorScope {launch {delay(100)println("child 1")throw IllegalArgumentException()}try {delay(Long.MAX_VALUE)} finally {println("child 2 finished.")}}}

打印输出如下

child 1
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IllegalArgumentExceptionat com.example.testkotlin1030.ExampleUnitTest$my test$1$job1$1.invokeSuspend(ExampleUnitTest.kt:27)at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

但是,我们作用域自身执行失败的时候(错误代码不是在子协成里面),所有的子协程都会被取消

代码如下

   @Testfun `test supervisorScope2`() = runBlocking<Unit> {supervisorScope {//启动子协成val child = launch {try {println("The child is sleeping")delay(Long.MAX_VALUE)} finally {println("The child is cancelled")}}yield()println("Throwing an exception from the scope")throw AssertionError()//作用域里面抛异常}}

打印输出如下:

The child is sleeping
Throwing an exception from the scope
The child is cancelled
接下来是红色异常信息,此处省略……

学了那么多异常,就是为了对异常捕获

代码如下

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.AssertionError
import java.lang.Exception
import java.lang.IllegalArgumentException
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {//异常的捕获器val handler = CoroutineExceptionHandler{_,exception ->println("caught $exception")}//GlobalScope是根协程val job = GlobalScope.launch (handler){throw AssertionError()//该异常会被捕获}val deferred = GlobalScope.async (handler){throw ArithmeticException()//该异常不会被捕获}job.join()deferred.await()}
}

打印输出如下

caught AssertionError
红色异常信息

异常捕获的错误写法,

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.AssertionError
import java.lang.Exception
import java.lang.IllegalArgumentException
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {//异常处理器val handler = CoroutineExceptionHandler{_,exception ->println("caught $exception")}//作用域val scope = CoroutineScope(Job())val job = scope.launch {launch (handler){//异常捕获器应该安装到外部协程,而不是内部协程,throw IllegalArgumentException()}}job.join()}
}

运行直接崩溃,显示红色错误。表明没有捕获到异常

正确的写法应该是

package com.example.testkotlin1030import kotlinx.coroutines.*
import org.junit.Testimport org.junit.Assert.*
import java.lang.ArithmeticException
import java.lang.AssertionError
import java.lang.Exception
import java.lang.IllegalArgumentException
import java.lang.IndexOutOfBoundsException
import kotlin.coroutines.CoroutineContext
import kotlin.system.measureTimeMillis/*** Example local unit test, which will execute on the development machine (host).** See [testing documentation](http://d.android.com/tools/testing).*/
class ExampleUnitTest {@Testfun `my test`() = runBlocking<Unit> {val handler = CoroutineExceptionHandler { _, exception ->println("caught $exception")}val scope = CoroutineScope(Job())val job = scope.launch(handler) {launch {throw IllegalArgumentException()//回想上下文继承,异常抛出后是往父协程抛,父协程的handler就会捕获异常}}job.join()}
}

打印输出如下

caught java.lang.IllegalArgumentException

android 中捕获异常阻止程序闪退

package com.dongnaoedu.kotlincoroutineexceptionimport android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch/**** @author ningchuanqi* @version V1.0*/
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val handler = CoroutineExceptionHandler { _, exception ->Log.d("ning", "Caught $exception")}findViewById<Button>(R.id.button).also {it.setOnClickListener {//点击按钮启动协程GlobalScope.launch(handler ) {//如果没有handler捕获,点击程序闪退Log.d("ning", "on Click.")"abc".substring(10)}}}}}

运行结果如下

on Click
Caught java.lang.StringIndexOutofBoundException:length=3;index=10

程序没有闪退

package com.dongnaoedu.kotlincoroutineexceptionimport android.util.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlin.coroutines.CoroutineContext/**** @author ningchuanqi* @version V1.0*/
class GlobalCoroutineExceptionHandler : CoroutineExceptionHandler {override val key = CoroutineExceptionHandleroverride fun handleException(context: CoroutineContext, exception: Throwable) {Log.d("ning","Unhandled Coroutine Exception: $exception")//依然会崩溃,但会打印异常。可以在这里上报异常}
}

相关文件路径如下

取消与异常

 取消与异常紧密相关,协程内部使用CancellationException来进行取消,会抛出异常,这个异常会被忽略当子协程被取消时,不会取消它的父协程@Testfun `test cancel and exception`() = runBlocking<Unit> {val job = launch {//子协成val child = launch {try {delay(Long.MAX_VALUE)//如果这里增加try catch会发现,异常不会被静默处理,红色打印会出现} finally {println("Child is cancelled.")}}yield()//出让执行权,让子协成有机会执行println("Cancelling child")child.cancelAndJoin()yield()println("Parent is not cancelled")}job.join()}
执行到  child.cancelAndJoin()时候,子协程肯定被取消掉了。如果println("Parent is not cancelled")执行了,说明父协程还在,没有被取消掉。打印输出Cancelling childChild is cancelled.Parent is not cancelled说明父协程并没有被取消掉,取消异常不会向上传播
 //如果一个协程遇到了CancellationException以外的异常,它将使用该异常取消它的父协程。当//父协程的所有子协程都结束后,异常才会被父协程处理。//如果要释放子协程的资源,必须在最后的父协程里的异常处理器中释放。@Testfun `test cancel and exception2`() = runBlocking<Unit> {//异常处理器val handler = CoroutineExceptionHandler { _, exception ->println("Caught $exception")}val job = GlobalScope.launch(handler) {//父协程指定异常处理器//启动第一个子协成launch {try {delay(Long.MAX_VALUE)} finally {withContext(NonCancellable) {//子协成取消了,但是异常还没有处理,要等到所有的子协程都被取消了,异常才会被处理println("Children are cancelled, but exception is not handled until all children terminate")delay(100)//第一个子协成处理完毕println("The first child finished its non cancellable block")}}}
//启动第二个子线程,launch {delay(10)println("Second child throws an exception")throw ArithmeticException()}}job.join()}

运行的时候第二个子协成会抛异常,第一个子协成也会被取消,父协程等待两个子协成都执行完毕后才会处理异常
打印输出如下

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
Cautht java.lang.ArithmeticException
  //如果一个协程遇到了CancellationException以外的异常,它将使用该异常取消它的父协程。当//父协程的所有子协程都结束后,异常才会被父协程处理。@Testfun `test cancel and exception2`() = runBlocking<Unit> {val handler = CoroutineExceptionHandler { _, exception ->println("Caught $exception")}val job = GlobalScope.launch(handler) {launch {try {delay(Long.MAX_VALUE)} finally {withContext(NonCancellable) {println("Children are cancelled, but exception is not handled until all children terminate")delay(100)println("The first child finished its non cancellable block")}}}launch {delay(10)println("Second child throws an exception")throw ArithmeticException()}}job.join()}打印输出:Second child throws an exceptionChildren are cancelled, but exception is not handled until all children terminateThe first child finished its non cancellable blockCaught java.lang.ArithmeticException

@Testfun `test exception aggregation`() = runBlocking<Unit> {val handler = CoroutineExceptionHandler { _, exception ->//注意此处异常打印的写法,此处是一个数组,该数组中的异常会被遍历出来println("Caught $exception  ${exception.suppressed.contentToString()}")}val job = GlobalScope.launch(handler) {launch {try {delay(Long.MAX_VALUE)} finally {//1处抛出异常后,本协程作为兄弟协程,也会被取消掉,就会执行finally代码,2处异常就会执行throw ArithmeticException()  //2}}launch {try {delay(Long.MAX_VALUE)} finally {throw IndexOutOfBoundsException()  //3}}launch {delay(100)throw IOException()  //1 它抛出异常后,兄弟协程就会执行,,2处就会执行}}job.join()}}

打印输出
Caught java.io.IOException [java.lang.IndexOutOfBoundsException, java.lang.ArithmeticException]

王学岗Kotlin协程(三)---协程的上下文与协程的异常处理相关推荐

  1. 王学岗Kotlin协程(四)————Flow异步流

    参考文章 异步返回值的多个方案 1,什么时候用flow呢?----kotlin要表示多个值 如何表示多个值?挂起函数可以异步返回单个值,但是如果要异步返回多个计算好的值, 就只能用flow了.其它方案 ...

  2. Kotlin 学习笔记(十四)浅读协程

    上一篇-Kotlin 学习笔记(十三)高阶函数 为什么需要协程   举例一个异步编程中最常见的场景:后台线程执行一个A任务,下一个B任务依赖于A任务的执行结果,所以必须等待上一个任务执行完成后才能开始 ...

  3. Kotlin学习笔记26 协程part6 协程与线程的关系 Dispatchers.Unconfined 协程调试 协程上下文切换 Job详解 父子协程的关系

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 协程与线程的关系 import kotlinx.coroutines.* import java.util.concu ...

  4. 【Kotlin 协程】协程启动 ② ( 多协程控制 | launch 协程执行顺序控制 | Job#join() 函数 | async 协程执行顺序控制 | Deferred#await() 函数 )

    文章目录 一.launch 协程执行顺序控制 二.async 协程执行顺序控制 三.完整代码 源码地址 : https://download.csdn.net/download/han1202012/ ...

  5. python协程三种实现方法

    协程的三种方法 1,协程中名词 event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上.当满足事件发生的时候,调用相应的协程函数. coroutine 协程 ...

  6. C++协程(三):Understanding the promise type

    本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,本篇为第三篇,原文内容在https://lewissbaker.github.io/2018/09/05/un ...

  7. go 怎么等待所有的协程完成_优雅地等待子协程执行完毕

    goroutine模拟了线程级别的返场的能力,但它的执行需要主协程给机会.一般的作法用sleep,chan阻塞,看起来让人不爽,本文介绍sync.WaitGroup 类型结合 defer 的特性,给出 ...

  8. python进程线程协程区别_进程和线程、协程的区别

    现在多进程多线程已经是老生常谈了,协程也在最近几年流行起来.python中有协程库gevent,py web框架tornado中也用了gevent封装好的协程.本文主要介绍进程.线程和协程三者之间的区 ...

  9. java 协程线程的区别_线程和协程的区别的通俗说明

    表面上看协程和线程似乎是同一个东西,能达到的效果也相同,但是在底层的实现上却有着非常大的区别,在服务器端的绝大部分应用中,协程要比线程节省资源的多. 通俗易懂的讲,线程是操作系统的资源,当java程序 ...

最新文章

  1. MySql练习题参考答案
  2. shell处理curl返回数据_shell神器curl用法笔记
  3. JavaWeb(七)——Cookie、Session
  4. WinAPI: midiOutCachePatches - 预装音色
  5. 牛客16500 珠心算测试
  6. Function(函数)
  7. python做excel自动化-Python控制Excel实现自动化办公
  8. python的文本编辑geny_android模拟器(genymotion)+appium+python 框架执行基本原理(目前公司自己写的)...
  9. Android入门(四)UI-创建自定义控件
  10. Centos 编译安装mysql 5.6.21
  11. Win32编程day14 学习笔记
  12. 用ansi语法美化你的winrar和win启动界面 【 抄袭至互联网 作者不明】
  13. 百分字符知识付费教程
  14. 【无限互联】SDWebImage图片缓存流程分析
  15. 英特尔携手生态伙伴亮相InfoComm,赋能协作办公迈向智能时代
  16. TeamTalk的windows客户端流程
  17. Zemax学习笔记——序列模式点光源与平行光设置
  18. 京东历史价格查询的方法是?
  19. 如何保存gif表情包里面的部分图片?
  20. flink常用参数说明

热门文章

  1. windows10删除EFI分区(绝对安全)
  2. 12 年前我刷了 500 道,谈谈我的学习感受
  3. 微软中国CTO:手机里装的App一上网基本等于裸奔
  4. 8个免费的PNG素材网站推荐
  5. RooT最好软件,root手机最好的软件
  6. 中国麻纺行业竞争动态及产销需求预测报告(2022-2027年)
  7. Spring Security和Angular教程
  8. 联想模拟器无网络(快速)
  9. 感性认识spring的IoC
  10. Android图文识别