一篇文章帶你瞭解——Kotlin協程
前言
Kotlin Coroutine 簡介
Kotlin Coroutine Version
Kotlin Coroutine 生態
接入Coroutine
dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32" // 協程核心庫 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" // 協程Android支援庫 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3" // 協程Java8支援庫 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3" // lifecycle對於協程的擴充套件封裝 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" }
Coroutine 基本使用
suspend
簡單用法
fun test1() { GlobalScope.launch { val arg1 = sunpendF1() var arg2 = sunpendF2() doLog("suspend finish arg1:$arg1 arg2:$arg2 result:${arg1 + arg2}") } } private suspend fun sunpendF1(): Int { delay(1000) doLog("suspend fun 1") return 2 } private suspend fun sunpendF2(): Int { delay(1000) doLog("suspend fun 2") return 4 }
01-07 19:21:49.626 9616-10074/com.yy.yylite.kotlinshare I/suspend: suspend fun 1 01-07 19:21:50.633 9616-10074/com.yy.yylite.kotlinshare I/suspend: suspend fun 2 suspend finish arg1:2 arg2:4 result:6
Suspend 掛起函式原理
suspend fun test1() { KLog.i("test1") { "test1" } val homeItemInfo = HomeItemInfo() homeItemInfo.adId = "89" delay(100) KLog.i("test1") { "test1-end" } }
public final Object test1(@NotNull Continuation var1) { Object $continuation; label28: { if (var1 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var1; if ((((<undefinedtype>)$continuation).getLabel() & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).setLabel(((<undefinedtype>)$continuation).getLabel() - Integer.MIN_VALUE); break label28; } } $continuation = new CoroutineImpl(var1) { // $FF: synthetic field Object data; // $FF: synthetic field Throwable exception; Object L$0; Object L$1; @Nullable public final Object doResume(@Nullable Object data, @Nullable Throwable throwable) { this.data = data; this.exception = throwable; super.label |= Integer.MIN_VALUE; return SuspendTest.this.test1(this); } // $FF: synthetic method final int getLabel() { return super.label; } // $FF: synthetic method final void setLabel(int var1) { super.label = var1; } }; } Object var3 = ((<undefinedtype>)$continuation).data; Throwable var4 = ((<undefinedtype>)$continuation).exception; Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); HomeItemInfo homeItemInfo; switch(((<undefinedtype>)$continuation).getLabel()) { case 0: if (var4 != null) { throw var4; } KLog.INSTANCE.i("test1", (Function0)null.INSTANCE); homeItemInfo = new HomeItemInfo(); homeItemInfo.adId = "89"; ((<undefinedtype>)$continuation).L$0 = this; ((<undefinedtype>)$continuation).L$1 = homeItemInfo; ((<undefinedtype>)$continuation).setLabel(1); if (DelayKt.delay(100, (Continuation)$continuation) == var6) { return var6; } break; case 1: homeItemInfo = (HomeItemInfo)((<undefinedtype>)$continuation).L$1; SuspendTest var7 = (SuspendTest)((<undefinedtype>)$continuation).L$0; if (var4 != null) { throw var4; } break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } KLog.INSTANCE.i("test1", (Function0)null.INSTANCE); return Unit.INSTANCE; }
public interface Continuation<in T> { /** * Context of the coroutine that corresponds to this continuation. */ // todo: shall we provide default impl with EmptyCoroutineContext? public val context: CoroutineContext /** * Resumes the execution of the corresponding coroutine passing successful or failed [result] as the * return value of the last suspension point. */ public fun resumeWith(result: Result<T>) }
kotlinx/coroutines/Deferred.delay (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
建立協程
CoroutineScope.launch()
import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import kotlinx.coroutines.* class MainActivity : AppCompatActivity() { /** * 使用官方庫的 MainScope()獲取一個協程作用域用於建立協程 */ private val mScope = MainScope() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 建立一個預設引數的協程,其預設的排程模式為Main 也就是說該協程的執行緒環境是Main執行緒 val job1 = mScope.launch { // 這裡就是協程體 // 延遲1000毫秒 delay是一個掛起函式 // 在這1000毫秒內該協程所處的執行緒不會阻塞 // 協程將執行緒的執行權交出去,該執行緒該幹嘛幹嘛,到時間後會恢復至此繼續向下執行 delay(1000) } // 建立一個指定了排程模式的協程,該協程的執行執行緒為IO執行緒 val job2 = mScope.launch(Dispatchers.IO) { // 此處是IO執行緒模式 // 切執行緒 將協程所處的執行緒環境切至指定的排程模式Main withContext(Dispatchers.Main) { // 現在這裡就是Main執行緒了 可以在此進行UI操作了 } } // 下面直接看一個例子: 從網路中獲取資料 並更新UI // 該例子不會阻塞主執行緒 mScope.launch(Dispatchers.IO) { // 執行getUserInfo方法時會將執行緒切至IO去執行 val userInfo = getUserInfo() // 獲取完資料後 切至Main執行緒進行更新UI withContext(Dispatchers.Main) { // 更新UI } } } /** * 獲取使用者資訊 該函式模擬IO獲取資料 * @return String */ private suspend fun getUserInfo(): String { return withContext(Dispatchers.IO) { delay(2000) "Kotlin" } } override fun onDestroy() { super.onDestroy() // 取消協程 防止協程洩漏 如果使用lifecycleScope則不需要手動取消 mScope.cancel() } }
CoroutineScope.async()
fun asyncTest() { mScope.launch { // 開啟一個IO模式的執行緒 並返回一個Deferred,Deferred可以用來獲取返回值 // 程式碼執行到此處時會新開一個協程 然後去執行協程體 父協程的程式碼會接著往下走 val deferred = async(Dispatchers.IO) { // 模擬耗時 delay(2000) // 返回一個值 "Quyunshuo" } // 等待async執行完成獲取返回值 此處並不會阻塞執行緒 而是掛起 將執行緒的執行權交出去 // 等到async的協程體執行完畢後 會恢復協程繼續往下執行 val date = deferred.await() } }
fun asyncTest2() { mScope.launch { // 此處有一個需求 同時請求5個介面 並且將返回值拼接起來 val job1 = async { // 請求1 delay(5000) "1" } val job2 = async { // 請求2 delay(5000) "2" } val job3 = async { // 請求3 delay(5000) "3" } val job4 = async { // 請求4 delay(5000) "4" } val job5 = async { // 請求5 delay(5000) "5" } // 程式碼執行到此處時 5個請求已經同時在執行了 // 等待各job執行完 將結果合併 Log.d( "TAG", "asyncTest2: ${job1.await()} ${job2.await()} ${job3.await()} ${job4.await()} ${job5.await()}" ) // 因為我們設定的模擬時間都是5000毫秒 所以當job1執行完時 其他job也均執行完成 } }
Coroutine的深入
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, // 協程上下文 start: CoroutineStart = CoroutineStart.DEFAULT, // 協程啟動模式 block: suspend CoroutineScope.() -> Unit // 執行在協程的邏輯 ): Job { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
CoroutineContext - 協程上下文
fun main() { val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext") println("$coroutineContext,${coroutineContext[CoroutineName]}") val newCoroutineContext = coroutineContext.minusKey(CoroutineName) println("$newCoroutineContext") }
[JobImpl{Active}@7eda2dbb, CoroutineName(myContext), Dispatchers.Default],CoroutineName(myContext) [JobImpl{Active}@7eda2dbb, Dispatchers.Default]
public interface CoroutineContext { public operator fun <E : Element> get(key: Key<E>): E? public fun <R> fold(initial: R, operation: (R, Element) -> R): R public operator fun plus(context: CoroutineContext): CoroutineContext{...} public fun minusKey(key: Key<*>): CoroutineContext public interface Key<E : Element> public interface Element : CoroutineContext {...} }
Job & Deferred - 任務
Job 的狀態
State | [isActive] | [isCompleted] | [isCancelled] |
---|---|---|---|
New (optional initial state) |
false |
false |
false |
Active (default initial state) |
true |
false |
false |
Completing (transient state) |
true |
false |
false |
Cancelling (transient state) |
false |
false |
true |
Cancelled (final state) |
false |
true |
true |
Completed (final state) |
false |
true |
false |
wait children +-----+ start +--------+ complete +-------------+ finish +-----------+ | New | -----> | Active | ---------> | Completing | -------> | Completed | +-----+ +--------+ +-------------+ +-----------+ | cancel / fail | | +----------------+ | | V V +------------+ finish +-----------+ | Cancelling | --------------------------------> | Cancelled | +------------+ +-----------+
Job 的常用函式
Deferred
public interface Deferred<out T> : Job { public val onAwait: SelectClause1<T> public suspend fun await(): T @ExperimentalCoroutinesApi public fun getCompleted(): T @ExperimentalCoroutinesApi public fun getCompletionExceptionOrNull(): Throwable? }
SupervisorJob
/** * Creates a _supervisor_ job object in an active state. * Children of a supervisor job can fail independently of each other. * * A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, * so a supervisor can implement a custom policy for handling failures of its children: * * * A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context. * * A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value. * * If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its * parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. The invocation of * [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent. * * @param parent an optional parent job. */ @Suppress("FunctionName") public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
/** * Creates the main [CoroutineScope] for UI components. * * Example of use: * ``` * class MyAndroidActivity { * private val scope = MainScope() * * override fun onDestroy() { * super.onDestroy() * scope.cancel() * } * } * ``` * * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements. * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator: * `val scope = MainScope() + CoroutineName("MyActivity")`. */ @Suppress("FunctionName") public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
CoroutineDispatcher - 排程器
CoroutineStart - 協程啟動模式
CoroutineScope - 協程作用域
CoroutineScope 介面
public interface CoroutineScope { public val coroutineContext: CoroutineContext }
分類及行為規則
常用作用域
GlobalScope - 不推薦使用
public object GlobalScope : CoroutineScope { /** * Returns [EmptyCoroutineContext]. */ override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext }
runBlocking{} - 主要用於測試
/** * Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion. * This function should not be used from a coroutine. It is designed to bridge regular blocking code * to libraries that are written in suspending style, to be used in `main` functions and in tests. * * The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations * in this blocked thread until the completion of this coroutine. * See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`. * * When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of * the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`, * then this invocation uses the outer event loop. * * If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and * this `runBlocking` invocation throws [InterruptedException]. * * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available * for a newly created coroutine. * * @param context the context of the coroutine. The default value is an event loop on the current thread. * @param block the coroutine code. */ @Throws(InterruptedException::class) public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } val currentThread = Thread.currentThread() val contextInterceptor = context[ContinuationInterceptor] val eventLoop: EventLoop? val newContext: CoroutineContext if (contextInterceptor == null) { // create or use private event loop if no dispatcher is specified eventLoop = ThreadLocalEventLoop.eventLoop newContext = GlobalScope.newCoroutineContext(context + eventLoop) } else { // See if context's interceptor is an event loop that we shall use (to support TestContext) // or take an existing thread-local event loop if present to avoid blocking it (but don't create one) eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() } ?: ThreadLocalEventLoop.currentOrNull() newContext = GlobalScope.newCoroutineContext(context) } val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop) coroutine.start(CoroutineStart.DEFAULT, coroutine, block) return coroutine.joinBlocking() }
MainScope() - 可用於開發
/** * Creates the main [CoroutineScope] for UI components. * * Example of use: * ``` * class MyAndroidActivity { * private val scope = MainScope() * * override fun onDestroy() { * super.onDestroy() * scope.cancel() * } * } * ``` * * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements. * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator: * `val scope = MainScope() + CoroutineName("MyActivity")`. */ @Suppress("FunctionName") public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
LifecycleOwner.lifecycleScope - 推薦使用
/** * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. */ val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope
ViewModel.viewModelScope - 推薦使用
/** * [CoroutineScope] tied to this [ViewModel]. * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] */ val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY) if (scope != null) { return scope } return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) }
coroutineScope & supervisorScope
/** * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope. * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides * context's [Job] with [SupervisorJob]. * * A failure of a child does not cause this scope to fail and does not affect its other children, * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details. * A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children, * but does not cancel parent job. */ public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return suspendCoroutineUninterceptedOrReturn { uCont -> val coroutine = SupervisorCoroutine(uCont.context, uCont) coroutine.startUndispatchedOrReturn(coroutine, block) } } /** * Creates a [CoroutineScope] and calls the specified suspend block with this scope. * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides * the context's [Job]. * * This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails, * this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]). * This function returns as soon as the given block and all its children coroutines are completed. * A usage example of a scope looks like this: * * ``` * suspend fun showSomeData() = coroutineScope { * val data = async(Dispatchers.IO) { // <- extension on current scope * ... load some UI data for the Main thread ... * } * * withContext(Dispatchers.Main) { * doSomeWork() * val result = data.await() * display(result) * } * } * ``` * * The scope in this example has the following semantics: * 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI. * 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception. * 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled. * 4) If the `async` block fails, `withContext` will be cancelled. * * The method may throw a [CancellationException] if the current job was cancelled externally * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope). */ public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return suspendCoroutineUninterceptedOrReturn { uCont -> val coroutine = ScopeCoroutine(uCont.context, uCont) coroutine.startUndispatchedOrReturn(coroutine, block) } }
協程的取消和異常
import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import kotlinx.coroutines.* class MainActivity : AppCompatActivity() { /** * 使用官方庫的 MainScope()獲取一個協程作用域用於建立協程 */ private val mScope = MainScope() companion object { const val TAG = "Kotlin Coroutine" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mScope.launch(Dispatchers.Default) { delay(500) Log.e(TAG, "Child 1") } mScope.launch(Dispatchers.Default) { delay(1000) Log.e(TAG, "Child 2") throw RuntimeException("--> RuntimeException <--") } mScope.launch(Dispatchers.Default) { delay(1500) Log.e(TAG, "Child 3") } } } 列印結果: E/Kotlin Coroutine: Child 1 E/Kotlin Coroutine: Child 2 E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3 Process: com.quyunshuo.kotlincoroutine, PID: 24240 java.lang.RuntimeException: --> RuntimeException <-- at com.quyunshuo.kotlincoroutine.MainActivity$onCreate$2.invokeSuspend(MainActivity.kt:31) 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) E/Kotlin Coroutine: Child 3
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mScope.launch(Dispatchers.Default) { delay(500) Log.e(TAG, "Child 1") } // 在Child 2的上下文新增了異常處理 mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable -> Log.e(TAG, "CoroutineExceptionHandler: $throwable") }) { delay(1000) Log.e(TAG, "Child 2") throw RuntimeException("--> RuntimeException <--") } mScope.launch(Dispatchers.Default) { delay(1500) Log.e(TAG, "Child 3") } } 輸出結果: E/Kotlin Coroutine: Child 1 E/Kotlin Coroutine: Child 2 E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <-- E/Kotlin Coroutine: Child 3
import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import kotlinx.coroutines.* class MainActivity : AppCompatActivity() { companion object { const val TAG = "Kotlin Coroutine" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch(CoroutineExceptionHandler { coroutineContext, throwable -> Log.e(TAG, "CoroutineExceptionHandler: $throwable") }) { supervisorScope { launch { delay(500) Log.e(TAG, "Child 1 ") } launch { delay(1000) Log.e(TAG, "Child 2 ") throw RuntimeException("--> RuntimeException <--") } launch { delay(1500) Log.e(TAG, "Child 3 ") } } } } } 輸出結果: E/Kotlin Coroutine: Child 1 E/Kotlin Coroutine: Child 2 E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <-- E/Kotlin Coroutine: Child 3
結語
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2839679/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Kotlin Coroutine(協程): 三、瞭解協程Kotlin
- 一篇文章帶你初步瞭解—CSS特指度CSS
- 一篇文章帶你瞭解和使用Promise物件Promise物件
- 一篇文章帶你瞭解HTML5 MathMLHTML
- 一篇文章帶你瞭解介面自動化
- 一篇文章帶你瞭解HTML格式化元素HTML
- 一篇文章帶你瞭解CSS 分頁例項CSS
- 一篇文章帶你瞭解高可用架構分析架構
- 一篇文章帶你瞭解設計模式——建立者模式設計模式
- 一篇文章帶你瞭解設計模式——結構型模式設計模式
- 一篇文章帶你瞭解如何測試訊息佇列佇列
- 你真的瞭解python嗎?這篇文章帶你快速瞭解!Python
- 一篇文章帶你瞭解高質量代理ip的使用技巧
- 一篇文章帶你瞭解Python基礎測試工具——UnitTestPython
- 機器學習到底是什麼?一篇文章帶你瞭解透徹機器學習
- 一篇帶你瞭解TCP/IP 概念TCP
- 一篇文章讓你瞭解Android各個版本的歷程Android
- 什麼是工藝流程圖?一篇文章帶你詳細瞭解流程圖
- 一篇文章帶你更深入瞭解區塊鏈有哪些應用?區塊鏈
- 一篇文章幫你瞭解 PHP 7.3 更新PHP
- 帶你瞭解TCP/IP協議族TCP協議
- 一篇文章帶你瞭解網路爬蟲的概念及其工作原理爬蟲
- 什麼是Python爬蟲?一篇文章帶你全面瞭解爬蟲Python爬蟲
- 一文章帶你瞭解微服務微服務
- 一篇文章帶你熟悉 TCP/IP 協議(網路協議篇二)TCP協議
- 一篇文章帶你瞭解Python常用自動化測試框架——PytestPython框架
- 一篇文章瞭解大前端前端
- Kotlin Coroutines(協程)講解Kotlin
- 一篇文章帶你吃透 Docker 原理Docker
- 一篇文章帶你入門Zookeeper
- 一篇文章讓你徹底瞭解Java內部類Java
- 一篇文章帶你瞭解 Java 自動記憶體管理機制及效能優化Java記憶體優化
- 一篇文章帶你瞭解設計模式原理——UML圖和軟體設計原則設計模式
- 帶你瞭解動態路由協議OSPF基礎路由協議
- 一篇文章瞭解JsBridgeJS
- 帶你瞭解webpackWeb
- IPIDEA帶你瞭解HTTP協議和SOCKS5協議IdeaHTTP協議
- 一篇文章帶你認識 SpringSecuritySpringGse