欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Kotlin協(xié)程launch啟動(dòng)流程原理詳解

 更新時(shí)間:2022年12月08日 09:01:09   作者:無(wú)糖可樂(lè)愛(ài)好者  
這篇文章主要為大家介紹了Kotlin協(xié)程launch啟動(dòng)流程原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

1.launch啟動(dòng)流程

已知協(xié)程的啟動(dòng)方式之一是Globalscope.launch,那么Globalscope.launch的流程是怎樣的呢,直接進(jìn)入launch的源碼開(kāi)始看起。

fun main() {
    coroutineTest()
    Thread.sleep(2000L)
}
val block = suspend {
    println("Hello")
    delay(1000L)
    println("Kotlin")
}
private fun coroutineTest() {
    CoroutineScope(Job()).launch {
        withContext(Dispatchers.IO) {
            block.invoke()
        }
    }
}

反編譯后的Java代碼

public final class CoroutineDemoKt {
   @NotNull
   private static final Function1 block;
   public static final void main() {
      coroutineTest();
      Thread.sleep(2000L);
   }
   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
   @NotNull
   public static final Function1 getBlock() {
      return block;
   }
   private static final void coroutineTest() {
      BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)JobKt.Job$default((Job)null, 1, (Object)null)), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
         int label;
         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO();
               Function2 var10001 = (Function2)(new Function2((Continuation)null) {
                  int label;
                  @Nullable
                  public final Object invokeSuspend(@NotNull Object $result) {
                     Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                     switch(this.label) {
                     case 0:
                        ResultKt.throwOnFailure($result);
                        Function1 var10000 = CoroutineDemoKt.getBlock();
                        this.label = 1;
                        if (var10000.invoke(this) == var2) {
                           return var2;
                        }
                        break;
                     case 1:
                        ResultKt.throwOnFailure($result);
                        break;
                     default:
                        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                     }
                     return Unit.INSTANCE;
                  }
                  @NotNull
                  public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
                     Intrinsics.checkNotNullParameter(completion, "completion");
                     Function2 var3 = new <anonymous constructor>(completion);
                     return var3;
                  }
                  public final Object invoke(Object var1, Object var2) {
                     return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
                  }
               });
               this.label = 1;
               if (BuildersKt.withContext(var10000, var10001, this) == var2) {
                  return var2;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
            return Unit.INSTANCE;
         }
         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }
         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), 3, (Object)null);
   }
   static {
      Function1 var0 = (Function1)(new Function1((Continuation)null) {
         int label;
         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            String var2;
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               var2 = "Hello";
               System.out.println(var2);
               this.label = 1;
               if (DelayKt.delay(1000L, this) == var3) {
                  return var3;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
            var2 = "Kotlin";
            System.out.println(var2);
            return Unit.INSTANCE;
         }
         @NotNull
         public final Continuation create(@NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(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);
         }
      });
      block = var0;
   }
}

先分析一下上面代碼的流程:

  • 首先聲明了一個(gè)Function1類(lèi)型的block變量,這個(gè)變量就是demo中的block,然后會(huì)在static函數(shù)中會(huì)被賦值。
  • 接下來(lái)就是coroutineTest函數(shù)的調(diào)用。這個(gè)函數(shù)中的第一行代碼就是CoroutineScope的傳參和一些默認(rèn)值
  • 然后通過(guò)89行的invoke進(jìn)入到了外層狀態(tài)機(jī)流轉(zhuǎn)的過(guò)程
  • 95行的static表示的是內(nèi)部的掛起函數(shù)就是demo中的block.invoke,它是以匿名內(nèi)部類(lèi)的方式實(shí)現(xiàn),然后執(zhí)行內(nèi)部的狀態(tài)機(jī)流轉(zhuǎn)過(guò)程,最后給block賦值。
  • block被賦值后最終在Function1 var10000 = CoroutineDemoKt.getBlock();被調(diào)用

那么這個(gè)過(guò)程又是如何實(shí)現(xiàn)的,進(jìn)入launch源碼進(jìn)行查看:

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
}

這里的block指的就是demo中的block代碼段

