Kotlin?coroutineContext源碼層深入分析
1.CoroutineContext
表示一個(gè)元素或者是元素集合的接口。它有一個(gè)Key(索引)的Element實(shí)例集合,每一個(gè)Element的實(shí)例也是一個(gè)CoroutineContext,即集合中每個(gè)元素也是集合。
如下圖所示,CoroutineContext的常見(jiàn)官方實(shí)現(xiàn)有以下幾種(少見(jiàn)的或者自定義的實(shí)現(xiàn)就不列舉,以后再聊):
- Job:協(xié)程實(shí)例,控制協(xié)程生命周期(new、acruve、completing、conpleted、cancelling、cancelled)。
- CoroutineDIspatcher:協(xié)程調(diào)度器,給指定線程分發(fā)協(xié)程任務(wù)(IO、Default、Main、Unconfined)。
- CoroutineName:協(xié)程名稱(chēng),用于定義協(xié)程的名稱(chēng),調(diào)試打印信息使用。
- CoroutineExceptionHandler:協(xié)程異常處理器,用于處理未捕獲的異常。

2.Element的作用
Element類(lèi)也是繼承自CoroutineContext接口的,該類(lèi)的作用是給子類(lèi)保留一個(gè)Key成員變量,用于在集合查詢(xún)的時(shí)候可以快速查找到目標(biāo)coroutineContext,Key成員變量是一個(gè)泛型變量,每個(gè)繼承自Element的子類(lèi)都會(huì)去覆蓋實(shí)現(xiàn)Key成員變量(一般是使用子類(lèi)自己去覆蓋Key),就比如拿最簡(jiǎn)單的CoroutineName類(lèi)來(lái)舉例子:
public data class CoroutineName(
/**
* User-defined coroutine name.
*/
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
/**
* Key for [CoroutineName] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<CoroutineName>
/**
* Returns a string representation of the object.
*/
override fun toString(): String = "CoroutineName($name)"
}
@SinceKotlin("1.3")
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element
/**
* Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
*/
public interface Key<E : Element>
/**
* An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
*/
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}上面的CoroutineName構(gòu)造函數(shù)定義為
public data class CoroutineName(
/**
* User-defined coroutine name.
*/
val name: String
) : AbstractCoroutineContextElement(CoroutineName)
父類(lèi)構(gòu)造函數(shù)中傳遞的參數(shù)是CoroutineName,但是我們發(fā)現(xiàn)CoroutineName也不是Key接口public interface Key<E : Element>的實(shí)現(xiàn),為啥可以這樣直接傳遞呢?但是我們仔細(xì)看發(fā)現(xiàn)CoroutineName類(lèi)定義了伴生對(duì)象: public companion object Key : CoroutineContext.Key<CoroutineName>,在kotlin中伴生對(duì)象是可以直接省略 類(lèi).companion.調(diào)用方式的,CoroutineName類(lèi)也就代表著伴生對(duì)象,所以可以直接作為CoroutineName父類(lèi)構(gòu)造函數(shù)的參數(shù),神奇的kotlin語(yǔ)法搞得我一愣一愣的。
類(lèi)似的還有Job,CoroutineDIspatcher,CoroutineExceptionHandler的成員變量Key的覆蓋實(shí)現(xiàn):
//Job
public interface Job : CoroutineContext.Element {
/**
* Key for [Job] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<Job> { //省略 }
//省略
}
//CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element {
/**
* Key for [CoroutineExceptionHandler] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
//省略
}
// CoroutineDIspatcher
@SinceKotlin("1.3")
public interface ContinuationInterceptor : CoroutineContext.Element {
/**
* The key that defines *the* context interceptor.
*/
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}3.CoroutineContext相關(guān)的操作符原理解析
CoroutineContext的操作符??有點(diǎn)莫名其妙的感覺(jué),僅僅憑借我的直覺(jué)的話很難理解,但是平常使用協(xié)程的過(guò)程中,我們經(jīng)常會(huì)使用這些相關(guān)的操作符,比如 +,[]等等符號(hào),下面代碼示例:
val comb = Job() + CoroutineName("")
val cName = comb[CoroutineName]
上面的+代表兩個(gè)coroutineContext合并到集合中,這里的集合實(shí)際上是一個(gè)鏈表,后面會(huì)講到。
上面的[]代表著從集合中索引出CoroutineName類(lèi)型的CoroutineContext,這里也可以看出來(lái)僅僅通過(guò)key就查找出元素和map很相似,那么可以知道value是唯一的。key都是coroutineContext子類(lèi)作為泛型類(lèi)型的,具有唯一性,那也可以間接推斷出上面+操作其實(shí)也會(huì)覆蓋擁有相同key的value的值。
還有其他操作函數(shù):fold展開(kāi)操作, minusKey刪除集合中存在的元素。
還有一個(gè)問(wèn)題就是,這個(gè)集合到底是什么類(lèi)型的集合,已經(jīng)如何管理的,我們來(lái)一一解答:
3.1.什么類(lèi)型的集合
CoroutineConetxt集合是鏈表結(jié)構(gòu)的集合,是一個(gè)從本節(jié)點(diǎn)開(kāi)始,向左遍歷parent節(jié)點(diǎn)的一個(gè)鏈表,節(jié)點(diǎn)的都是CoroutineContext的子類(lèi),分為Element,CombinedContext,EmptyCoroutineContext三種。
有以下代碼作為舉例:
val scope = CoroutineScope(CoroutineName("") + Job() + CoroutineExceptionHandler{<!--{C}%3C!%2D%2D%20%2D%2D%3E--> _, _ -> } + Dispatchers.Default)假如CoroutineScope自己的coroutineContext變量集合中是包含CoroutineName,Job,CoroutineExceptionHanlder,CoroutineDIspatcher四種上下文的,那么他們組成的集合結(jié)構(gòu)可能就會(huì)是下圖所示的鏈表結(jié)構(gòu),

