Android中的Coroutine協(xié)程原理解析
前言
協(xié)程是一個(gè)并發(fā)方案。也是一種思想。
傳統(tǒng)意義上的協(xié)程是單線程的,面對(duì)io密集型任務(wù)他的內(nèi)存消耗更少,進(jìn)而效率高。但是面對(duì)計(jì)算密集型的任務(wù)不如多線程并行運(yùn)算效率高。
不同的語(yǔ)言對(duì)于協(xié)程都有不同的實(shí)現(xiàn),甚至同一種語(yǔ)言對(duì)于不同平臺(tái)的操作系統(tǒng)都有對(duì)應(yīng)的實(shí)現(xiàn)。
我們kotlin語(yǔ)言的協(xié)程是 coroutines for jvm的實(shí)現(xiàn)方式。底層原理也是利用java 線程。
基礎(chǔ)知識(shí)
生態(tài)架構(gòu)
相關(guān)依賴庫(kù)
dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32" // 協(xié)程核心庫(kù) implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" // 協(xié)程Android支持庫(kù) implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3" // 協(xié)程Java8支持庫(kù) implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3" // lifecycle對(duì)于協(xié)程的擴(kuò)展封裝 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" }
為什么一些人總覺(jué)得協(xié)程晦澀難懂?
1.網(wǎng)絡(luò)上沒(méi)有詳細(xì)的關(guān)于協(xié)程的概念定義,每種語(yǔ)言、每個(gè)系統(tǒng)對(duì)其實(shí)現(xiàn)都不一樣。可謂是眾說(shuō)紛紜,什么內(nèi)核態(tài)用戶態(tài)巴拉巴拉,很容易給我們帶偏
2.kotlin的各種語(yǔ)法糖對(duì)我們?cè)斐傻母蓴_。如:
- 高階函數(shù)
- 源碼實(shí)現(xiàn)類(lèi)找不到
所以扎實(shí)的kotlin語(yǔ)法基本功是學(xué)習(xí)協(xié)程的前提。
實(shí)在看不懂得地方就反編譯為java,以java最終翻譯為準(zhǔn)。
協(xié)程是什么?有什么用?
kotlin中的協(xié)程干的事就是把異步回調(diào)代碼拍扁了,捋直了,讓異步回調(diào)代碼同步化。除此之外,沒(méi)有任何特別之處。
創(chuàng)建一個(gè)協(xié)程,就是編譯器背后偷偷生成一系列代碼,比如說(shuō)狀態(tài)機(jī)。
通過(guò)掛起和恢復(fù)讓狀態(tài)機(jī)狀態(tài)流轉(zhuǎn)實(shí)現(xiàn)把層層嵌套的回調(diào)代碼變成像同步代碼那樣直觀、簡(jiǎn)潔。
它不是什么線程框架,也不是什么高深的內(nèi)核態(tài),用戶態(tài)。它其實(shí)對(duì)于咱們安卓來(lái)說(shuō),就是一個(gè)關(guān)于回調(diào)函數(shù)的語(yǔ)法糖。。。
本文將會(huì)圍繞掛起與恢復(fù)徹底剖析協(xié)程的實(shí)現(xiàn)原理
Kotlin函數(shù)基礎(chǔ)知識(shí)復(fù)習(xí)
再Kotlin中函數(shù)是一等公民,有自己的類(lèi)型
函數(shù)類(lèi)型
fun foo(){} //類(lèi)型為 () -> Unit fun foo(p: Int){} //類(lèi)型為 (Int) -> String class Foo{ fun bar(p0: String,p1: Long):Any{} } //那么 bar 的類(lèi)型為:Foo.(String,Long) -> Any //Foo就是bar的 receiver。也可以寫(xiě)成 (Foo,String,Long) ->Any
函數(shù)引用
fun foo(){} //引用是 ::foo fun foo(p0: Int): String //引用也是 ::foo
咋都一樣?沒(méi)辦法,就這樣規(guī)定的。使用的時(shí)候 只能靠編譯器推斷
val f: () -> Unit = ::foo //編譯器會(huì)推斷出是fun foo(){} val g: (Int) -> String = ::foo //推斷為fun foo(p0: Int): String
帶Receiver的寫(xiě)法
class Foo{ fun bar(p0: String,p1: Long):Any{} }
val h: (Foo,String,Long) -> Any = Foo:bar
綁定receiver的函數(shù)引用:
val foo: Foo = Foo() val m: (String,Long) -> Any = foo:bar
額外知識(shí)點(diǎn)
val x: (Foo,String,Long) -> Any = Foo:bar val y: Function3<Foo,String,Long,Any> = x Foo.(String,Long) -> Any = (Foo,String,Long) ->Any = Function3<Foo,String,Long,Any>
函數(shù)作為參數(shù)傳遞
fun yy(p: (Foo,String,Long)->Any){ p(Foo(),"Hello",3L)//直接p()就能調(diào)用 //p.invoke(Foo(),"Hello",3L) 也可以用invoke形式 }
Lambda
就是匿名函數(shù),它跟普通函數(shù)比是沒(méi)有名字的,聽(tīng)起來(lái)好像是廢話
//普通函數(shù) fun func(){ println("hello"); } ? //去掉函數(shù)名 func,就成了匿名函數(shù) fun(){ println("hello"); //可以賦值給一個(gè)變量 val func = fun(){ //匿名函數(shù)的類(lèi)型 val func :()->Unit = fun(){ //Lambda表達(dá)式 val func={ print("Hello"); //Lambda類(lèi)型 val func :()->String = { print("Hello"); "Hello" //如果是Lambda中,最后一行被當(dāng)作返回值,能省掉return。普通函數(shù)則不行 //帶參數(shù)Lambda val f1: (Int)->Unit = {p:Int -> print(p); //可進(jìn)一步簡(jiǎn)化為 val f1 = {p:Int -> print(p); //當(dāng)只有一個(gè)參數(shù)的時(shí)候,還可以寫(xiě)成 val f1: (Int)->Unit = { print(it);
關(guān)于函數(shù)的個(gè)人經(jīng)驗(yàn)總結(jié)
函數(shù)跟匿名函數(shù)看起來(lái)沒(méi)啥區(qū)別,但是反編譯為java后還是能看出點(diǎn)差異
如果只是用普通的函數(shù),那么他跟普通java 函數(shù)沒(méi)啥區(qū)別。
比如 fun a()
就是對(duì)應(yīng)java方法public void a(){}
但是如果通過(guò)函數(shù)引用(:: a)來(lái)用這個(gè)函數(shù),那么他并不是直接調(diào)用fun a()
而是重新生成一個(gè)Function0
掛起函數(shù)
suspend 修飾。
掛起函數(shù)中能調(diào)用任何函數(shù)。
非掛起函數(shù)只能調(diào)用非掛起函數(shù)。
換句話說(shuō),suspend函數(shù)只能在suspend函數(shù)中調(diào)用。
簡(jiǎn)單的掛起函數(shù)展示:
//com.example.studycoroutine.chapter.CoroutineRun.kt suspend fun suspendFun(): Int { return 1; }
掛起函數(shù)特殊在哪?
public static final Object suspendFun(Continuation completion) { return Boxing.boxInt(1); }
這下理解suspend為啥只能在suspend里面調(diào)用了吧?
想要讓道貌岸然的suspend函數(shù)干活必須要先滿足它?。。【褪墙o它里面塞入一顆球。
然后他想調(diào)用其他的suspend函數(shù),只需將球繼續(xù)塞到其它的suspend方法里面。
普通函數(shù)里沒(méi)這玩意啊,所以壓根沒(méi)法調(diào)用suspend函數(shù)。。。
讀到這里,想必各位會(huì)有一些疑問(wèn):
- question1.這不是雞生蛋生雞的問(wèn)題么?第一顆球是哪來(lái)的?
- question2.為啥編譯后返回值也變了?
- question3.suspendFun 如果在協(xié)程體內(nèi)被調(diào)用,那么他的球(completion)是誰(shuí)?
標(biāo)準(zhǔn)庫(kù)給我們提供的最原始工具
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) { createCoroutineUnintercepted(completion).intercepted().resume(Unit) } ? public fun <T> (suspend () -> T).createCoroutine(completion: Continuation<T>): Continuation<Unit> = SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
以一個(gè)最簡(jiǎn)單的方式啟動(dòng)一個(gè)協(xié)程。
Demo-K1
fun main() { val b = suspend { val a = hello2() a } b.createCoroutine(MyCompletionContinuation()).resume(Unit) } ? suspend fun hello2() = suspendCoroutine<Int> { thread{ Thread.sleep(1000) it.resume(10086) class MyContinuation() : Continuation<Int> { override val context: CoroutineContext = CoroutineName("Co-01") override fun resumeWith(result: Result<Int>) { log("MyContinuation resumeWith 結(jié)果 = ${result.getOrNull()}")
兩個(gè)創(chuàng)建協(xié)程函數(shù)區(qū)別
startCoroutine 沒(méi)有返回值 ,而createCoroutine返回一個(gè)Continuation,不難看出是SafeContinuation
好像看起來(lái)主要的區(qū)別就是startCoroutine直接調(diào)用resume(Unit),所以不用包裝成SafeContinuation,而createCoroutine則返回一個(gè)SafeContinuation,因?yàn)椴恢缹?huì)在何時(shí)何處調(diào)用resume,必須保證resume只調(diào)用一次,所以包裝為safeContinuation
SafeContinuationd的作用是為了確保只有發(fā)生異步調(diào)用時(shí)才掛起
分析createCoroutineUnintercepted
//kotlin.coroutines.intrinsics.CoroutinesIntrinsicsH.kt @SinceKotlin("1.3") public expect fun <T> (suspend () -> T).createCoroutineUnintercepted(completion: Continuation<T>): Continuation<Unit>
先說(shuō)結(jié)論
其實(shí)可以簡(jiǎn)單的理解為kotlin層面的原語(yǔ),就是返回一個(gè)協(xié)程體。
開(kāi)始分析
引用代碼Demo-K1首先b 是一個(gè)匿名函數(shù),他肯定要被編譯為一個(gè)FunctionX,同時(shí)它還被suspend修飾 所以它肯定跟普通匿名函數(shù)編譯后不一樣。
編譯后的源碼為
public static final void main() { Function1 var0 = (Function1)(new Function1((Continuation)null) { int label; ? @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); Object var10000; switch(this.label) { case 0: ResultKt.throwOnFailure($result); this.label = 1; var10000 = TestSampleKt.hello2(this); if (var10000 == var3) { return var3; } break; case 1: var10000 = $result; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } int a = ((Number)var10000).intValue(); return Boxing.boxInt(a); } @NotNull public final Continuation create(@NotNull Continuation completion) { Intrinsics.checkParameterIsNotNull(completion, "completion"); Function1 var2 = new <anonymous constructor>(completion); return var2; public final Object invoke(Object var1) { return((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE); }); boolean var1 = false; Continuation var7 = ContinuationKt.createCoroutine(var0, (Continuation)(newMyContinuation())); Unit var8 = Unit.INSTANCE; boolean var2 = false; Companion var3 = Result.Companion; boolean var5 = false; Object var6 = Result.constructor-impl(var8); var7.resumeWith(var6); }
我們可以看到先是 Function1 var0 = new Function1
創(chuàng)建了一個(gè)對(duì)象,此時(shí)跟協(xié)程沒(méi)關(guān)系,這步只是編譯器層面的匿名函數(shù)語(yǔ)法優(yōu)化
如果直接
fun main() { suspend { val a = hello2() a }.createCoroutine(MyContinuation()).resume(Unit) }
也是一樣會(huì)創(chuàng)建Function1 var0 = new Function1
解答question1
繼續(xù)調(diào)用createCoroutine
再繼續(xù)createCoroutineUnintercepted ,找到在JVM平臺(tái)的實(shí)現(xiàn)
//kotlin.coroutines.intrinsics.IntrinsicsJVM.class @SinceKotlin("1.3") public actual fun <T> (suspend () -> T).createCoroutineUnintercepted( completion: Continuation<T> ): Continuation<Unit> { //probeCompletion還是我們傳入completion對(duì)象,在我們的Demo就是myCoroutine val probeCompletion = probeCoroutineCreated(completion)//probeCoroutineCreated方法點(diǎn)進(jìn)去看了,好像是debug用的.我的理解是這樣的 //This就是這個(gè)suspend lambda。在Demo中就是myCoroutineFun return if (this is BaseContinuationImpl) create(probeCompletion) else //else分支在我們demo中不會(huì)走到 //當(dāng) [createCoroutineUnintercepted] 遇到不繼承 BaseContinuationImpl 的掛起 lambda 時(shí),將使用此函數(shù)。 createCoroutineFromSuspendFunction(probeCompletion) { (this as Function1<Continuation<T>, Any?>).invoke(it) } }
@NotNull public final Continuation create(@NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function1 var2 = new <anonymous constructor>(completion); return var2; }
把completion傳入,并創(chuàng)建一個(gè)新的Function1,作為Continuation返回,這就是創(chuàng)建出來(lái)的協(xié)程體對(duì)象,協(xié)程的工作核心就是它內(nèi)部的狀態(tài)機(jī),invokeSuspend函數(shù)
調(diào)用 create
@NotNull public final Continuation create(@NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function1 var2 = new <anonymous constructor>(completion); return var2; }
把completion傳入,并創(chuàng)建一個(gè)新的Function1,作為Continuation返回,這就是創(chuàng)建出來(lái)的協(xié)程體對(duì)象,協(xié)程的工作核心就是它內(nèi)部的狀態(tài)機(jī),invokeSuspend函數(shù)
補(bǔ)充---相關(guān)類(lèi)繼承關(guān)系
解答question2&3
已知協(xié)程啟動(dòng)會(huì)調(diào)用協(xié)程體的resume,該調(diào)用最終會(huì)來(lái)到BaseContinuationImpl::resumeWith
internal abstract class BaseContinuationImpl{ fun resumeWith(result: Result<Any?>) { // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume var current = this var param = result while (true) { with(current) { val completion = completion!! // fail fast when trying to resume continuation without completion val outcome: Result<Any?> = try { val outcome = invokeSuspend(param)//調(diào)用狀態(tài)機(jī) if (outcome === COROUTINE_SUSPENDED) return Result.success(outcome) } catch (exception: Throwable) { Result.failure(exception) } releaseIntercepted() // this state machine instance is terminating if (completion is BaseContinuationImpl) { // unrolling recursion via loop current = completion param = outcome } else { //最終走到這里,這個(gè)completion就是被塞的第一顆球。 completion.resumeWith(outcome) return } } } } }
狀態(tài)機(jī)代碼截取
public final Object invokeSuspend(@NotNull Object $result) { Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); Object var10000; switch(this.label) { case 0://第一次進(jìn)來(lái) label = 0 ResultKt.throwOnFailure($result); // label改成1了,意味著下一次被恢復(fù)的時(shí)候會(huì)走case 1,這就是所謂的【狀態(tài)流轉(zhuǎn)】 this.label = 1; //全體目光向我看齊,我宣布個(gè)事:this is 協(xié)程體對(duì)象。 var10000 = TestSampleKt.hello2(this); if (var10000 == var3) { return var3; } break; case 1: ResultKt.throwOnFailure($result); var10000 = $result; break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } int a = ((Number)var10000).intValue(); return Boxing.boxInt(a); }
question3答案出來(lái)了,傳進(jìn)去的是create創(chuàng)建的那個(gè)continuation
最后再來(lái)聊聊question2,從上面的代碼已經(jīng)很清楚的告訴我們?yōu)樯稈炱鸷瘮?shù)反編譯后的返回值變?yōu)閛bject了。
以hello2為例子,hello2能返回代表掛起的白板,也能返回result。如果返回白板,狀態(tài)機(jī)return,協(xié)程掛起。如果返回result,那么hello2執(zhí)行完畢,是一個(gè)沒(méi)有掛起的掛起函數(shù),通常編譯器也會(huì)提醒 suspend 修飾詞無(wú)意義。所以這就是設(shè)計(jì)需要,沒(méi)有啥因?yàn)樗浴?/p>
最后,除了直接返回結(jié)果的情況,掛起函數(shù)一定會(huì)以resume結(jié)尾,要么返回result,要么返回異常。代表這個(gè)掛起函數(shù)返回了。
調(diào)用resume意義在于重新回調(diào)BaseContinuationImpl的resumeWith,進(jìn)而喚醒狀態(tài)機(jī),繼續(xù)執(zhí)行協(xié)程體的代碼。
換句話說(shuō),我們自定義的suspend函數(shù),一定要利用suspendCoroutine 獲得續(xù)體,即狀態(tài)機(jī)對(duì)象,否則無(wú)法實(shí)現(xiàn)真正的掛起與resume。
suspendCoroutine
我們可以不用suspendCoroutine,用更直接的suspendCoroutineUninterceptedOrReturn也能實(shí)現(xiàn),不過(guò)這種方式要手動(dòng)返回白板。不過(guò)一定要小心,要在合理的情況下返回或者不返回,不然會(huì)產(chǎn)生很多意想不到的結(jié)果
suspend fun mySuspendOne() = suspendCoroutineUninterceptedOrReturn<String> { continuation -> thread { TimeUnit.SECONDS.sleep(1) continuation.resume("hello world") } //因?yàn)槲覀冞@個(gè)函數(shù)沒(méi)有返回正確結(jié)果,所以必須返回一個(gè)掛起標(biāo)識(shí),否則BaseContinuationImpl會(huì)認(rèn)為完成了任務(wù)。 // 并且我們的線程又在運(yùn)行沒(méi)有取消,這將很多意想不到的結(jié)果 kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED }
而suspendCoroutine則沒(méi)有這個(gè)隱患
suspend fun mySafeSuspendOne() = suspendCoroutine<String> { continuation -> thread { TimeUnit.SECONDS.sleep(1) continuation.resume("hello world") } //suspendCoroutine函數(shù)很聰明的幫我們判斷返回結(jié)果如果不是想要的對(duì)象,自動(dòng)返 kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED }
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T = suspendCoroutineUninterceptedOrReturn { c: Continuation<T> -> //封裝一個(gè)代理Continuation對(duì)象 val safe = SafeContinuation(c.intercepted()) block(safe) //根據(jù)block返回結(jié)果判斷要不要返回COROUTINE_SUSPENDED safe.getOrThrow() }
SafeContinuation的奧秘
//調(diào)用單參數(shù)的這個(gè)構(gòu)造方法 internal actual constructor(delegate: Continuation<T>) : this(delegate, UNDECIDED) @Volatile private var result: Any? = initialResult //UNDECIDED賦值給 result //java原子屬性更新器那一套東西 private companion object { @Suppress("UNCHECKED_CAST") @JvmStatic private val RESULT = AtomicReferenceFieldUpdater.newUpdater<SafeContinuation<*>, Any?>( SafeContinuation::class.java, Any::class.java as Class<Any?>, "result" ) } ? internal actual fun getOrThrow(): Any? { var result = this.result // atomic read if (result === UNDECIDED) { //如果UNDECIDED,那么就把result設(shè)置為COROUTINE_SUSPENDED if (RESULT.compareAndSet(this, UNDECIDED, COROUTINE_SUSPENDED)) returnCOROUTINE_SUSPENDED result = this.result // reread volatile var return when { result === RESUMED -> COROUTINE_SUSPENDED // already called continuation, indicate COROUTINE_SUSPENDED upstream result is Result.Failure -> throw result.exception else -> result // either COROUTINE_SUSPENDED or data <-這里返回白板 } public actual override fun resumeWith(result: Result<T>) { while (true) { // lock-free loop val cur = this.result // atomic read。不理解這里的官方注釋為啥叫做原子讀。我覺(jué)得 Volatile只能保證可見(jiàn)性。 when { //這里如果是UNDECIDED 就把 結(jié)果附上去。 cur === UNDECIDED -> if (RESULT.compareAndSet(this, UNDECIDED, result.value)) return //如果是掛起狀態(tài),就通過(guò)resumeWith回調(diào)狀態(tài)機(jī) cur === COROUTINE_SUSPENDED -> if (RESULT.compareAndSet(this, COROUTINE_SUSPENDED, RESUMED)){ delegate.resumeWith(result) return } else -> throw IllegalStateException("Already resumed") } }
val safe = SafeContinuation(c.intercepted()) block(safe) safe.getOrThrow()
先回顧一下什么叫真正的掛起,就是getOrThrow返回了“白板”,那么什么時(shí)候getOrThrow能返回白板?答案就是result被初始化后值沒(méi)被修改過(guò)。那么也就是說(shuō)resumeWith沒(méi)有被執(zhí)行過(guò),即:block(safe)這句代碼,block這個(gè)被傳進(jìn)來(lái)的函數(shù),執(zhí)行過(guò)程中沒(méi)有調(diào)用safe的resumeWith。原理就是這么簡(jiǎn)單,cas代碼保證關(guān)鍵邏輯的原子性與并發(fā)安全
繼續(xù)以Demo-K1為例子,這里假設(shè)hello2運(yùn)行在一條新的子線程,否則仍然是沒(méi)有掛起。
{ thread{ Thread.sleep(1000) it.resume(10086) } }
總結(jié)
最后,可以說(shuō)開(kāi)啟一個(gè)協(xié)程,就是利用編譯器生成一個(gè)狀態(tài)機(jī)對(duì)象,幫我們把回調(diào)代碼拍扁,成為同步代碼。
到此這篇關(guān)于Android中的Coroutine協(xié)程原理詳解的文章就介紹到這了,更多相關(guān)Android Coroutine原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 實(shí)現(xiàn)抖音小游戲潛艇大挑戰(zhàn)的思路詳解
《潛水艇大挑戰(zhàn)》是抖音上的一款小游戲,最近特別火爆,很多小伙伴都玩過(guò)。接下來(lái)通過(guò)本文給大家分享Android 手?jǐn)]抖音小游戲潛艇大挑戰(zhàn)的思路,需要的朋友可以參考下2020-04-04android Retrofit2網(wǎng)絡(luò)請(qǐng)求封裝介紹
大家好,本篇文章主要講的是android Retrofit2網(wǎng)絡(luò)請(qǐng)求封裝介紹,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12Android?Banner本地和網(wǎng)絡(luò)輪播圖使用介紹
大家好,本篇文章講的是Android?Banner本地和網(wǎng)絡(luò)輪播圖使用介紹,感興趣的同學(xué)趕快來(lái)看一看吧,希望本篇文章對(duì)你起到幫助2021-11-11Android RecyclerView仿新聞?lì)^條的頻道管理功能
這篇文章主要介紹了Android RecyclerView仿新聞?lì)^條的頻道管理功能,需要的朋友可以參考下2017-06-06Android TextView多文本折疊展開(kāi)效果
這篇文章主要為大家詳細(xì)介紹了Android TextView多文本折疊展開(kāi)效果的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10Android智能指針輕量級(jí)Light Pointer初識(shí)
這篇文章主要為大家介紹了Android智能指針輕量級(jí)Light Pointer初識(shí)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Android開(kāi)發(fā)應(yīng)用第一步 安裝及配置模擬器Genymotion
這篇文章主要介紹了Android開(kāi)發(fā)應(yīng)用第一步,即安裝及配置模擬器Genymotion,感興趣的小伙伴們可以參考一下2015-12-12Android仿微信圖片選擇器ImageSelector使用詳解
這篇文章主要為大家詳細(xì)介紹了Android仿微信圖片選擇器ImageSelector的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android開(kāi)發(fā)之Android.mk模板的實(shí)例詳解
這篇文章主要介紹了Android開(kāi)發(fā)之Android.mk模板的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-10-10Android中ConstraintLayout約束布局的最全詳細(xì)解析
ConstraintLayout是Google在Google?I/O?2016大會(huì)上發(fā)布的一種新的布局容器(ViewGroup),它支持以靈活的方式來(lái)放置子控件和調(diào)整子控件的大小,下面這篇文章主要給大家介紹了關(guān)于Android中ConstraintLayout約束布局詳細(xì)解析的相關(guān)資料,需要的朋友可以參考下2022-08-08