再來(lái)看一下里面的幾行代碼的含義:

  • newCoroutineContext: 通過(guò)默認(rèn)的或者傳入的context創(chuàng)建一個(gè)新的Context;
  • coroutine: launch 會(huì)根據(jù)傳入的啟動(dòng)模式來(lái)創(chuàng)建對(duì)應(yīng)的協(xié)程對(duì)象。這里有兩種,一種是標(biāo)準(zhǔn)的,一種是懶加載的。
  • coroutine.start: 嘗試啟動(dòng)協(xié)程

2.協(xié)程是如何被啟動(dòng)的

通過(guò)launch的源碼可知協(xié)程的啟動(dòng)是通過(guò)coroutine.start啟動(dòng)的,那么協(xié)程的啟動(dòng)流程又是怎樣的?

public abstract class AbstractCoroutine<in T>(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
    ...
    /**
     * 用給定的代碼塊啟動(dòng)這個(gè)協(xié)程并啟動(dòng)策略。這個(gè)函數(shù)在這個(gè)協(xié)程上最多調(diào)用一次。
     */
    public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        start(block, receiver, this)
    }
}

start函數(shù)中傳入了三個(gè)參數(shù),只需要關(guān)注第一個(gè)參數(shù)即可。

public enum class CoroutineStart {
    ...
    /**
     * 用這個(gè)協(xié)程的啟動(dòng)策略啟動(dòng)相應(yīng)的塊作為協(xié)程。
     */
    public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
    when (this) {
        DEFAULT -> block.startCoroutineCancellable(completion)
        ATOMIC -> block.startCoroutine(completion)
        UNDISPATCHED -> block.startCoroutineUndispatched(completion)
        LAZY -> Unit // will start lazily
    }
}

啟動(dòng)策略的具體實(shí)現(xiàn)有三種方式,這里只需要分析startCoroutine,另外兩個(gè)其實(shí)就是它的基礎(chǔ)上增加了一些功能,其中前者代表啟動(dòng)協(xié)程以后可以在等待調(diào)度時(shí)取消,后者表示協(xié)程啟動(dòng)后不會(huì)被分發(fā)。

/**
 * 創(chuàng)建沒(méi)有接收方且結(jié)果類(lèi)型為T(mén)的協(xié)程,這個(gè)函數(shù)每次調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)新的可掛起的實(shí)例。
 */ 
public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}

createCoroutineUnintercepted在源代碼中只是一個(gè)聲明,它的具體實(shí)現(xiàn)是在IntrinsicsJvm.kt文件中。

//IntrinsicsJvm.kt#createCoroutineUnintercepted
/**
 * 創(chuàng)建沒(méi)有接收方且結(jié)果類(lèi)型為T(mén)的非攔截協(xié)程。這個(gè)函數(shù)每次調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)新的可掛起的實(shí)例。
 */
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
        create(probeCompletion)
    else
        createCoroutineFromSuspendFunction(probeCompletion) {
            (this as Function1<Continuation<T>, Any?>).invoke(it)
        }
}

actual代表了 createCoroutineUnintercepted() 在 JVM 平臺(tái)的實(shí)現(xiàn)。

createCoroutineUnintercepted是一個(gè)擴(kuò)展函數(shù),接收者類(lèi)型是一個(gè)無(wú)參數(shù),返回值為 T 的掛起函數(shù)或者 Lambda。

第9行代碼中的this代表的是(suspend () -> T)也就是invoke函數(shù)中的block變量,這個(gè)block變量就是demo中的block代碼段。

第9行的BaseContinuationImpl是一個(gè)抽象類(lèi)它實(shí)現(xiàn)了Continuation。

關(guān)于if (this is BaseContinuationImpl)的結(jié)果暫且不分析,先分析兩種情況下的create函數(shù):

  • create(probeCompletion):
//ContinuationImpl.kt#create
public open fun create(completion: Continuation<*>): Continuation<Unit> {
    throw UnsupportedOperationException("create(Continuation) has not been overridden")
}
public open fun create(value: Any?, completion: Continuation<*>): Continuation<Unit> {
    throw UnsupportedOperationException("create(Any?;Continuation) has not been overridden")
}