使用scope查找對(duì)應(yīng)的Job的話直接調(diào)用scope[Job]方法,替代Job的話調(diào)用 scope + Job(),看源碼就是使用scope的上下文集合替換Job
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
ContextScope(coroutineContext + context)
為啥是鏈表結(jié)構(gòu)的集合呢,接下來(lái)直接看源碼就知道了。
3.2.如何管理
我們集合的鏈表結(jié)構(gòu),每個(gè)節(jié)點(diǎn)都是CombinedContext類(lèi)型,里面包含了element,left兩個(gè)成員變量,left指向鏈表的左邊,element表示當(dāng)前節(jié)點(diǎn)的上下文元素(一般是job,name,handler,dispatcher四種),鏈表的最左端節(jié)點(diǎn)一定是Element元素
Element
主要實(shí)現(xiàn)在combinedContext,Element元素的方法實(shí)現(xiàn)比較簡(jiǎn)單,不單獨(dú)列舉。
combinedContext
構(gòu)造函數(shù)
@SinceKotlin("1.3")
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
get函數(shù):
//Element
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
//CombinedContext
override fun <E : Element> get(key: Key<E>): E? {
var cur = this
while (true) {
cur.element[key]?.let { return it }
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return next[key]
}
}
}在代碼中一般不會(huì)使用get方法,而是使用context[key]來(lái)代替,類(lèi)似于map集合的查詢(xún)。上下文是Element類(lèi)型,key是對(duì)應(yīng)類(lèi)型那么返回當(dāng)前Element,不是當(dāng)前類(lèi)型,返回null;上下文是CombinedContext類(lèi)型,指針cur指向當(dāng)前節(jié)點(diǎn),while循環(huán)開(kāi)始,當(dāng)前的element元素的key查找到了,那么就返回當(dāng)前combinedContext,如果沒(méi)找到,那么將指針指向left節(jié)點(diǎn),如果left節(jié)點(diǎn)是combinedContext類(lèi)型,那么重復(fù)上述操作,如果是Element類(lèi)型直接判斷是否可以查找到key值。那么從這里看出鏈表的最左端元素一定是Element節(jié)點(diǎn)。
contain函數(shù)
private fun contains(element: Element): Boolean =
get(element.key) == element
private fun containsAll(context: CombinedContext): Boolean {
var cur = context
while (true) {
if (!contains(cur.element)) return false
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return contains(next as Element)
}
}
}類(lèi)似于get操作,contains函數(shù)直接調(diào)用get方法來(lái)判斷元素是不是和傳入?yún)?shù)相等。
containAll函數(shù)就是遍歷參數(shù)的鏈表節(jié)點(diǎn)是不是都包含在當(dāng)前鏈表中。
fold函數(shù)
//coroutineContext
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
從表面意思就是展開(kāi)操作,第一個(gè)入?yún)?CoroutineContext,第二個(gè)入?yún)?lambda表達(dá)式 用表達(dá)式的兩個(gè)參數(shù)CoroutineContext, Element 返回一個(gè)新的 CoroutineContext:
operation :(R , Element) -> R
Job.fold(CoroutineName("測(cè)試"),{ coroutineContext , element ->
TODO("return new CoroutineContext")
}) //example
//Element
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
作為receiver的上下文是Element,調(diào)用fold的話,是讓ELement和入?yún)⒌腃oroutineContext作為lambda表達(dá)式 的兩個(gè)參數(shù)調(diào)用該lambda表達(dá)式返回結(jié)果。
MainScope().coroutineContext.fold(Job(),{ coroutineContext , element ->
TODO("return new CoroutineContext")
}) //example
//CombinedContext
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)
作為receiver的上下文是CombinedContext,調(diào)用fold的話,是讓left深度遞歸調(diào)用fold函數(shù),一直到鏈表的最左端節(jié)點(diǎn),我們知道鏈表的最左端節(jié)點(diǎn)一定是Element,那么根據(jù)上面的代碼,Element的fold函數(shù)內(nèi)調(diào)用operation返回一個(gè)CoroutineContext后,遞歸回溯到上一層,繼續(xù)調(diào)用operation返回一個(gè)CoroutineContext,繼續(xù)回溯,一直回溯到開(kāi)始調(diào)用MainScope().coroutineContext.fold(Job的地方。如下圖所示:

minusKey函數(shù)
該函數(shù)的意思是從上下文集合中刪除key對(duì)應(yīng)的上下文。
//Element
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
接收者receiver是Element類(lèi)型的話,如果入?yún)ey和receiver是相等的話,那么返回EmptyCoroutineContext空上下文,否則返回receiver本身。(可見(jiàn)找到得到key的話會(huì)返回空上下文,找不到的話返回本身)
//CombinedContext
public override fun minusKey(key: Key<*>): CoroutineContext {
element[key]?.let { return left }
val newLeft = left.minusKey(key)
return when {
newLeft === left -> this
newLeft === EmptyCoroutineContext -> element
else -> CombinedContext(newLeft, element)
}
}
接收者receiver是CombinedContext類(lèi)型的話,
- element[key]不為空說(shuō)明當(dāng)前節(jié)點(diǎn)就是要找的節(jié)點(diǎn),直接返回該節(jié)點(diǎn)的left節(jié)點(diǎn)(代表著把當(dāng)前節(jié)點(diǎn)跳過(guò),也就是移除該節(jié)點(diǎn))。
- element[key]為空那么說(shuō)明當(dāng)前節(jié)點(diǎn)不是要找的節(jié)點(diǎn),需要向鏈表的左端left去尋找目標(biāo),深度遞歸遍歷
left.minusKey(key),返回的newLeft有三種情況: - newLeft === left,在左邊找不到目標(biāo)Key(根據(jù)Element.minusKey函數(shù)發(fā)現(xiàn),返回的是this的話就是key沒(méi)有匹配到),從該節(jié)點(diǎn)到左端節(jié)點(diǎn)都可以返回。
- newLeft === EmptyCoroutineContext ,在左邊找到了目標(biāo)key(根據(jù)Element.minusKey函數(shù)發(fā)現(xiàn),返回的是EmptyCoroutineContext 的話key匹配到了Element),找到了目標(biāo)那么需要將目標(biāo)跳過(guò),那么從本節(jié)點(diǎn)開(kāi)始返回,左邊節(jié)點(diǎn)需要跳過(guò)移除,該節(jié)點(diǎn)就成了鏈表的最左端節(jié)點(diǎn)Element。
- 不是上述的情況,那么就是newLeft是觸發(fā)了1.或者4.情況,返回的是left的element元素,或者是本節(jié)點(diǎn)的left節(jié)點(diǎn)跳過(guò)了,返回的是left.left節(jié)點(diǎn),這樣將newLeft和本節(jié)點(diǎn)的element構(gòu)造出新的CombinedContext節(jié)點(diǎn)。
上述操作,都只是在鏈表上跳過(guò)節(jié)點(diǎn),然后將跳過(guò)的節(jié)點(diǎn)左節(jié)點(diǎn)left和右節(jié)點(diǎn)創(chuàng)建新的CombinedContext,產(chǎn)生一個(gè)新的鏈表出來(lái)。
操作例子:
刪除最左端節(jié)點(diǎn)

