Android在非UI线程中更新UI的方法与扩展

很多时候我们开发Android程序的时候都会遇到一些耗时的操作,比如获取网络资源啊,比如截图并保存啊等等,我们不可能把这些耗时的任务放到主线程中来完成,因为那样的话很容易就会出现ANR,那么我们一般的思路应该就是开辟一个新的线程来执行耗时操作,举个例子:我们需要获取某深圳的天气预报,界面上有一个Button,点击Button开始获取,还有一个TextView控件用于显示获取的天气,获取天气是一个耗时操作,我们应该单独开一个线程,于是很容易想到这么做:

1
2
3
4
5
6
7
8
9
10
11
12
public void onClick(View v) {
//创建一个子线程执行耗时的从网络上获得天气信息的操作
new Thread() {
    @Override
    public void run() {
        //调用Google中的结构获取天气信息
        String weather = getWetherByCity("深圳");
        //把图片显示到ImageView中
        textView.setText(weather.toString());
    }
}.start();
}

但是很不幸,你会发现Android会提示程序由于异常而终止。看起来合情合理的一段代码为什么会出错呢?这是因为出于性能考虑,Android的UI操作并不是线程安全的,这意味着如果有多个线程并发操作UI组件,可能导致线程安全问题,为了解决这个问题,Android制定了一条简单的规则:只允许UI线程修改Activity里的UI组件

本例中显示天气信息的textView实际是就是一个由UI线程所创建的TextView,所以试图在一个子线程中去更改TextView的肯定会报错了,在这种机制下,为了解决类似的问题,Android设计了一个MessageQueue(消息队列),线程间可以通过该MessageQueue并结合Handler和线程的Looper组件进行信息交换,简单介绍一下:

Handler

Handler的官方解释是

1
A Handler allows you to send and process [Message](file:///F:/android%E5%AD%A6%E4%B9%A0/docs-3.0_r01-linux/docs/reference/android/os/Message.html) and Runnable objects associated with a thread’s [MessageQueue](file:///F:/android%E5%AD%A6%E4%B9%A0/docs-3.0_r01-linux/docs/reference/android/os/MessageQueue.html). Each Handler instance is associated with a single thread and that thread’s message queue.

翻译过来的意思大致就是:Handler会关联一个单独的线程和消息队列。Handler默认关联主线程,虽然要提供Runnable参数 ,但默认是直接调用Runnable中的run()方法。也就是默认下会在主线程执行,如果在这里面的操作会有阻塞,界面也会卡住。如果要在其他线程执行,可以使用HandlerThread。也就是说,假如你通过Handler发布消息的话,消息将只会发送到与它关联的这个消息队列,当然也只能处理该消息队列中的消息

Message

Handler接收和处理的消息对象。

Looper

每个线程只能拥有一个Looper。它的loop方法负责读取MessageQueue中的消息,程序首先通过Handler把消息传送给Looper,Looper把消息放入队列,同时Looper也把消息队列里的消息广播给所有的Handler,Handler接受到消息后调用handleMessage进行处理

  • 可以通过Looper类的静态方法Looper.myLooper得到当前线程的Looper实例,假如当前线程未关联一个Looper实例,该方法将返回空。
  • 可以通过静态方法Looper. getMainLooper方法得到主线程的Looper实例

    MessageQueue

    MessageQueue是一个消息队列,它采用先进先出的方式来管理Message,程序创建Looper对象的时候会在它的构造器中创建MessageQueue对象。用来存放通过Handler发送的消息。消息队列通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列。Android在第一启动程序时会默认会为UI thread创建一个关联的消息队列,用来管理程序的一些上层组件,activities,broadcast receivers 等等。你可以在自己的子线程中创建Handler与UI thread通讯

在了解Android的这些设计机制和消息队列之后,我们可以改进获取天气的方法:

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
private Handler messageHandler;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//得到当前线程的Looper实例,由于当前线程是UI线程也可以通过Looper.getMainLooper()得到
Looper looper = Looper.myLooper();
//此处甚至可以不需要设置Looper,因为 Handler默认就使用当前线程的Looper
messageHandler = new MessageHandler(looper);
}

public void onClick(View v) {
//创建一个子线程去做耗时的网络连接工作
new Thread() {
    @Override
    public void run() {
        //调用Google 天气API查询指定城市的当日天气情况
        String weather = getWetherByCity(city);
        //创建一个Message对象,并把得到的天气信息赋值给Message对象
        Message message = Message.obtain();
        message.obj = weather;
        //通过Handler发布携带有天气情况的消息
        messageHandler.sendMessage(message);
    }
}.start();
}

//重写一个Handler构造函数使用指定Looper的那个构造函数
class MessageHandler extends Handler {
public MessageHandler(Looper looper) {
    super(looper);
}
public void handleMessage(Message msg) {
    //处理收到的消息,把天气信息显示在textView上
    textView.setText((String) msg.obj);
}
}

通过消息队列改写过后的天气预告程序已经可以成功运行,因为Handler的handleMessage方法实际是由关联有该消息队列的UI thread调用,而在UI thread中更新title并没有违反Android的单线程模型的原则

遇到不会的问题了不要急于搜索,要查找出错信息,然后查看官方文档,重要的是对Android的机制有不错的理解。