這個(gè)create函數(shù)拋出一個(gè)異常,意思就是這個(gè)create()沒(méi)有被重寫(xiě),而這個(gè)create()的重寫(xiě)就是在反編譯后的Java代碼中的create函數(shù)

@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
    Intrinsics.checkNotNullParameter(completion, "completion");
    Function2 var3 = new <anonymous constructor>(completion);
    return var3;
}
  • createCoroutineFromSuspendFunction(probeCompletion):
//IntrinsicsJvm.kt#createCoroutineFromSuspendFunction
/**
 * 當(dāng)一個(gè)被suspend修飾的lambda表達(dá)式?jīng)]有繼承BaseContinuationImpl類(lèi)時(shí),則通過(guò)此方法創(chuàng)建協(xié)程。
 *
 * 它發(fā)生在兩種情況下:
 * 1.lambda表達(dá)式中調(diào)用了其他的掛起方法
 * 2.掛起方法是通過(guò)Java實(shí)現(xiàn)的
 *
 * 必須將它封裝到一個(gè)擴(kuò)展[BaseContinuationImpl]的實(shí)例中,因?yàn)檫@是所有協(xié)程機(jī)制的期望。 
 */
private inline fun <T> createCoroutineFromSuspendFunction(
	completion: Continuation<T>,
	crossinline block: (Continuation<T>) -> Any?
		): Continuation<Unit> {
	val context = completion.context
	// context為空創(chuàng)建一個(gè)受限協(xié)程
	return if (context === EmptyCoroutineContext)
	//受限協(xié)程:只能調(diào)用協(xié)程作用域中提供的掛起方式掛起,其他掛起方法不能調(diào)用
	object : RestrictedContinuationImpl(completion as Continuation<Any?>) {
		private var label = 0
		override fun invokeSuspend(result: Result<Any?>): Any? =
		when (label) {
			0 -> {
				label = 1
				result.getOrThrow() // 如果試圖以異常開(kāi)始,則重新拋出異常(將被BaseContinuationImpl.resumeWith捕獲)
				block(this) // 運(yùn)行塊,可以返回或掛起
			}
			1 -> {
				label = 2
				result.getOrThrow() // 這是block掛起的結(jié)果
			}
			else -> error("This coroutine had already completed")
		}
	}
	else
	//創(chuàng)建一個(gè)正常的協(xié)程
	object : ContinuationImpl(completion as Continuation<Any?>, context) {
		private var label = 0
		override fun invokeSuspend(result: Result<Any?>): Any? =
		when (label) {
			0 -> {
				label = 1
				result.getOrThrow() // 如果試圖以異常開(kāi)始,則重新拋出異常(將被BaseContinuationImpl.resumeWith捕獲)
				block(this) // 運(yùn)行塊,可以返回或掛起
			}
			1 -> {
				label = 2
				result.getOrThrow() // 這是block掛起的結(jié)果
			}
			else -> error("This coroutine had already completed")
		}
	}
}

createCoroutineFromSuspendFunction就是當(dāng)一個(gè)被suspend修飾的Lambda表達(dá)式?jīng)]有繼承BaseContinuationImpl是才會(huì)被調(diào)用,然后根據(jù)上下文是否為空創(chuàng)建不同類(lèi)型的協(xié)程。

兩種情況都已經(jīng)分析完了,那么現(xiàn)在if (this is BaseContinuationImpl)會(huì)執(zhí)行哪一個(gè)呢,首先這里的this所指的就是demo中的block代碼段,Kotlin編譯器編譯后會(huì)自動(dòng)生成一個(gè)類(lèi)就是上面的static,它會(huì)繼承SuspendLambda類(lèi),而這個(gè)SuspendLambda類(lèi)繼承自ContinuationImpl,ContinuationImpl繼承自BaseContinuationImpl,因此可以得到判斷結(jié)果為true,

createCoroutineUnintercepted的過(guò)程就是協(xié)程創(chuàng)建的過(guò)程。

然后就是intercepted函數(shù),這個(gè)函數(shù)的具體實(shí)現(xiàn)也在IntrinsicsJvm.kt中,那么intercepted又做了什么呢