刪除中間節(jié)點(diǎn):

結(jié)論:minusKey的操作只是將原始鏈表集合中排除某一個(gè)節(jié)點(diǎn),然后復(fù)制一個(gè)鏈表返回,所以并不會(huì)影響原始集合
plus函數(shù)
該函數(shù)重寫(xiě)+操作符,函數(shù)定義operator fun plus(context: CoroutineContext): CoroutineContext ,作用是對(duì)上下文集合進(jìn)行添加(相同會(huì)覆蓋)指定上下文操作。這個(gè)函數(shù)只有CoroutineContext實(shí)現(xiàn)了,代碼如下:
/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}入?yún)⑷绻荅mptyContext,那么直接返回;不是空的話,對(duì)入?yún)⑦M(jìn)行fold操作,上面講了fold操作是將context鏈表展開(kāi),從鏈表最左端開(kāi)始向context回溯調(diào)用fold函數(shù)的入?yún)ambda表達(dá)式。那么我們就知道了 是如何操作的了,首先B作為plus的入?yún)?,那么B先展開(kāi)到B鏈表結(jié)構(gòu)的最左端,然后執(zhí)行l(wèi)ambda操作{ acc, element -> ... }, 這個(gè)lambda里面
第一步
context.fold(this) { acc, element ->
acc.minusKey(Element.Key)
// ...
}
//CombineContext的實(shí)現(xiàn)
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)根據(jù)CombineContext的實(shí)現(xiàn),知道lambda的acc參數(shù)是A (CombineContext),element參數(shù)是B(CombineContext)的fold遞歸的當(dāng)前位置的element的元素,acc.minusKey(Element.Key)所做的事情就是移除A (CombineContext)鏈表中的B(CombineContext)的element元素。
第二步
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
// ...
}
第一步移除掉element之后,判斷剩余的removed鏈表是不是empty的,如果為空,返回B(CombineContext)的fold遞歸位置的element元素;不為空,接著從removed鏈表中獲取ContinuationInterceptor上下文(也就是dispatcher)。
第三步
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
獲取的interceptor為空,那將element和removed鏈表構(gòu)造出一個(gè)新的CombinedContext節(jié)點(diǎn)返回;如果不為空,從removed鏈表中移除interceptor返回一個(gè)不包含interceptor的鏈表left;移除后left鏈表為空,那么將element和interceptor構(gòu)造出一個(gè)新的CombinedContext節(jié)點(diǎn)返回;left鏈表不為空,那么將left, element構(gòu)造出一個(gè)新的CombinedContext節(jié)點(diǎn),將新的CombinedContext節(jié)點(diǎn)和interceptor早構(gòu)造出一個(gè)新的節(jié)點(diǎn)返回。
每一層遞歸fold操作結(jié)束后,返回一個(gè)新的context給上一層繼續(xù)遞歸,直到結(jié)束為止。
操作例子圖如下:
有如下兩個(gè)集合A (CombineContext) + B(CombineContext):

