探索Android协程:简化异步编程的强大工具

在Android开发中,异步编程一直是一个极其重要的话题。为了避免主线程被阻塞导致应用无响应,我们需要将耗时操作迁移到其他线程中执行。传统上,我们使用线程和Handler来管理异步任务,但这种方式存在一些缺陷,代码可读性较差、嵌套毁掉过多、资源管理较为困难等。随着Kotlin协程的引入,情况有了极大改善。

协程为什么好?

  1. 轻量级:与线程相比,协程的开销极小,可以轻松创建上万个协程而不会过多消耗系统资源。
  2. 可组合:协程之间可以相互组合,从而构建出复杂的异步流程,大大提高了代码的可读性。
  3. 结构化并发(Structural Concurrency):协程支持了"结构化并发"的概念,父子协程之间存在着明确的层级关系,子协程的声明周期由父协程决定。这种机制简化了异常处理和资源管理。
  4. 暂停和恢复:与传统协程不同,协程可以在不阻塞线程的情况下被暂停和恢复执行,这种特性被称为"非阻塞挂起"。
  5. 基于线程池:协程默认基于线程池运行,从而避免了线程创建和销毁的开销。

基本用法

创建协程最常见的方式就是使用launchasync函数,例如:

kotlin 复制代码
// 在后台线程启动一个协程
GlobalScope.launch(Dispatchers.IO) {
    // 执行耗时操作
    val resylt = doNetworkCall()
    // 切换到主线程更新UI
    withContext(Dispatchers.Main) {
        updateUI(result)
    }
}

这个例子展示了几个重要概念:

  • GlobalScope - 协程的作用域,决定了协程的生命周期。
  • Dispatchers.IO - 用于指定协程在哪个线程上下文中运行。
  • withContext - 用于切换协程上下文。

处理异常和资源

协程支持结构化并发,意味着子协程的生命周期由父协程决定。当父协程被取消,所有子协程也被自动取消。因为,我们可以利用这种机制轻松地处理异常和资源管理。

kotlin 复制代码
val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught $exception")
}

lifecycleScope.launch(handle) {
    val data = async(Dispatcher.IO) {
        // 可能会抛出异常的代码
    }.await()
    // 使用data
}

在上面的例子中,我们捕获子协程中抛出的异常,并可以做一些清理工作。同时,当外层协程被取消时,async创建的子协程也会被自动取消并释放相关资源。

通过这种结构,我们可以把异步任务的声明周期管理集中在一个逻辑单元中,从而大幅降低代码复杂度。

小结

Android协程无疑是一个强有力的工具,它简化了异步编程模型,增强了代码的可读性和和维护性。通过结构化并发、可组合性和轻量级等特性,协程有望成为Android平台上异步编程的标准解决方案。当然,学习新技术都需要一个过程,希望通过本文的介绍,可以帮助读者对协程有一个初步的认识。

No No No

以为就这么爽快的结束了嘛?是不是感觉看完了之后就理解协程是啥了?

不相信,那看看我下面的问题你是否能回答出来。。。

1、传统的Java SDK里面的Thread和Executor那套框架,加上Android的Handler机制,是怎么实现并发任务管理的?怎么开子线程?怎么结束子线程?如何处理子线程的异常?

感觉好像知道,又好像说不明白,代码没少写,你问我这个我一时间还真答不出来,等等我Google一下...

1.1 Thread类

  • 创建新线程 :通过扩展Thread类或实现Runnable接口,然后调用start()方法启动新线程。
  • 结束线程run()方法执行完毕或调用interrupt()方法中断线程的执行。
  • 异常处理 :通过try-catch或实现UncaughtExceptionHandler接口捕获线程中抛出的未捕获异常。

代码示例:

java 复制代码
// 创建新线程
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        //执行任务
    }
});

thread.start(); // 启动线程

// 结束线程
thread.interrupt(); // 中断线程

// 异常处理
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHanlder() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 处理异常
    }
});

1.2、Executor框架

  • 线程池 :通过调用Executors工厂类的静态方法创建不同类型的线程池,避免频繁创建和销毁线程的开销。
  • 任务提交 :使用ExecutorService接口的submit()方法 提交RunnableCallable任务。
  • 任务终止ExecutorService提供了shotdown()shotdownNow()方法安全地终止执行器。
  • 异常处理 :通过Future接口的get()方法捕获任务中抛出的异常。

