在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
最后,还是要多练习才能掌握好它们