在Android后台线程中更新UI的几种方法

先来解释一下为什么要在子线程中更新UI。

没有人愿意使用经常卡顿的APP,用户希望他们的App用起来流畅而不卡顿。当然每个开发者也希望这样——没有人会在构建自己的App时会说,“这个App跑的太快了,也许我应该放慢一点速度”;

尽管没有人希望这样做, 那么为什么还会有的App用起来会那么卡呢? 你之前可能已经看过我所说的这些App

可以列出一些导致某些App卡顿的原因,但是我敢打赌排在前10位的原因之一就是主线程上有太多的东西。 它有可能是被I/O处理或者复杂的计算(或者两者兼之)所拖累,这很糟糕。

难道这意味着我们的App中不应该有I/O处理或者复杂的计算吗?显然不是,但是我们应该知道要把这些放在哪里,而不是全部放在主线程中。

那么前面的主线程是什么鬼。

是这样的,当我们启动一个App的时候, Android系统会启动一个Linux Process,该Process包含一个Thread,称为UI ThreadMain Thread。通常一个应用的所有组件都运行在这一个 Process中。当然,你可以通过修改 4大组件在Manifest.xml中的代码块(<activity><service><provider><receiver>)中的 android:process 属性指定其运行在不同 的process中 。当一个组件在启动的时候,如果该process已经存在了,那么该组件就直接通过这个process被启动起来,井且运行在这个processUIThread中 。简单来说,它是负责用户界面的线程。处理的逻辑有,系统事件处理、用户输入事件处理、UI绘制、Service、 Alarm等。

Android默认约定当UI线程阻塞超过20秒将会引起ANR(Application Not Responding)异常。但是实际上,不要说20秒,即使是5秒甚至是2秒,用户都会感到不爽。因此避免在UI线程执行一些耗时操作很有必要。

这里介绍4种常用的操作多线程的方法

  1. Threads and Runnables, 来自于java
  2. AsyncTask, Android框架的一部分
  3. Handlers and Messages, 还是Android框架的一部分
  4. Anko’s doAsync, 用kotlin编写的第三方库


Threads and Runnables

通过一个例子更能理解它的使用方法。

Android Studio新建一个项目,名字随意。项目实现的效果是每2秒就更新一次界面上的数字

主界面上只用两个控件,一个TextView,一个Button


/app/res/layout/activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:text="TextView"
android:textSize="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv" app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="69dp" android:layout_marginEnd="14dp"
app:layout_constraintEnd_toEndOf="@+id/bn"/>
<Button
android:text="Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bn" app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="43dp" app:layout_constraintTop_toBottomOf="@+id/tv"
app:layout_constraintEnd_toEndOf="parent"/>
</android.support.constraint.ConstraintLayout>

如果把更新界面的逻辑放在主线程中,虽然不一定会导致ANR异常,但是会有明显的卡顿。这里就要使用Java创建子线程的方法了。

分成下面几步:

  1. 创建一个实现Runnable接口的类

  2. 把你更新UI的逻辑放在这个类的重写方法run

  3. 创建一个Thread对象,然后将刚才在步骤1中创建的Runnable对象传递给Thread的构造函数。

  4. 调用Threadstart 方法。

  5. 每当变量i的值发生更改时,我们就更新TextView


代码清单1.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.com.sshpark.mvpdemo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 绑定Button事件
bn.setOnClickListener {
val runnable = Worker()
val thread = Thread(runnable)
thread.start()
}

}
// Runnable 对象
inner class Worker: Runnable {
override fun run() {
killSomeTime()
}
}

// 更新UI的逻辑
private fun killSomeTime() {
for (i in 1..20) {
Thread.sleep(2000)
println("i: $i")
}
}
}

当然也可以写成lambda形式。代码更加简洁

代码清单1.2

1
2
3
4
5
bn.setOnClickListener {
Thread(Runnable {
killSomeTime()
}).start()
}

清单1.2使用Kotlin lambda表达式创建Runnable匿名对象。 它被传递给Thread类的构造函数。

我们不需要编写run方法。因为Runnable是一个SAM类(具有单个抽象方法的类)。 在lambda表达式中使用SAM类时,不需要显式地编写抽象方法的名称。

如果我们想做的只是打印到控制台,那么我们的代码现在应该可以正常工作了。 但是请记住,我们需要将TextField的值设置为i的当前值。