代码示例:

java 复制代码
// 创建线程池
ExecutorService executor = Execotors.newFixedThreadPool(4); // 固定大小的线程池

// 提交任务
Future<String> future = executor.submit(new Callcable<String>() {
    @Override
    public String call() throws Exception {
        // 执行任务
        return "Result";
    }
});

// 捕获结果并处理异常
try {
    String result = future.get();
} catch (Exception e) {
    // 处理异常
}

// 终止线程池
executor.showdown(); // 等待所有任务执行完毕

1.3、Android Handler机制

  • 创建Handler :通过构造函数指定管理的Looper(主线程LooperHandlerThread创建的工作线程Looper
  • 发送消息 :使用HandlerpostsendMessagesendEmptyMessage方法将RunnableMessage加入MesssageQueue
  • 处理消息LooperMessageQueue取出Message,执行关联HandlerhandleMessage方法。
  • 结束线程quit()quitSafely()方法结束线程的消息循环。
  • 异常处理 :在handleMessage中捕获异常或给线程设置默认的UncaughtExceptionHandler

代码示例:

java 复制代码
// 创建Handler
Handler handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // 处理消息
    }
};

// 发送消息
new Thread(new Runnable() {
    @Override
    public void run() {
        // 执行任务
        Message message = handler.obtainMessage();
        handler.sendMessage(message);
    }
}).start();

// 处理异常
Thread.setDefaultUncaghtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 处理异常
    }
});

这些框架的主要区别在于

  • Thread是较低级别的API,需要手动处理线程创建、同步等细节。
  • Executors是更高层的并发框架,更易用且提供线程池复用机制。
  • Android的Handler主要用于Android主线程与子线程之间通信。

2、那么RxJava是怎么管理并发的?上面的操作RxJava都是怎么做的?Rxjava带来哪些新能力?

2.1、创建线程

RxJava利用Scheduler来指定线程,比如:

java 复制代码
Observable.just(1, 2, 3)
    .subscrubeOn(Schedulers.io()) // 指定订阅操作在IO线程
    .observeOn(AndroidSchedulers.mainThread()) // 指定观察者回调在主线程
    .subscrube(/* ... */);

2.2、结束线程

RxJava利用响应式拉取的方式,只在有观察者订阅时才创建新线程,观察者取消订阅时会自动结束相关线程。

2.3、异常处理

RxJava提供了onError回调以及一系列错误处理操作符(retrycatch等)来处理异常。

java 复制代码
observable.subscribe(
    data -> {}, // onNext
    error -> {}, // onError
    () -> {} // onCompleted
);

2.4、新能力

  1. 声明式编程 :RxJava使用像mapflatMap这样的操作符组合异步操作,代码更简洁易读。
  2. 线程切换:RxJava提供线程切换的能力,例如在IO线程执行耗时操作,在主线程更新UI。
  3. 组合操作:RxJava丰富的操作符使得可以轻松组合多个异步操作,如并发执行、合并结果等。
  4. 背压处理 :通过操作符如filtersample等,可以有效控制上游事件产生的速度,避免下游处理过载。
  5. 取消订阅 :RxJava支持通过dispose取消订阅,自动清理资源,避免内存泄露。

总的来说,RxJava 让异步编程变得更简单、更可组合,同时具备响应式拉取、背压和资源自动管理等特性,使得一些本来难以实现的异步组合逻辑变得可行。当然,学习曲线较陡也是 RxJava 被诟病的一点。

3、什么是协程的结构化并发(Structural Concurrency)

协程由于改变了视角,可以线性地管理并发任务,所以可以把复杂的工作全都包进一个整块代码里面,这就让它有了「子协程」的概念。这跟Java的子线程不是一个概念,Java的子线程指的是「非主线程的线程」,而协程的子协程,指的是「工作于这个协程内部的一个子模块」。

父子协程的概念的引入,就让父子协程的关系管理(注意是取消和异常的相互影响,例如父协程取消触发子协程的取消、子协程的异常触发父协程的取消)有了可坑,这个对于Java的线程来说,是几乎不可能的。

