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#register
和 androidx.lifecycle.LifecycleCoroutineScopeImpl#onStateChanged
我们可以看出 lifecycleScope
使用的生命周期如下
// 开始
override fun onCreate(…)
// 结束
override fun onDestroy()
3. Fragment 的 viewLifecycleScope
Fragment
其实并没有 viewLifecycleScope
的拓展属性,这里的 viewLifecycleScope
是指在 Fragment
对 View
的 LifecycleScope
,因为 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
,从而导致 mViewLifecycleOwner
为 null
而复写了就会,所以我们不应该在没有 View
的 Fragment
中使用 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);
从上面源码可以看出,viewModelScope
是 lazy
的,调用的时候进行初始化,而 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) | 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 |
描述的是一个任务的状态:新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。但我们无法直接方位这些状态,可以通过方位 Job 的几个属性:isActive 、isCancelled 和 isCompleted |
描述 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 cancel
了 Job
会跟着 cancel
,Job cancel
了 CoroutineScope
未必需要 cancel
,CoroutineScope cancel
后 Job
就不活跃了。 Job
的 cancel
场景其中要注意的有:例如我们 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
回调里面继续执行下一步。 而我们在协程外是无法调用的,这里可以看出因为需要传递一个 NotNull
的 Continuation
。
切线程原理
接下来讲下切线程,在项目开发中,遇到切线程的比较多的做法 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截取片段 与 切线(新思路实现轨迹变换)