后台线程不允许更改 UI 中的任何内容。 这个责任只属于 UI 线程。 因此,我们需要解决的下一个问题是如何回到 UI 线程,以便更新TextView。 有几种方法可以做到这一点,但是最简单的方法是调用Activity类的runOnUiThread方法。

代码清单1.3展示了完整的MainActivity.kt

代码清单1.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package cn.com.sshpark.mvpdemo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 绑定Button事件
bn.setOnClickListener {
Thread(Runnable {
killSomeTime()
}).start()
}


}

// 更新UI的逻辑
private fun killSomeTime() {
for (i in 1..20) {
runOnUiThread(Runnable {
tv.text = i.toString()
})
Thread.sleep(2000)
println("i: $i")
}
}
}

运行效果:

图片1


Handlers and Messages

Thread不同,Handler类是Android框架的一部分ーー不是Java的一部分。 处理程序对象主要用于管理线程。

Android主线程包含一个消息队列 (MessageQueue),该消息队列里面可以存入一系列的MessageRunnable对象。通过一个Handler你可以往这个消息队列发送Message或者Runnable对象,并且处理这些对象。每次你新创建一个 Handle对象,它会绑定于创建它的线程(也就是 UI 线程),以及该线程的消息队列,从这时起,这个handler就会开始把MessageRunnable对象传递到消息队列中,并在它们出队列的时候执行它们。

基本思想是获得一个对主线程的Handler的引用,然后,当我们在后台线程(在这里我们不能做任何 UI 更改)中的时候,向Handler对象发送一条Message。 使用Message对象在后台线程和主线程之间传输数据。

要使用Handler对象,需要执行以下操作:

  1. 获取与UI Thread关联的Handler对象。

  2. 将可能引起ANR异常的代码放在子线程中

  3. 在子线程中需要更改 UI 中的一些东西的时候,执行以下操作:

    a. 创建一个 Message 对象,最好的方法是调用Message.obtain()

    b. 通过调用sendMessage方法向Handler对象发送消息。 消息对象可以携带数据。Message对象的data属性

    Bundle对象,因此您可以对它使用各种putXXX ()方法(例如,putString()、 putInt()、 putBundle()、putFloat()

    等)。

  4. Handler对象的handleMessage回调中进行 UI 更改。


这里继续使用前面的项目,修改MainActivity.kt中的代码。

代码清单2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package cn.com.sshpark.mvpdemo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

lateinit var mHandler: Handler

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

mHandler = object : Handler() {
override fun handleMessage(msg: Message?) {
tv.text = msg?.data?.getString("counter")
}
}

bn.setOnClickListener {
Thread(Runnable {
killSomeTime()
}).start()
}

}

private fun killSomeTime() {
for (i in 1..20) {
var msg = Message.obtain()
msg.data.putString("counter", i.toString())
mHandler.sendMessage(msg)

Thread.sleep(2000)
}
}
}

上面代码中将Handler对象声明为类的属性。在这里使用lateinit是因为我们还没有准备好实例化对象。

第17行代码实例化Handler对象。 我们将获得与 UI Thread关联的Handler对象。

第19行代码,在这里进行 UI 更改是安全的。这是与 UI 线程关联的处理程序。当我们调用sendMessage时,运行库将调用handleMessage回调。 此方法的Message参数会携带数据。

第33行代码,创建了一个Message对象。这是我们稍后将发送给处理程序的内容。Message对象的data属性就像一个bundle ーー你可以把东西放进去。我们用putString ()方法传递了一个键值对

第35行代码发送一条消息给我们的Handler

运行效果跟图1是一样的


AsyncTask

在后台运行代码的另一种方法是使用AsyncTask类。 与Handler类一样,AsyncTask也是Android框架的一部分。 与处理程序一样,它有一种在后台执行工作的机制,并且它还提供了一种(更简洁的)更新 UI 的方法。

要使用AsyncTask,通常需要执行以下操作:

  1. 继承AsyncTask
  2. 重写AsyncTaskdoInBackground方法,以便完成后台工作。
  3. 重写更多的AsyncTask的生命周期方法,这样就可以更新 UI 并报告后台任务的总体状态。
  4. 创建AsyncTask子类的一个实例并调用execute ーー这就是你如何启动后台操作的方法。