协程的结构化并发主要由一下几个部分组成:

  1. CoroutineScope

CoroutineScope用于定义协程的作用域,它管理协程的生命周期。常用的CoroutineScope包括:

  • CoroutineScope接口:定义协程作用域的基本协议。
  • GlobalScope:进程生命周期作用域。
  • LifecycleScope:与Android生命周期相关的作用域。
  • ViewModelScope:与ViewModel生命周期相关的作用域。
  1. CoroutineContext

CoroutineContext用于配置协程的行为,如指定运行的线程(Dispatchers)、命名协程、设置错误处理器等。

  1. Job

Job表示一个具体的协程任务,通过CoroutineScope.launchCoroutineScope.async启动一个协程任务,会返回一个Job实例。可以通过Job来取消协程或检查协程的状态。

  1. Dispatchers

Dispatcher决定了协程运行的线程环境,包括:

  • Dispatchers.Main:Android主线程。
  • Dispatchers.IO:适用于I/O阻塞操作的线程池。
  • Dispatchers.Default:适用于CPU密集型工作的线程池。
  1. Structured Concurrency

Structured Concurrency是协程最大的优势之一,它提供了launchasync等结构化并发作用域构造函数,让我们可以用极其简洁的方式构建复杂的异步逻辑。

示例代码:

kotlin 复制代码
fun loadDataAsync() = viewModelScope.launch {
    try {
        val data1 = async(Dispatchers.IO) { fetchData1() }
        val data2 = asnyc(Dispatchers.IO) { fetchData2() }
        
        val result1 = data1.await()
        val result2 = data2.await()
        
        render(result1, result2)
    } catch (e: Exception) {
        handleError(e)
    }
}

上面的示例中,通过viewModelScope.launch创建一个ViewModel作用域的协程。在协程内部,我们并发执行两个网络请求任务,通过async启动两个子协程,并通过await等待它们的记过。如果任何一个任务抛出异常,catch块会捕获并处理它。

4、delay()Thread.sleep()的区别

Thread.sleep()会阻塞当前执行的线程,导致线程暂停,并释放CPU资源。sleep期间线程无法做任何其他事情。

delay()是一个协程内的非阻塞式的延迟函数。它不会阻塞线程,而是让出线程继续执行其他协程,直到延迟时间到期再回复执行被延迟的协程。

简单来说,Thread.sleep()是阻塞式的,会导致当前线程无法做其他事情,而delay()是非阻塞式的,可以让出线程继续执行其他任务。

5、suspend和阻塞式的区别

阻塞式是线程级别的概念,阻塞会导致当前线程暂停执行。

suspend是协程级别的概念,它只暂停当前协程,而不会影响整个线程。被挂起的协程让出线程给其他协程执行。

示例对比:

kotlin 复制代码
// 阻塞式
fun blocker() {
    Thread.sleep(1000) // 当前线程暂停1秒
    println("Blocked for 1 sec") // 暂停期间无法执行其他代码
}

// 非阻塞式
suspend fun sleeper() = coroutineScope {
    delay(1000) // 无需阻塞线程,让出线程执行其他协程
    println("Resumed after 1 sec delay) // 在1秒后输出
}

在阻塞式的blocker()函数中,当前线程将暂停1秒,期间无法执行其他代码。而是sleeper()协程函数中,delay(1000)并不会阻塞线程,线程可以继续执行其他协程,等待1秒后恢复执行打印语句。

总的来说,协程的suspend是一种轻量级的暂停方式,不会造成线程阻塞。这种非阻塞式设计避免了阻塞操作导致的线程暂停和上下文切换开销,从而更高效地利用CPU资源。

线程和协程初步就讲解到这儿,希望本文对广大猿们有所帮助

距离居中无敌程序员又近了一步...

相关推荐
ChinaDragonDreamer2 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院4 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下4 小时前
android navigation 用法详细使用
android
小比卡丘7 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭8 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss9 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.10 小时前
数据库语句优化
android·数据库·adb
GEEKVIP12 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model200514 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏68914 小时前
Android广播
android·java·开发语言