public expect fun <T> Continuation<T>.intercepted(): Continuation<T>
//具體實(shí)現(xiàn)
//IntrinsicsJvm.kt#intercepted
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    (this as? ContinuationImpl)?.intercepted() ?: this

首先有個(gè)強(qiáng)轉(zhuǎn),通過(guò)上面的分析這個(gè)強(qiáng)轉(zhuǎn)是一定會(huì)成功的,到這里intercepted就進(jìn)入到了ContinuationImpl中了

internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
	...
    @Transient
    private var intercepted: Continuation<Any?>? = null
	//如果沒(méi)有緩存,則從上下文獲取攔截器,調(diào)用interceptContinuation進(jìn)行攔截
	//將獲取到的內(nèi)容保存到全局變量
    public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
}

這里的ContinuationInterceptor指的就是Demo中傳輸?shù)?code>Dispatcher.IO,默認(rèn)值時(shí)Dispatcher.Default。

再回到startContinue中還剩最后一個(gè)resume

/**
 * 恢復(fù)執(zhí)行相應(yīng)的協(xié)程傳遞值作為最后一個(gè)掛起點(diǎn)的返回值。
 */
public inline fun <T> Continuation<T>.resume(value: T): Unit =
	resumeWith(Result.success(value))
public interface Continuation<in T> {
	/**
     * 與此延續(xù)相對(duì)應(yīng)的協(xié)程的上下文。
     */
	public val context: CoroutineContext
	/**
     * 恢復(fù)執(zhí)行相應(yīng)的協(xié)程傳遞值作為最后一個(gè)掛起點(diǎn)的返回值。
     */
	public fun resumeWith(result: Result<T>)
}

這里的resume(Unit)作用就相當(dāng)與啟動(dòng)了一個(gè)協(xié)程。

上面的啟動(dòng)流程中為了方便分析的是CoroutineStart.ATOMIC,而默認(rèn)的是CoroutineStart.DEFAULT,下面分析一下DEFAULT的流程

//Cancellable.kt#startCoroutineCancellable
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
    createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

startCoroutineCancellable對(duì)于協(xié)程的創(chuàng)建和攔截與ATOMIC是一樣的,區(qū)別就在于resumeCancellableWith

//DispatchedContinuation#resumeCancellableWith
public fun <T> Continuation<T>.resumeCancellableWith(
	result: Result<T>,
	onCancellation: ((cause: Throwable) -> Unit)? = null
): Unit = when (this) {
	is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
	else -> resumeWith(result)
}
// 我們內(nèi)聯(lián)它來(lái)保存堆棧上的一個(gè)條目,在它顯示的情況下(無(wú)限制調(diào)度程序)
// 它只在Continuation<T>.resumeCancellableWith中使用
@Suppress("NOTHING_TO_INLINE")
inline fun resumeCancellableWith(
	result: Result<T>,
	noinline onCancellation: ((cause: Throwable) -> Unit)?
		) {
	val state = result.toState(onCancellation)
	//是否需要分發(fā)
	if (dispatcher.isDispatchNeeded(context)) {
		_state = state
		resumeMode = MODE_CANCELLABLE
		//將可運(yùn)行塊的執(zhí)行分派給給定上下文中的另一個(gè)線(xiàn)程
		dispatcher.dispatch(context, this)
	} else {
		executeUnconfined(state, MODE_CANCELLABLE) {
			//協(xié)程未被取消
			if (!resumeCancelled(state)) {
				// 恢復(fù)執(zhí)行
				resumeUndispatchedWith(result)
			}
		}
	}
}
//恢復(fù)執(zhí)行前判斷協(xié)程是否已經(jīng)取消執(zhí)行
inline fun resumeCancelled(state: Any?): Boolean {
	//獲取當(dāng)前協(xié)程任務(wù)
	val job = context[Job]
	//如果不為空且不活躍
	if (job != null && !job.isActive) {
		val cause = job.getCancellationException()
		cancelCompletedResult(state, cause)
		//拋出異常
		resumeWithException(cause)
		return true
	}
	return false
}
//我們需要內(nèi)聯(lián)它來(lái)在堆棧中保存一個(gè)條目
inline fun resumeUndispatchedWith(result: Result<T>) {
	withContinuationContext(continuation, countOrElement) {
		continuation.resumeWith(result)
	}
}