第一次遞歸回溯:

第二次遞歸回溯:

回溯深度取決于入?yún)的鏈表長(zhǎng)度,B有多長(zhǎng)回溯就會(huì)發(fā)生幾次,這里沒(méi)有加入interceptor上下文元素,減少畫(huà)圖復(fù)雜度。
plus操作結(jié)論:
1.發(fā)現(xiàn)每次返回節(jié)點(diǎn)的時(shí)候,都會(huì)將interceptor移除后,放到節(jié)點(diǎn)的最右邊的位置,可以知道interceptor一定在鏈表的頭部;
2.lambda表達(dá)式中,一定會(huì)先移除掉相同key的上下文元素,然后用后加入的element和left鏈表新建一個(gè)CombinedContext節(jié)點(diǎn)插入到頭部
3.plus操作會(huì)覆蓋掉有相同key的上下文元素
4.驗(yàn)證以及總結(jié)
經(jīng)過(guò)對(duì)上面的源碼的分析,可以推斷出一些上下文元素的操作符操作后,集合的元素排列狀態(tài)。比如下面操作:
private fun test() {
val coroutineContext = Job() + CoroutineName("name1") + Dispatchers.IO + CoroutineExceptionHandler{ c,e -> }
Log.i(TAG, "coroutineContext $coroutineContext")
val newContext = coroutineContext + SupervisorJob()
Log.i(TAG, "newContext $newContext")
val newContext2 = newContext + (Job() + CoroutineName("name2"))
Log.i(TAG, "newContext2 $newContext2")
Log.i(TAG, "newContext2[CoroutineName] ${newContext2[CoroutineName]}")
}打印的日志如下:
I/MainActivity: coroutineContext [
JobImpl{Active}@b32c44,
CoroutineName(name1),
com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d,
Dispatchers.IO
]I/MainActivity: newContext [
CoroutineName(name1),
com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d,
SupervisorJobImpl{Active}@1022662,
Dispatchers.IO
]
I/MainActivity: newContext2 [
com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d,
JobImpl{Active}@76863f3,
CoroutineName(name2),
Dispatchers.IO
]I/MainActivity: newContext2[CoroutineName] CoroutineName(name2)
I/MainActivity: coroutineContext [
JobImpl{Active}@b32c44,
CoroutineName(name1),
com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d,
Dispatchers.IO
]
可以看出來(lái):
1. Dispatchers元素一定是在鏈表的頭部;
2. 重復(fù)key的元素會(huì)被后加入的元素覆蓋,集合中不存在重復(fù)key的元素;
3. +操作后返回新的鏈表集合,不會(huì)影響原始集合鏈表結(jié)構(gòu)
上面總結(jié)的這些性質(zhì),可以很好的為job協(xié)程的父子關(guān)系,子job繼承父job的上下文集合這些特性,下一篇我將講解 協(xié)程Job父子關(guān)系的原理。
到此這篇關(guān)于Kotlin coroutineContext源碼層深入分析的文章就介紹到這了,更多相關(guān)Kotlin coroutineContext內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中AlertDilog顯示簡(jiǎn)單和復(fù)雜列表的方法
這篇文章主要介紹了Android中AlertDialog顯示簡(jiǎn)單和復(fù)雜列表的方法,結(jié)合實(shí)例形式分析了Android的AlertDialog創(chuàng)建列表顯示對(duì)話框的相關(guān)方法與常見(jiàn)操作技巧,需要的朋友可以參考下2016-08-08
Android實(shí)現(xiàn)圖片疊加效果的兩種方法
這篇文章主要介紹了Android實(shí)現(xiàn)圖片疊加效果的兩種方法,結(jié)合實(shí)例形式分析了Android實(shí)現(xiàn)圖片疊加效果的兩種操作方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-08-08
Android自定義控件之三點(diǎn)循環(huán)縮放效果
這篇文章主要為大家詳細(xì)介紹了Android自定義控件之三點(diǎn)循環(huán)縮放效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10
Android的TextView與Html相結(jié)合的具體方法
Android的TextView與Html相結(jié)合的具體方法,需要的朋友可以參考一下2013-06-06
Android開(kāi)關(guān)控件Switch的使用案例
今天小編就為大家分享一篇關(guān)于Android開(kāi)關(guān)控件Switch的使用案例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03
Android啟動(dòng)頁(yè)設(shè)置及動(dòng)態(tài)權(quán)限跳轉(zhuǎn)問(wèn)題解決
在我遇到這個(gè)實(shí)際問(wèn)題之前,我一直認(rèn)為啟動(dòng)頁(yè)的作用是美化產(chǎn)品,提升軟件逼格。但實(shí)際上,它更重要的是起到了一個(gè)攔截器的作用,這篇文章主要介紹了Android啟動(dòng)頁(yè)設(shè)置以及動(dòng)態(tài)權(quán)限跳轉(zhuǎn),需要的朋友可以參考下2022-04-04
Android 進(jìn)度條顯示在標(biāo)題欄的實(shí)現(xiàn)方法
android進(jìn)度條顯示在標(biāo)題欄的實(shí)現(xiàn)方法,大概分文xml文件和java文件,具體代碼內(nèi)容大家可以通過(guò)本文學(xué)習(xí)下2017-01-01
解決android studio android monitor打不開(kāi)的問(wèn)題
下面小編就為大家分享一篇解決android studio android monitor打不開(kāi)的問(wèn)題,具有很的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01

