第5章 Android的并发处理 在移动应用中常常需要多个任务同时处理。对于访问在线资源、访问数据库、解析数 据、加载音频视频等特别耗时的操作或者在移动应用中必须同时处理多任务时,可以使用 Android系统的多线程、Handler机制及异步任务来执行并发处理。本章将结合多线程、 Handlder消息处理机制、异步任务来解决多任务。由于Android11及以后版本不支持 AsyncTask来处理异步任务,而是使用Kotlin的协程处理异步任务,因此本章会介绍如何 使用Kotlin协程解决异步任务处理。 5.1 多 线 程 线程是操作系统调度的最小单位,是依附进程而存在的。Kotlin对Java的线程进行了 封装,简化的线程的处理。 创建一个线程类,有两种形式,一种是定义类,让它实现Runnable接口,形式如下: class MyThread: Runnable{ override fun run(){ println("定义实现Runnable 的类") } } 然后,利用这个线程体类对象创建线程对象并启动线程。代码的表现形式如下: Thread(MyThread()).start() 另外一种自定义线程的定义方式是,定义一个Thread类的子类,然后创建这个线程类 的对象,再调用start()方法启动线程,形式如下: Thread { override fun run() { println("使用对象表达式创建") } }.start() Kotlin对线程对象的创建和启动进行简化,可以采用下列方式来创建和启动线程: thread { println("running from thread(): ${Thread.currentThread()}") } 每一个启动的Android移动应用都有一个单独的一个进程。这个进程中有多个线程。 ·166· 这些线程中仅有一个UI主线程。Android应用程序运行时创建的UI主线程主要负责控制 UI界面的显示、更新和控件交互。Android程序创建之初,一个进程是单线程状态,所有的 任务都在主线程运行,这会导致UI主线程的负担过重。一个应用中可通过定义多个线程 来完成不同任务,分担主线程的责任,避免主线程阻塞。当主线程阻塞超过规定时间,会出 现ANR(ApplicationNotResponding,应用程序无响应)问题,导致程序中断运行。 例5-1 显示计时的应用实例1,代码如下: //模块Ch05_01 主活动定义MainActivity.kt class MainActivity : AppCompatActivity() { private var running =true //设置线程运行的控制变量 private var time =0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startBtn.setOnClickListener {//定义按钮执行启动线程 running =true thread{ //创建并启动自定义线程 while(running){ Thread.sleep(1000) time++ Log.d("MainActivity","${time}秒") } } } endBtn.setOnClickListener { //定义按钮执行停止线程,设置线程运行的条件为false running =false } } } 运行效果如图5-1所示。 图5-1 计时运行 在MainActivity中定义并启动了一个自定义线程。这个线程的主要任务是每秒更新 ·167· 一次日志并记录流逝的时间。再通过布尔值running来控制线程运行。单击“开始”按钮, running为true,观察日志可以发现,时间动态显示。单击“结束”按钮,设置running为 false,使得线程停止。观察日志,可以发现停止计时。 对上述例子进行修改,自定义线程体,让文本框timeTxt内容发生变化,代码如下: class MainActivity : AppCompatActivity() { private var running =true private var time =0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startBtn.setOnClickListener { running =true thread{ while(running){ Thread.sleep(1000) time++ timeTxt.text ="${time}秒" Log.d("MainActivity","${time}秒") } } } endBtn.setOnClickListener { running =false } } } 执行上述代码,会出现android.view.ViewRootImpl$CalledFromWrongThreadException异 常,导致程序中断闪退移动应用。这是因为自定义线程无法修改主线程定义的UI控件。只有 UI主线程才能修改和变更UI控件。下一节Handler机制中来解决这个问题。 5.2 Handler机制 用于消息(Message)处理的Handler机制是一种异步处理方式。通过Handler机制可 以实现不同线程之间的通信。Handler机制相关的类有Looper、MessageQueue、Message 和Handler类。它们各司其职,彼此之间的关系如图5-2所示。 其中,Looper在线程内部定义,每个线程只能定义一个Looper。Looper内部封装了 MessageQueue(消息队列)。Looper对象消息队列进行循环管理。通常所说的Looper线 程就是循环工作的线程。一个线程不断循环,一旦有新任务则执行,执行完毕,继续等待下 一个任务,以这种方式执行任务的线程就是Looper线程。 Message也称为任务。有时将Runnable线程体对象封装成Message对象来处理其中 的任务。Handler对象发送、接收并进行Message的处理。Message封装了任务携带的信 息和处理该任务的handler。可以直接调用Message构造函数来创建Message对象,但是 这种方法并不常用。最常见获取Message对象的方式如下。 ·168· 图5-2 Handler消息处理机制 (1)通过Message.obtain()函数从消息池中获得空消息对象,以节省资源。 (2)通过handler.obtainMessage()函数获得Message对象实例,即从全局的消息池中 获得消息对象。 Message对象具有属性Message.arg1和Message.arg2,它们用来传递基本简单数据信 息,例如数值等。这比用Bundle更省内存。如果需要传递更复杂的对象,可以通过消息 Message对象的obj属性来实现。Message对象的what属性来标识信息,以便用不同方式 处理消息对象。 当产生一个消息时,关联Looper的Handler对象会将Message对象发送给MessageQueue。 Looper对MessageQueue进行管理和控制,控制Message进出MessageQueue。一旦Message从 MessageQueue出列,可以使用Handler对这个Message对象进行处理。 Handler既是处理器,也是调度器,用于调度和处理线程。它最主要的工作是发送和接 收并处理Message。Handler往往与Looper关联。Handler可以调度Message,将一个任 务切换到某个指定的线程中去执行。在Handler中常见的任务是用于更新UI。在其他线 程也称为工作线程(WorkThread),修改了数据,而这些数据会影响到UI的界面。因为这 些工作线程自己并不能变更UI,所以常见的处理方式是将数据封装成Message,并通过 Handler对象发送到MessageQueue中。最后在UI主线程中接收这些数据,对UI界面进 行变更,如图5-3所示。 例5-2 显示计时的应用实例2,代码如下: //模块Ch05_02 MainActivity.kt class MainActivity : AppCompatActivity() { var running =true val handler =object: Handler(Looper.get MainLooper()){ override fun handleMessage(msg: Message) { if(msg.what ==0x123){ //判断消息的来源 timeTxt.text ="${msg.arg1}秒" ·169· 图5-3 Handler机制 } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startBtn.setOnClickListener { //定义按钮处理启动线程 running =true var time =0 thread{ //创建并启动工作线程 while(running){ Thread.sleep(1000) //当前线程休眠1s time++ var message =Message.obtain() message.arg1 =time //设置参数 message.what =0x123 //设置消息标识 handler.sendMessage(message) //发送消息 } } } endBtn.setOnClickListener { //让线程停止循环自动终结 running =false } } } ·170· 运行结果如图5-4所示。 图5-4 计时器运行结果 在上述的MainActivity中并没有创建Looper对象,而是通过Looper.getMainLooper()函 数调用UI主线程内置的Looper对象。在这个计数器的活动中,可直接利用UI主线程的 Looper来控制MessageQueue。上述代码中,object表示生成一个匿名对象。Handler对象 在工作线程(自定义的线程)中,发送消息。在主线程中,handler对象对出列的Message的 what来源标识进行判断,确定其是否是从指定工作线程发出的,最后对这个消息进行处理。 在本例中,实现了修改TextView 控件的文本信息,从而实现了动态计时功能。 5.3 异步任务 在Android10及之前的版本中,当处理耗时的任务时,会通过异步任务AsyncTask来 实现。异步任务的执行步骤有3个步骤。 (1)在UI线程接收事件。 (2)在非UI线程中处理相应事件。 (3)UI根据处理结果进行刷新。 通过这3个步骤,将一些耗时的操作在工作线程中完成,并实现了用户界面更新的处 理。下面是一个异步任务的基本结构,代码如下: class SomeTask(): AsyncTask<Unit, Int, Boolean>() { /**任务执行前调用*/ override fun onPreExecute() { … } /**这个方法的所有方法会在子线程中被调用,用于处理耗时的任务*/ override fun doInBackground(vararg params: Unit?): Boolean { … ·171· return true } /**修改执行的进度*/ override fun onProgressUpdate(vararg values: Int?) { … } /**完成所有的处理*/ override fun onPostExecute(result: Boolean?) { … } } 上面代码定义SomeTask是AsyncTask的子类,要求指定3个类型参数<Unit,Int, Boolean>,这3个类型参数也可以是别的类型。不管怎样,这3个类型参数按照出现的顺 序,它们分别表示输入的数据类型、进度的数据类型、返回结果的数据类型。结合表5-1中 AsyncTask常见的函数,可以更好地处理异步任务。 表5-1 AsyncTask常见函数 函 数说 明 onPreExecute() 在执行实际的后台操作前被Thread调用。可以在该方法中做一些 准备工作,如在界面上显示一个进度条 onProgressUpdate(Progress…) 在publishProgress()函数后调用,用来在UI上更新进度 onPostExecute(Result) 在doInBackground()函数执行完成后,被Thread调用,后台的计算 结果将通过该方法传递到Thread doInBackground(Params…) 在onPreExecute()函数执行后马上执行,该函数运行在后台线程中。 这里将主要负责执行那些很耗时的后台计算工作。可以调用 publishProgress()函数来更新实时的任务进度。它是抽象方法,子类 必须实现 onCancelled(Result) 在激活cancel(boolean)函数以及doInBackground(Object[])函数完 成之后,在Thread中运行 onCancelled() 默认由它的实现进行激活 AsyncTask执行流程如图5-5所示。首先由主线程调用异步任务的onPreExecute()函 数执行一些准备工作。然后由后台的工作线程调用doInBackground()函数执行主要耗时 的业务。在doInBackground()函数中会调用publishProgress()函数对发布并更新当选业 务的执行进度。当doInBackground()函数后台任务执行完毕。UI主线程调用onCancelled() 函数处理任务取消业务。最后,UI主线程执行onPostExecute()函数来处理计算结果,将后 台执行的计算结果传递给UI主线程。至此,异步任务执行完毕。 例5-3 使用AsyncTask动态显示图片。 (1)定义在MainActivity。首先在MainActivity对应的布局文件中定义了FrameLayout, 用于包含Fragment,代码如下: ·172· 图5-5 AsyncTask异步任务执行流程 <!--模块Ch05_03 定义主活动的布局activity_main.xml --> <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns: android="http://schemas.android.com/apk/res/android" xmlns: app="http://schemas.android.com/apk/res-auto" xmlns: tools="http://schemas.android.com/tools" android: layout_width="match_parent" android: layout_height="match_parent" tools: context=".MainActivity" android: id="@+id/mainFrag" /> MainActivity用于加载Fragment,代码如下: //模块Ch05_03 定义主活动MainActivity.kt class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) replaceFragment(MainFragment()) } fun replaceFragment(fragment: Fragment){ supportFragmentManager.beginTransaction().apply{ replace(R.id.mainFrag,fragment) addToBackStack(null) commit() } } } (2)定义MainFragment和定义异步任务。MainFragment是嵌入在MainActivity中的 Fragment,它对应的布局包括了要显示的ImageView和显示进度相关的View(视图)组件,代 码如下: ·173· <!--模块Ch05_03 定义MainFragment 的布局fragment_main.xml --> <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns: android="http://schemas.android.com/apk/res/android" xmlns: tools="http://schemas.android.com/tools" android: layout_width="match_parent" android: layout_height="match_parent" xmlns: app="http://schemas.android.com/apk/res-auto" android: background="@android: color/black" tools: context=".MainFragment"> <!--定义图像视图设置默认的图片--> <ImageView android: id="@+id/imageView" android: layout_width="match_parent" android: layout_height="match_parent" app: layout_constraintEnd_toEndOf="parent" app: layout_constraintHorizontal_bias="0.0" app: layout_constraintStart_toStartOf="parent" app: layout_constraintTop_toTopOf="parent" app: srcCompat="@mipmap/scene1" /> <ProgressBar android: id="@+id/progressBar" style="?android: attr/progressBarStyle" android: layout_width="wrap_content" android: layout_height="wrap_content" android: progress="0" app: layout_constraintBottom_toBottomOf="@+id/imageView" app: layout_constraintEnd_toEndOf="parent" app: layout_constraintHorizontal_bias="0.553" app: layout_constraintStart_toStartOf="parent" app: layout_constraintTop_toTopOf="@+id/imageView" app: layout_constraintVertical_bias="1.0" /> <!--记录进度的文本标签--> <TextView android: id="@+id/progressTxt" android: layout_width="wrap_content" android: layout_height="wrap_content" android: text="TextView" android: textSize="32sp" android: textColor="@android: color/white" app: layout_constraintBottom_toBottomOf="parent" app: layout_constraintEnd_toEndOf="parent" app: layout_constraintHorizontal_bias="0.539" app: layout_constraintStart_toStartOf="parent" app: layout_constraintTop_toTopOf="parent" app: layout_constraintVertical_bias="0.022" /> <!--定义单选按钮组,包括的单选按钮表现为圆形--> <RadioGroup android: id="@+id/group" android: layout_width="match_parent" android: layout_height="wrap_content" android: gravity="center_horizontal" android: orientation="horizontal" app: layout_constraintBottom_toBottomOf="parent" ·174· app: layout_constraintEnd_toEndOf="parent" app: layout_constraintStart_toStartOf="parent" app: layout_constraintTop_toTopOf="parent" app: layout_constraintVertical_bias="0.931"> <RadioButton android: id="@+id/one" android: layout_width="wrap_content" android: layout_height="wrap_content" android: button="@drawable/radiobtn_style" android: checked="true" /> <RadioButton android: id="@+id/two" android: layout_width="wrap_content" android: layout_height="wrap_content" android: button="@drawable/radiobtn_style" /> <RadioButton android: id="@+id/three" android: layout_width="wrap_content" android: layout_height="wrap_content" android: button="@drawable/radiobtn_style" /> <RadioButton android: id="@+id/four" android: layout_width="wrap_content" android: layout_height="wrap_content" android: button="@drawable/radiobtn_style" /> <RadioButton android: id="@+id/five" android: layout_width="wrap_content" android: layout_height="wrap_content" android: button="@drawable/radiobtn_style" /> </RadioGroup> </androidx.constraintlayout.widget.ConstraintLayout> 下列的strings.xml定义了相关的字符资源和图片资源的整型数组,代码如下: <!--模块Ch05_03 定义用户界面使用的资源res/values/strings.xml --> <resources> <string name="app_name">AsyncTask01</string> <integer-array name="images"> <item>@mipmap/scene1</item> <item>@mipmap/scene2</item> <item>@mipmap/scene3</item> <item>@mipmap/scene4</item> <item>@mipmap/scene5</item> </integer-array> <string name="title_hello">主页</string> <string name="hello_blank_fragment">欢迎来到Android 世界!</string> </resources> 在MainFragment中加载图片资源,并结合异步任务依次显示,代码如下: ·175·