Android 协程使用指南

Posted 小陈乱敲代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 协程使用指南相关的知识,希望对你有一定的参考价值。

协程是什么

协程是我们在 android 上进行异步编程的推荐解决方案之一,通过挂起和恢复让状态机状态流转实现把层层嵌套的回调代码变成像同步代码那样直观、简洁,协程的出现很好的避免了回调地狱的出现。

所谓挂起,是指挂起协程,而非挂起线程,并且这个操作对线程是非阻塞式的。当线程执行到协程的 suspend 函数的时候,对于线程而言,线程会被回收或者再利用执行其他工作,就像主线程其实是会继续 UI 刷新工作。而对于协程本身,会根据 withContext 传入的 Dispatchers 所指定的线程去执行任务。

关于恢复,当挂起函数执行完毕后,会自动根据 CoroutineContext 切回原来的线程往下执行。

协程怎样集成

dependencies 
    // -----1----
    // Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.30"

    // -----2----
    // 协程核心库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
    // 协程 Android 支持库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"
  
    // -----3----
    // lifecycle 对于协程的扩展封装
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
 

其中 part 3 主要是对写 view 层的一些库,lifecycle 对于协程的扩展封装在业务开发上非常重要。

下面,介绍一些使用上的一些基本概念

CoroutineScope

CoroutineScope 是指协程作用域,它其实是一个接口,作用是使得协程运行在其范围内

public interface CoroutineScope 
    public val coroutineContext: CoroutineContext


public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job()) 

执行协程代码块的还有 runBlocking,其只有当内部相同作用域的所有协程都运行结束后,声明在 runBlocking 之后的代码才能执行,即 runBlocking 会阻塞其所在线程,但其内部运行的协程又是非阻塞的,由于对线程有阻塞行为,日常开发中一般不会用到,多用于做单元测试,在此不展开说了。

下面看看官方自带的几种 CoroutineScope

1. GlobalScope

public object GlobalScope : CoroutineScope 
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext


public object EmptyCoroutineContext : CoroutineContext, Serializable 
    ...
 

从源码可以看出,GlobalScope 是一个单例,该实例所用的 CoroutineContext 是一个 EmptyCoroutineContext 实例,且 EmptyCoroutineContext 也是一个单例,GlobalScope 对象没有和 view 的生命周期组件相关联,是全局协程作用域,需要自己管理 GlobalScope 所创建的 Coroutine,所以一般而言我们不直接使用 GlobalScope 来创建 Coroutine

2. Fragment/Activity 的 lifecycleScope

// LifecycleOwner.kt
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

// Lifecycle.kt
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() 
        while (true) 
            ...
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (...) 
                newScope.register()
                return newScope
            
        
    

// Lifecycle.kt
internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver 
    ...

    fun register() 
        launch(Dispatchers.Main.immediate) 
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) 
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
             else 
                coroutineContext.cancel()
            
        
    

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) 
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) 
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        
    
 

从上面的 androidx.lifecycle.LifecycleCoroutineScopeImpl#registerandroidx.lifecycle.LifecycleCoroutineScopeImpl#onStateChanged 我们可以看出 lifecycleScope 使用的生命周期如下

// 开始
override fun onCreate(…)

// 结束
override fun onDestroy() 

3. Fragment 的 viewLifecycleScope

Fragment 其实并没有 viewLifecycleScope 的拓展属性,这里的 viewLifecycleScope 是指在 FragmentViewLifecycleScope,因为 Fragment 可以没有 View 我们可以给 Fragment 写一个拓展属性

val Fragment.viewLifecycleScope get() = viewLifecycleOwner.lifecycleScope 

这里我们可以看看 viewLifecycleOwner 是什么

// Fragment.java

    void performCreateView(...) 
        mViewLifecycleOwner = new FragmentViewLifecycleOwner(this, getViewModelStore());
        mView = onCreateView(inflater, container, savedInstanceState);
        if (mView != null) 
            // Initialize the view lifecycle
            mViewLifecycleOwner.initialize();
         else 
            if (mViewLifecycleOwner.isInitialized()) 
                throw new IllegalStateException("Called getViewLifecycleOwner() but "
                            + "onCreateView() returned null");
                
                mViewLifecycleOwner = null;
            
    

    public LifecycleOwner getViewLifecycleOwner() 
        if (mViewLifecycleOwner == null) 
            throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when "
                    + "getView() is null i.e., before onCreateView() or after onDestroyView()");
        
        return mViewLifecycleOwner;
     

performCreateView 的调用是在创建 View 的时候,可以看出,如果我们没有复写 onCreateView,那么 mView 就会为 null,从而导致 mViewLifecycleOwnernull 而复写了就会,所以我们不应该在没有 ViewFragment 中使用 viewLifecycleScope,否则在 getViewLifecycleOwner 的时候就会抛异常。所以可以看看在复写 View 时候 viewLifecycleScope 使用的生命周期为

