在Android后台线程中更新UI的几种方法
先来解释一下为什么要在子线程中更新UI。
没有人愿意使用经常卡顿的APP,用户希望他们的App用起来流畅而不卡顿。当然每个开发者也希望这样——没有人会在构建自己的App时会说,“这个App跑的太快了,也许我应该放慢一点速度”;

尽管没有人希望这样做, 那么为什么还会有的App用起来会那么卡呢? 你之前可能已经看过我所说的这些App。
可以列出一些导致某些App卡顿的原因,但是我敢打赌排在前10位的原因之一就是主线程上有太多的东西。 它有可能是被I/O处理或者复杂的计算(或者两者兼之)所拖累,这很糟糕。
难道这意味着我们的App中不应该有I/O处理或者复杂的计算吗?显然不是,但是我们应该知道要把这些放在哪里,而不是全部放在主线程中。 那么前面的主线程是什么鬼。

是这样的,当我们启动一个App的时候, Android系统会启动一个Linux Process,该Process包含一个Thread,称为UI Thread 或 Main Thread。通常一个应用的所有组件都运行在这一个 Process中。当然,你可以通过修改 4大组件在Manifest.xml中的代码块(<activity><service><provider><receiver>)中的 android:process 属性指定其运行在不同 的process中 。当一个组件在启动的时候,如果该process已经存在了,那么该组件就直接通过这个process被启动起来,井且运行在这个process的UIThread中 。简单来说,它是负责用户界面的线程。处理的逻辑有,系统事件处理、用户输入事件处理、UI绘制、Service、 Alarm等。
Android默认约定当UI线程阻塞超过20秒将会引起ANR(Application Not Responding)异常。但是实际上,不要说20秒,即使是5秒甚至是2秒,用户都会感到不爽。因此避免在UI线程执行一些耗时操作很有必要。
这里介绍4种常用的操作多线程的方法
- Threads and Runnables, 来自于java
- AsyncTask,
Android框架的一部分 - Handlers and Messages, 还是
Android框架的一部分 - Anko’s doAsync, 用kotlin编写的第三方库
Threads and Runnables
通过一个例子更能理解它的使用方法。
用Android Studio新建一个项目,名字随意。项目实现的效果是每2秒就更新一次界面上的数字
主界面上只用两个控件,一个TextView,一个Button
/app/res/layout/activity_main.xml
1 |
|
如果把更新界面的逻辑放在主线程中,虽然不一定会导致ANR异常,但是会有明显的卡顿。这里就要使用Java创建子线程的方法了。
分成下面几步:
创建一个实现
Runnable接口的类把你更新UI的逻辑放在这个类的重写方法
run中创建一个
Thread对象,然后将刚才在步骤1中创建的Runnable对象传递给Thread的构造函数。调用
Thread的 start 方法。每当变量
i的值发生更改时,我们就更新TextView。
代码清单1.1
1 | package cn.com.sshpark.mvpdemo |
当然也可以写成lambda形式。代码更加简洁
代码清单1.2
1 | bn.setOnClickListener { |
清单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 | package cn.com.sshpark.mvpdemo |
运行效果:

Handlers and Messages
与Thread不同,Handler类是Android框架的一部分ーー不是Java的一部分。 处理程序对象主要用于管理线程。
Android主线程包含一个消息队列 (MessageQueue),该消息队列里面可以存入一系列的Message或Runnable对象。通过一个Handler你可以往这个消息队列发送Message或者Runnable对象,并且处理这些对象。每次你新创建一个 Handle对象,它会绑定于创建它的线程(也就是 UI 线程),以及该线程的消息队列,从这时起,这个handler就会开始把Message或Runnable对象传递到消息队列中,并在它们出队列的时候执行它们。
基本思想是获得一个对主线程的Handler的引用,然后,当我们在后台线程(在这里我们不能做任何 UI 更改)中的时候,向Handler对象发送一条Message。 使用Message对象在后台线程和主线程之间传输数据。
要使用Handler对象,需要执行以下操作: 1. 获取与UI Thread关联的Handler对象。
将可能引起
ANR异常的代码放在子线程中在子线程中需要更改 UI 中的一些东西的时候,执行以下操作:
创建一个 Message 对象,最好的方法是调用
Message.obtain()通过调用
sendMessage方法向Handler对象发送消息。 消息对象可以携带数据。Message对象的data属性
是
Bundle对象,因此您可以对它使用各种putXXX ()方法(例如,putString()、 putInt()、 putBundle()、putFloat()等)。
在
Handler对象的handleMessage回调中进行 UI 更改。
这里继续使用前面的项目,修改MainActivity.kt中的代码。
代码清单2.1
1 | package cn.com.sshpark.mvpdemo |
上面代码中将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,通常需要执行以下操作:
- 继承
AsyncTask类 - 重写
AsyncTask的doInBackground方法,以便完成后台工作。 - 重写更多的
AsyncTask的生命周期方法,这样就可以更新 UI 并报告后台任务的总体状态。 - 创建
AsyncTask子类的一个实例并调用executeーー这就是你如何启动后台操作的方法。
AsyncTask不如简单的线程受欢迎的原因之一是它使用泛型。Asynctask类是参数化的。必须指定三种类型才能使用它。下面的代码展示了如何实现AsyncTask类。
1 | AsyncTask<void, String, Boolean> { |
必须指定三个泛型参数才能使用它。按出现的顺序,这三种参数类型如下:
- Params。这是我们需要传递给
AsyncTask的信息,以便它可以执行后台任务。它可以是任何东西,比如 url 列表、 View 对象或 String。但是在我们的示例中,我们将AsyncTask设置为一个内部类,这样它就可以引用MainActivity中的任何View元素(这就是为什么我使用Void作为第一个类型参数的原因) - Progess。后台任务完成的进度值的类型
- Result。 后台任务完成后返回结果的类型
doInBackground(Params...);重写该方法就是后台线程将要完成的任务。该方法可以调用publishProgress(Progess... values)方法更新任务的执行进度。
onProgressUpdate(Progess... values);在doInBackground()方法中调用publishProgess()方法更新任务的执行进度后,将会触发该方法。
onPostExecute(Result result);当doInBackground()完成后,系统会自动调用onPostExcute()方法,并将doInBackground()方法的返回值传给该方法。
修改MainActivity.kt中的代码。
1 | package cn.com.sshpark.mvpdemo |
Anko’s doAsync
Anko是JetBrains在Kotlin 开发的一个Android库(也是开发Kotlin的公司)。 你可以将其用于各种各样的任务,但是在这里我们只需要doAsync部分。 顾名思义,Anko的doAsync可以让我们异步在后台运行代码。
在使用Anko之前,需要将其添加到项目的Gradle文件的依赖项中
1 | dependencies { |
doAsync的语法
1 | doAsync { |
如何在后台运行代码以及如何返回到 UI 线程的示例代码。
1 | doAsync { |
修改MainActivity.kt中的代码。
1 | package cn.com.sshpark.mvpdemo |
像之前的 Thread、 Handler 和AsyncTask示例一样,doAsync 也能够很好地执行。
总结一下:
当你试图在主线程上做太多的事情时,App就会变得卡顿起来
- 该如何避免它? 不要在主线程上做太多耗时操作,比如读取一个大文件,从网络下载数据,做计算量大的逻辑
- 什么是主线程? 负责在你的应用程序中创建(和修改)视图元素。也就是
UI Thread - 什么是后台线程? 任何不是主线程的线程。 通常为你的应用程序创建一个后台线程。
- 如何创建后台线程?
Java Threads,Handlers,AsyncTask,以及doAsync
最后,还是要多练习才能掌握好它们