以上就是Kotlin協(xié)程launch啟動(dòng)流程原理詳解的詳細(xì)內(nèi)容,更多關(guān)于Kotlin協(xié)程launch啟動(dòng)流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解用RxJava實(shí)現(xiàn)事件總線(xiàn)(Event Bus)

    詳解用RxJava實(shí)現(xiàn)事件總線(xiàn)(Event Bus)

    本篇文章主要介紹了用RxJava實(shí)現(xiàn)事件總線(xiàn)(Event Bus),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-11-11
  • Android Zxing二維碼掃描圖片拉伸問(wèn)題的解決方法

    Android Zxing二維碼掃描圖片拉伸問(wèn)題的解決方法

    這篇文章主要為大家詳細(xì)介紹了Android Zxing二維碼掃描圖片拉伸問(wèn)題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • Android側(cè)滑效果簡(jiǎn)單實(shí)現(xiàn)代碼

    Android側(cè)滑效果簡(jiǎn)單實(shí)現(xiàn)代碼

    這篇文章主要為大家詳細(xì)介紹了Android側(cè)滑效果簡(jiǎn)單實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • 通過(guò)源碼角度看看AccessibilityService

    通過(guò)源碼角度看看AccessibilityService

    這篇文章主要給大家介紹了關(guān)于通過(guò)源碼角度看看AccessibilityService的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2018-06-06
  • Jsoup 抓取頁(yè)面的數(shù)據(jù)實(shí)例詳解

    Jsoup 抓取頁(yè)面的數(shù)據(jù)實(shí)例詳解

    這篇文章主要介紹了Jsoup 抓取頁(yè)面的數(shù)據(jù)實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下
    2016-12-12
  • Android基于OpenCV實(shí)現(xiàn)霍夫直線(xiàn)檢測(cè)

    Android基于OpenCV實(shí)現(xiàn)霍夫直線(xiàn)檢測(cè)

    霍夫變換利用點(diǎn)與線(xiàn)之間的對(duì)偶性,將圖像空間中直線(xiàn)上離散的像素點(diǎn)通過(guò)參數(shù)方程映射為霍夫空間中的曲線(xiàn),并將霍夫空間中多條曲線(xiàn)的交點(diǎn)作為直線(xiàn)方程的參數(shù)映射為圖像空間中的直線(xiàn)。給定直線(xiàn)的參數(shù)方程,可以利用霍夫變換來(lái)檢測(cè)圖像中的直線(xiàn)。本文簡(jiǎn)單講解Android的實(shí)現(xiàn)
    2021-06-06
  • Android開(kāi)發(fā)中Launcher3常見(jiàn)默認(rèn)配置修改方法總結(jié)

    Android開(kāi)發(fā)中Launcher3常見(jiàn)默認(rèn)配置修改方法總結(jié)

    這篇文章主要介紹了Android開(kāi)發(fā)中Launcher3常見(jiàn)默認(rèn)配置修改方法,結(jié)合實(shí)例形式分析了Android Launcher3的功能與配置修改相關(guān)操作技巧,需要的朋友可以參考下
    2017-11-11
  • Android編程ViewPager回彈效果實(shí)例分析

    Android編程ViewPager回彈效果實(shí)例分析

    這篇文章主要介紹了Android編程ViewPager回彈效果,以實(shí)例形式較為詳細(xì)的分析了ViewPager回彈效果的相關(guān)使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-10-10
  • android短信管理器SmsManager實(shí)例詳解

    android短信管理器SmsManager實(shí)例詳解

    這篇文章主要為大家詳細(xì)介紹了android短信管理器SmsManager實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-11-11
  • Android圓形控件實(shí)現(xiàn)畫(huà)圓效果

    Android圓形控件實(shí)現(xiàn)畫(huà)圓效果

    這篇文章主要為大家詳細(xì)介紹了Android圓形控件實(shí)現(xiàn)畫(huà)圓效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-05-05

最新評(píng)論