AsyncTask不如简单的线程受欢迎的原因之一是它使用泛型。Asynctask类是参数化的。必须指定三种类型才能使用它。下面的代码展示了如何实现AsyncTask类。

1
2
3
4
5
6
7
8
9
10
11
12
13
AsyncTask<void, String, Boolean> {
override fun doInBackground(vararg p0: Void?) : Boolean {
publishProgress("status of anything")
}

override fun onProgressUpdate(vararg values: String?) {
// 更新 UI
}

override fun onPostExecute(result: Boolean?) {
println(result)
}
}

必须指定三个泛型参数才能使用它。按出现的顺序,这三种参数类型如下:

  1. Params。这是我们需要传递给AsyncTask的信息,以便它可以执行后台任务。它可以是任何东西,比如 url 列表、 View 对象或 String。但是在我们的示例中,我们将AsyncTask设置为一个内部类,这样它就可以引用MainActivity中的任何View元素(这就是为什么我使用Void作为第一个类型参数的原因)
  2. Progess。后台任务完成的进度值的类型
  3. Result。 后台任务完成后返回结果的类型


doInBackground(Params...);重写该方法就是后台线程将要完成的任务。该方法可以调用publishProgress(Progess... values)方法更新任务的执行进度。

onProgressUpdate(Progess... values);doInBackground()方法中调用publishProgess()方法更新任务的执行进度后,将会触发该方法。

onPostExecute(Result result);doInBackground()完成后,系统会自动调用onPostExcute()方法,并将doInBackground()方法的返回值传给该方法。

修改MainActivity.kt中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package cn.com.sshpark.mvpdemo

import android.os.AsyncTask
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

bn.setOnClickListener {
Worker().execute()
}

}

inner class Worker: AsyncTask<Void, String, Boolean>() {
override fun doInBackground(vararg params: Void?): Boolean {
for (i in 1..20) {
publishProgress(i.toString())
Thread.sleep(2000)
}
return true
}
/* 现在,我们可以安全地将 TextView 的 text 属性设置为当前值 i。 我们只从 publishProgress 中传递了一个 参数,因此如果我们想用它,那么它就是 value 参数的第0个元素。*/
override fun onProgressUpdate(vararg values: String?) {
tv.text = values[0]
}

override fun onPostExecute(result: Boolean?) {
println(result)
}
}

}


Anko’s doAsync

AnkoJetBrainsKotlin 开发的一个Android库(也是开发Kotlin的公司)。 你可以将其用于各种各样的任务,但是在这里我们只需要doAsync部分。 顾名思义,AnkodoAsync可以让我们异步在后台运行代码。

在使用Anko之前,需要将其添加到项目的Gradle文件的依赖项中

1
2
3
4
dependencies {
....
implementation 'org.jetbrains.anko:anko-common:0.9'
}

doAsync的语法

1
2
3
doAsync {
// 后台任务
}

如何在后台运行代码以及如何返回到 UI 线程的示例代码。

1
2
3
4
5
6
7
doAsync {
{...} // 后台任务

activityUiThread {
// 更新 UI
}
}

修改MainActivity.kt中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package cn.com.sshpark.mvpdemo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.activityUiThread
import org.jetbrains.anko.doAsync

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

bn.setOnClickListener {
doAsync {
for (i in 1..20) {
Thread.sleep(2000)

activityUiThread {
tv.text = i.toString()
}
}
}
}
}
}

像之前的 ThreadHandlerAsyncTask示例一样,doAsync 也能够很好地执行。


总结一下:

当你试图在主线程上做太多的事情时,App就会变得卡顿起来

  1. 该如何避免它? 不要在主线程上做太多耗时操作,比如读取一个大文件,从网络下载数据,做计算量大的逻辑
  2. 什么是主线程? 负责在你的应用程序中创建(和修改)视图元素。也就是UI Thread
  3. 什么是后台线程? 任何不是主线程的线程。 通常为你的应用程序创建一个后台线程。
  4. 如何创建后台线程?Java ThreadsHandlersAsyncTask,以及doAsync

最后,还是要多练习才能掌握好它们

文章作者: Sshpark
文章链接: http://sshpark.com.cn/2019/02/11/在Android中更新UI的几种方法/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sshpark