// 开始
override fun onCreateView(…): View?

// 结束
override fun onDestroyView() 

4. ViewModel 的 viewModelScope

// ViewModel.kt
public 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)
        )
    

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope 
    override val coroutineContext: CoroutineContext = context

    override fun close() 
        coroutineContext.cancel()
    


//----------------------------------------------------

// ViewModel.java

    <T> T setTagIfAbsent(String key, T newValue) 
        ...
        synchronized (mBagOfTags) 
            previous = (T) mBagOfTags.get(key);
            if (previous == null) 
                mBagOfTags.put(key, newValue);
            
        
        ...
        return result;
    
    
    final void clear() 
        ...
        if (mBagOfTags != null) 
            synchronized (mBagOfTags) 
                for (Object value : mBagOfTags.values()) 
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                
            
        
        onCleared();
    
    
    
    private static void closeWithRuntimeException(Object obj) 
        if (obj instanceof Closeable) 
            try 
                ((Closeable) obj).close();
             catch (IOException e) 
                throw new RuntimeException(e);
            
        
     

从上面源码可以看出,viewModelScopelazy 的,调用的时候进行初始化,而 ViewModel#clear 方法是在 ViewModel 销毁的时候调用的,从而最终走到 CloseableCoroutineScope#close,使得协程被 cancel,所以可以得出,viewModelScope 的使用周期在 ViewModel 的生命周期内

Coroutine Builders

Coroutine Builders 是指 kotlinx.coroutines.Builders.kt,其内部有 CoroutineScope 的一些拓展方法等,下面介绍一下 Builders 类中两个重要的拓展方法的作用

1. launch

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
 

context:协程的上下文 start:协程的启动方式,默认值为 CoroutineStart.DEFAULT,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态,CoroutineStart.LAZY 能实现延迟启动 block:协程的执行体 返回值为 Job,指当前协程任务的句柄

我们在 view 层进行执行协程时候,一般会这样用

viewLifecycleScope.launchWhenStarted 
    ...
 

这其实就是个 launch,我们看看源码

/// Lifecycle.kt
    public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch 
        lifecycle.whenStarted(block)
     

2. async

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> 
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
 

async 返回值 Deferred 继承于 Job 接口,其主要是在 Job 的基础上扩展了 await 方法,是返回协程的执行结果,而 launch 返回的 Job 是不携带结果的

public interface Deferred<out T> : Job 
    public suspend fun await(): T  
    public val onAwait: SelectClause1<T>
    public fun getCompleted(): T
    public fun getCompletionExceptionOrNull(): Throwable?
 

CoroutineContext

协程的上下文,使用以下元素集定义协程的行为

  • Job:控制协程的生命周期
  • CoroutineDispatcher:将任务分发给适当的线程
  • CoroutineName:协程的名称,可用于辅助
  • CoroutineExceptionHandler:处理未捕获的异常

1. Job

在源码注释中,Job 有这样的描述 描述 1

State[isActive][isCompleted][isCancelled]
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (transient state)truefalsefalse
Cancelling (transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse
描述的是一个任务的状态:新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。但我们无法直接方位这些状态,可以通过方位 Job 的几个属性:isActiveisCancelledisCompleted

描述 2

 wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+ 

描述的是状态的流转,举个状态流转例子:当任务创建(New)后,协程处于活跃状态(Active),协程运行出错或者调用 job.cancel()(cancel / fail)都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true),当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true

我们再来认识一下 Job 的几个常用的方法

/// Job.kt
    /**
     * 启动 Coroutine, 当前 Coroutine 还没有执行调用该函数返回 true
     * 如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
     */
    public fun start(): Boolean
    
    /**
     * 取消当前任务,可以指定原因异常信息
     */
    public fun cancel(cause: CancellationException? = null)

    /**
     * 这个 suspend 函数会暂停当前所处的 Coroutine 直到该 Coroutine 执行完成。
     * 所以 join 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。
     * 当 Job 执行完成后,job.join 函数恢复,这个时候 job 这个任务已经处于完成状态
     * 调用 job.join 的 Coroutine 还继续处于 activie 状态
     */
    public suspend fun join()

    /**
     * 通过这个函数可以给 Job 设置一个完成通知
     */
    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle 

1.1 Deferred

Deferred 继承自 Job,是我们使用 async 创建协程的返回值,我们看看 Deferred 基于 Job 拓展的几个方法

