补篇:协程(Coroutine)实现异步IO操作
异步IO的概念
异步IO是一种非阻塞的数据读写方法,异步IO与同步IO相对。 当一个异步过程调用发出后,调用者不能立刻得到结果。 实际的IO处理部件在完成操作后,会通过状态、通知或回调机制来通知调用者。
在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数 据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成后获取数据。这种方式使用了异步IO允许程序在等待IO操作完成的同时继续执行其他任务,从而提高了系统的整体效率。
异步IO将比特分成小组进行传送,小组可以是8位的1个字符或更长。发送方可以在任何时刻发送这些比特组,而接收方从不知道它们会在什么时候到达。
异步传输存在一个潜在的问题,即接收方并不知道数据会在什么时候到达。在它检测到数据并做出响应之前,第一个比特已经过去了。这就像有人出乎意料地从后面走上来跟你说话,而你没来得及反应过来,漏掉了最前面的几个词。因此,每次异步传输的信息都以一个起始位开头,它通知接收方数据已经到达了,这就给了接收方响应、接收和缓存数据比特的时间;在传输结束时,一个停止位表示该次传输信息的终止。按照惯例,空闲(没有传送数据)的线路实际携带着一个代表二进制1的信号,异步传输的开始位使信号变成0,其他的比特位使信号随传输的数据信息而变化。最后,停止位使信号重新变回1,该信号一直保持到下一个开始位到达。例如在键盘上数字“1”,按照8比特位的扩展ASCII编码,将发送“00110001”,同时需要在8比特位的前面加一个起始位,后面一个停止位。
Android 实现异步IO操作:
在Android开发中,异步IO通常用于执行耗时的网络请求、文件读写等操作,避免UI线程被长时间运行的IO耗时任务阻塞(避免阻塞UI线程,保持应用的响应性)。但是,在使用异步IO时,也需要注意线程安全和资源管理的问题,确保不会出现内存泄漏或竞态条件等问题。
Android提供了多种机制来实现异步IO操作:使用AsyncTask,Thread,HandlerThread,IntentService,JobIntentService,RxJava等机制实现异步IO操作.它们本质上都是对线程或线程池的封装,该类可以将耗时的操作放在后台线程池来处理,而不需要人为地另开线程来处理。
使用AsyncTask实现异步IO操作:
这是一个轻量级的异步类,适合在后台执行简单的异步任务,如网络请求或文件读写。它包含doInBackground方法在后台线程执行耗时操作,onProgressUpdate在UI线程更新进度,以及onPostExecute在任务完成后在UI线程执行后续操作。使用AsyncTask来实现异步IO的优点就是简单便捷,各个过程都有明确的回调,过程可控。
使用HandlerThread实现异步IO操作:
HandlerThread是一种在单独的线程中处理消息和Runnable对象的机制。通过使用Handler与HandlerThread结合,可以在后台线程中处理耗时的操作,并通过Handler将结果发送回UI线程进行更新。
使用IntentService实现异步IO操作:
IntentService是一种在后台执行异步任务的服务。它会在一个单独的工作线程中处理传入的Intent,并自动处理线程间的通信。IntentService适用于执行不需要立即返回结果的长时间运行的操作。
使用JobIntentService实现异步IO操作:
在Android O及更高版本中,建议使用JobIntentService作为IntentService的替代方案。它提供了更多的灵活性和更好的电池效率。
使用RxJava实现异步IO操作:
RxJava是一个在Java VM上使用可观察序列来组成异步和基于事件的程序的库。它提供了一种声明式的方式来处理异步数据流。
Handler
在Android中,Handler本身并不直接实现异步IO操作。
可以在后台子线程中执行IO操作(耗时任务), 然后通过Handler将数据结果,发送传递 给UI主线程进行更新,从而间接地支持异步IO的实现。
例子:使用Handler结合Thread实现异步IO:
1.我们首先初始化了一个Handler对象,它绑定到主线程的Looper上,这样我们就可以在主线程中执行Runnable对象。
2.然后,我们创建并启动了一个新的Thread来执行耗时的IO操作。
3.在后台线程中,我们调用performIOOperation()方法模拟了一个耗时的IO操作。
4.一旦IO操作完成,我们使用Handler的post方法将一个Runnable对象发送回主线程。这个Runnable对象包含了更新UI或处理IO操作结果的逻辑。
5.最后,在updateUIWithResult()方法中,我们执行了必须在主线程中完成的UI更新操作。
public class AsyncIOActivity extends AppCompatActivity {
private static final String TAG = "AsyncIOActivity";
private Handler mainHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_io);
// 初始化主线程的Handler
mainHandler = new Handler(Looper.getMainLooper());
// 启动后台线程执行IO操作
new Thread(new Runnable() {
@Override
public void run() {
// 模拟耗时IO操作,比如网络请求或文件读写
final String result = performIOOperation();
// 使用Handler将结果发送回主线程
mainHandler.post(new Runnable() {
@Override
public void run() {
// 在主线程中更新UI或处理结果
updateUIWithResult(result);
}
});
}
}).start();
}
private String performIOOperation() {
// 在这里执行实际的IO操作,例如网络请求或文件读写
// 这个操作是耗时的,因此应该在后台线程中执行
// 模拟耗时操作
try {
Thread.sleep(2000); // 假设耗时2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
return "IO操作完成";
}
private void updateUIWithResult(String result) {
// 在这里更新UI或处理从IO操作中获取的结果
// 这个操作必须在主线程中执行,因为Android不允许在非主线程中更新UI
Log.d(TAG, "UI updated with result: " + result);
// 例如:textView.setText(result);
}
}
在主线程也可以通过 Handler发消息给子线程,不过在子线程接收数据,需要轮训Handler,需要初始化Looper.prepare()和Looper.loop()。
public class LooperThreadActivity extends Activity {
private Handler mHandler = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyThread().start();
//发消息到目标子线程
mHandler.obtainMessage(0).sendToTarget();
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
//1.建立消息循环,初始化Looper
Looper.prepare();
mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
int what = msg.what;
if(what == 0){
//
}
}
};
//启动消息循环
Looper.loop();
}
}
}
使用协程(Coroutine)实现异步IO操作
Kotlin 协程(Coroutine)是一种轻量级的线程,它允许我们以同步的方式编写异步代码,从而极大地简化了异步编程的复杂性。协程不是真正的线程,它们是在单个线程上通过协作调度来执行的,这避免了线程切换的开销,提高了效率。下面将详细讲解 Kotlin 使用协程实现异步 IO 的原理和流程。
使用协程实现异步 IO 的原理
1.挂起函数(Suspend Function):
Kotlin 协程的核心是suspend挂起函数。这些suspend挂起函数可以在执行过程中暂停(挂起),等待某个异步任务(IO操作,网络请求)完成后再恢复执行。这使得协程能够在不阻塞主线程的情况下执行耗时的操作。
挂起函数(Suspend Functions)在 Kotlin 协程中扮演着至关重要的角色,它们是实现异步 IO 的 关键。
挂起函数 原理:
挂起函数必须在一个协程上下文中执行,通常是通过 launch、async 等协程构建器创建的。此外,挂起函数只能被其他挂起函数或协程调用,以确保正确的执行顺序和线程安全性。挂起函数通过非阻塞性和协作式调度的原理,实现了在 Kotlin 协程中执行异步 IO 操作的高效流程。它们允许代码以同步的方式编写,而实际上却是在底层执行异步操作,从而简化了异步编程的复杂性。
❶非阻塞性:挂起函数允许协程在遇到耗时的操作时(IO 操作), 而不会阻塞当前(主)线程。这使得(主)线程可以继续执行其他任务,从而提高了整体的并发性能。
❷协作式调度:协程不是由操作系统来调度的,而是由用户代码(或者库代码)来显式地调度和挂起。挂起函数是这种协作式调度的关键部分。当这个挂起函数被调用执行时,它会暂停当前协程的执行,并释放当前(主)线程的控制权。当异步操作完成或条件满足时,协程会恢复执行,恢复对当前(主)线程的控制权。
❸轻量级:由于挂起函数并不涉及线程的切换,因此它们相比于传统的线程或回调机制更加轻量级。这减少了系统资源的消耗,提高了程序的性能。
2.协程构建器(Coroutine Builder):
Kotlin 标准库提供了多个协程构建器,如 launch、async 和 runBlocking 等。这些构建器用于创建协程并启动它们的执行。
3.调度器(Dispatcher):
协程的调度器决定了协程在哪个线程或线程池上执行任务。如果你要更新UI,就让调度器把协程调度到UI主线程Dispatchers.Main(或许默认就在UI主线程);如果你要做异步IO耗时任务,就让调度器把协程调度到IO子线程Dispatchers.IO。
Kotlin 提供了默认的调度器,如 Dispatchers.Default(用于计算密集型任务)、Dispatchers.IO(用于 IO 密集型任务)和 Dispatchers.Main(用于在 Android 的主线程上执行 UI 更新)。
4.延续(Continuation):
协程的挂起和恢复是通过 Continuation 机制实现的。
当一个挂起函数被调用时,它会将当前的执行状态保存到一个 Continuation 对象中,并释放当前线程的控制权(不在当前线程里了)。
当异步操作完成时,它会恢复该 Continuation,从而使挂起函数继续执行,重新掌控当前线程的控制权(又回到当前线程里了)。
使用协程实现异步 IO 的流程
以下是一个使用 Kotlin 协程实现异步 IO 的典型流程:
1.添加依赖:
首先,你需要在项目的 build.gradle 文件中添加 Kotlin 协程的依赖。
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:" // 对于 Android 项目
2.创建一个协程:
使用协程构建器(如 GlobalScope.launch 或 viewModelScope.launch 对于 Android ViewModel)来创建协程。
//协程调度器调度到IO子线程
GlobalScope.launch(Dispatchers.IO) {
// 在这里执行异步 IO 操作
}
3.调用挂起函数:
在协程中,你可以调用挂起函数来执行异步 IO 操作。这些挂起函数通常是异步 API 的封装,它们使用协程的挂起和恢复机制来处理异步性。
使用挂起函数withContext, 指定一个不同的调度器(Dispatchers.IO )调度到IO子线程里,在代码块里执行异步IO操作。
释放对当前主线程的控制权(不在当前主线程里了)。
/**
使用挂起函数withContext, 指定一个不同的调度器(Dispatchers.IO )调度到IO子线程里,在代码块里执行异步IO操作
*/
val result = withContext(Dispatchers.IO) {
// 执行网络请求或文件读写等异步操作
// 返回操作结果
}
4.挂起协程:
当挂起函数被调用时,它会检查当前是否处于可以立即执行的状态。如果不能立即执行(比如需要等待网络响应),挂起函数就会暂停协程的执行,并释放当前(主)线程的控制权。此时,协程的状态会被保存,以便稍后恢复。
5.执行异步 IO操作
挂起函数withContext内部会开始执行异步IO操作。这通常涉及到底层库或框架的异步 API,比如使用 kotlinx.coroutines 提供的网络请求库。这些 API 在后台线程上执行实际的 IO 操作,而不会阻塞主线程。
6.恢复协程:
当异步操作完成(比如网络请求返回了结果),挂起函数withContext会接收到通知。此时,它会检查之前挂起的协程是否还存活,并尝试恢复该协程的执行。恢复过程包括重新获取对(主)线程的控制权,并从上一次挂起的位置继续执行代码。
7.更新 UI:
一旦协程恢复执行,挂起函数withContext会返回异步IO操作的结果(或异常)。你可以在协程中处理这个结果,比如更新 UI 或进行进一步的计算。
如果需要在 UI 线程上更新数据结果,你可以使用 withContext(Dispatchers.Main),指定一个不同的调度器(Dispatchers.Main )调度到Main线程里(切换到UI主线程),在代码块里执行更新数据结果 。
//返回数据结果result
val result=withContext(Dispatchers.Main) {
// 更新 UI,如设置文本、显示图像等
}
在协程中,你可以使用 try-catch 块来处理可能出现的异常。
try {
//返回数据结果result
val result = withContext(Dispatchers.IO) {
// 更新 UI,如设置文本、显示图像等
// 执行可能抛出异常的异步操作
}
} catch (e: Exception) {
// 处理异常
}
8.取消协程:
如果需要取消协程的执行,你可以使用 Job 对象来管理协程的生命周期。launch 和 async 构建器都会返回一个 Job 对象,你可以调用它的 cancel 方法来取消协程。
val job = GlobalScope.launch {
// 协程代码
}
// 在某个时刻取消协程
job.cancel()