public interface Deferred<out T> : Job 

    /**
     * 用来等待这个 Coroutine 执行完毕并返回结果
     */
    public suspend fun await(): T

    /**
     * 用来获取Coroutine执行的结果
     * 如果Coroutine还没有执行完成则会抛出 IllegalStateException
     * 如果任务被取消了也会抛出对应的异常
     * 所以在执行这个函数前可以通过 isCompleted 来判断一下当前任务是否执行完毕了
     */
    @ExperimentalCoroutinesApi
    public fun getCompleted(): T

    /**
     * 获取已完成状态的 Coroutine 异常信息
     * 如果任务正常执行完成了,则不存在异常信息,返回 null
     */
    @ExperimentalCoroutinesApi
    public fun getCompletionExceptionOrNull(): Throwable?
 

1.2 SupervisorJob

public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent) 

SupervisorJob 是一个顶层函数,里面的子 Job 不相互影响,一个子 Job 失败了,不影响其他子 Job,可以看到有个 parent 入参,如果指定了这个参数,则所返回的 Job 就是参数 parent 的子 Job

2. CoroutineDispatcher

定义任务的线程

  • Dispatchers.Default

默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器,使用一个共享的后台线程池来运行里面的任务,任务执行在子线程

  • Dispatchers.IO

和 Default 共用一个共享的线程池来执行里面的任务,区别在最大并发数不同,用途在阻塞 IO 操作

  • Dispatchers.Unconfined

未定义线程池,所以执行的时候默认在启动线程,也就是在哪个线程启动就在哪个线程执行

  • Dispatchers.Main

主线程

协程项目使用场景

1. 回调变协程

以执行多个动画为例,场景是点击某个按钮要切换到其他图标。 首先将 suspendCancellableCoroutine 封装一下,这个方法的作用是将回调变协程,但是我们需要控制其释放

class ContinuationHolder<T>(continuation: CancellableContinuation<T>) 
    var continuation: CancellableContinuation<T>?
        private set

    init 
        this.continuation = continuation
        continuation.invokeOnCancellation 
            this.continuation = null
        
    


/**
 * 避免continuation泄漏
 */
suspend inline fun <T> suspendCancellableCoroutineRefSafe(
    crossinline block: (ContinuationHolder<T>) -> Unit
): T = suspendCancellableCoroutine 
    val continuationHolder = ContinuationHolder(it)
    block(continuationHolder)
 

接下来就可以使用 suspendCancellableCoroutineRefSafe,看看怎样来将一个回调处理改装成协程

private suspend fun viewScaleAnimator(view: View, duration: Long, vararg values: Float): Boolean 
        return suspendCancellableCoroutineRefSafe  holder ->
            val animatorSet = AnimatorSet()
            animatorSet.play(ObjectAnimator.ofFloat(view, "scaleX", *values))
                .with(ObjectAnimator.ofFloat(view, "scaleY", *values))
            animatorSet.duration = duration
            animatorSet.addListener(object : Animator.AnimatorListener 
                override fun onAnimationStart(animation: Animator?) 
                

                override fun onAnimationEnd(animation: Animator?) 
                    holder.continuation?.resume(true)
                

                override fun onAnimationCancel(animation: Animator?) 
                    holder.continuation?.resume(false)
                

                override fun onAnimationRepeat(animation: Animator?) 
                
            )
            animatorSet.start()
        
     

viewScaleAnimator 方法是将一个缩放动画变成协程的处理,返回动画执行的结果 这样,我们就可以顺序的执行多个动画了

val animator1End = viewScaleAnimator(imageView, 100, 1f, 0.75f)
if (animator1End) 
    imageView.setImageResource(nextImage)
    val animator2End = viewScaleAnimator(imageView, 100, 0.75f, 1f)
    if (animator2End) 
        onAllAnimationEnd.invoke()
    
 

2. IO 异步处理

以下载了文件后需要解压为例

/**
 * 异步解压文件
 */
suspend fun unZipFolderAsync(zipFileString: String, outPathString: String) = withContext(Dispatchers.IO) 
    unZipFolder(zipFileString, outPathString)

internal fun unZipFolder(zipFileString: String, outPathString: String) 
    // FileInputStream、ZipInputStream 等的一些操作
    ...
 

5. 自定义 CoroutineScope

官方的 CoroutineScope 并不能满足所有场景,所以这时候我们可以自定义 CoroutineScope

class MyRepository 

    private var mScope: CoroutineScope? = null
    
    /**
     * 打开的时候调用
     */
    fun initScope() 
        mScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
    

    /**
     * 操作
     */
    private fun handle() 
          mScope?.launch 
              ...
          
    
    
    /**
     * 退出时候调用
     */
    fun exit() 
        mScope?.cancel()
        mScope = null
        ...
    
 

协程项目踩坑案例

1. 在 Fragment 中,lifecycleScope 和 viewLifecycleScope 分不清用哪个

viewLifecycleScope 强调的是 View 生命周期内的协程执行范围

  • 在无 UI 的逻辑 fragment 中使用 viewLifecycleScope 会抛异常
  • 在不考虑 View 回收,如横竖屏切换,需要 keep 住一些状态可以使用 lifecycleScope
  • 需要跟 Fragment 生命周期的用 lifecycleScope
  • View 创建回收时机有关系的用 viewLifecycleScope
  • 大多数情况下使用 viewLifecycleScope

2. CoroutineScope 和 Job 的 cancel 问题

CoroutineScope cancelJob 会跟着 cancelJob cancelCoroutineScope 未必需要 cancelCoroutineScope cancelJob 就不活跃了。 Jobcancel 场景其中要注意的有:例如我们 collect 一个返回值为 StateFlow 的方法,其实该方法在执行了 trymit 处理完状态后,该协程并未执行完毕,而是始终在等待中,所以我们可以在 collect 内部检测到任务执行完了,就主动将当前 Job cancel 掉,可以避免浪费内存开销。结合上面提到的回调变协程,例子如下

 private fun ...(...) 
        ...
        mAnimatorJob = mAnimatorScope?.launch 
                val animator1End = viewScaleAnimator(imageView, 100, 1f, 0.75f)
                if (animator1End) 
                    imageView.setImageResource(nextImage)
                    val animator2End = viewScaleAnimator(imageView, 100, 0.75f, 1f)
                    if (animator2End) 
                        onAllAnimationEnd.invoke()
                    
                    // 注意!这里做了 Job 的 cancel
                    mAnimatorJob?.cancel()
                    mAnimatorJob = null
                
            
    

    override fun onDestroy() 
        ...
        // fragment 销毁,未处理完任务也应该销毁
        mAnimatorJob?.cancel()
        mAnimatorJob = null
        mAnimatorScope?.cancel()
        mAnimatorScope = null
     

挂起和切线程的原理

挂起原理

前面介绍挂起的时候提到挂起操作是非阻塞式的,那么我们来看看协程是怎样做到的。 我们先看看一个小例子

class TestClass 
    suspend fun test1() 
        test2()
    

    suspend fun test2() 
    
 

我们看看这个类的字节码

public final class TestClass 
   @Nullable
   public final Object test1(@NotNull Continuation $completion) 
      Object var10000 = this.test2($completion);
      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   

   @Nullable
   public final Object test2(@NotNull Continuation $completion) 
      return Unit.INSTANCE;
   
 

可以看到,挂起函数主要用到了 Continuation

public interface Continuation<in T> 
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
 

这么看,实际挂起函数用到了类似于 callback 的逻辑了,resumeWith 相当于 callback 中一个回调函数,其作用是执行接下来要执行的代码,可以理解成在 resumeWith 回调里面继续执行下一步。 而我们在协程外是无法调用的,这里可以看出因为需要传递一个 NotNullContinuation

切线程原理

接下来讲下切线程,在项目开发中,遇到切线程的比较多的做法 withContext,下面讲述其中原理

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T   
    return suspendCoroutineUninterceptedOrReturn sc@  uCont ->
        // 创建新的context
        val oldContext = uCont.context
        val newContext = oldContext + context
        ....
        // 使用新的Dispatcher,覆盖外层
        val coroutine = DispatchedCoroutine(newContext, uCont)
        coroutine.initParentJob()
        //DispatchedCoroutine作为了complete传入
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    


private class DispatchedCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) 
	// 在complete时会会回调
    override fun afterCompletion(state: Any?) 
        afterResume(state)
    

    override fun afterResume(state: Any?) 
        // uCont就是父协程,context 仍是老版 context, 因此可以切换回原来的线程上
        uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
    
 

传入的新的 CoroutineContext 会覆盖原来所在的 CoroutineContextDispatchedCoroutine 作为 complete: Continuation 传入协程体的创建函数中,因此协程体执行完成后会回调到 afterCompletion 中,DispatchedCoroutine 中传入的 uCont 是父协程,它的拦截器仍是外层的拦截器,因此会切换回原来的线程中

后话

思考:协程设计思想

  • 我认为,协程可以使得一个复杂的操作变得可追踪结果,如果这个复杂操作既涉及到异步操作场景,更为显著,将一个完整的操作变得可追踪,业务逻辑上很清晰。

文末

要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓

以上是关于Android 协程使用指南的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 协程协程异常处理 ④ ( Android 协程中出现异常导致应用崩溃 | Android 协程中使用协程异常处理器捕获异常 | Android 全局异常处理器 )

Kotlin 协程协程异常处理 ④ ( Android 协程中出现异常导致应用崩溃 | Android 协程中使用协程异常处理器捕获异常 | Android 全局异常处理器 )

Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Android 高级UI解密 :PathMeasure截取片段 与 切线(新思路实现轨迹变换)

Android 协程使用指南