让我们通过一个交通状况查询Activity来讨论下Android 的UI 界面更新问题:
当用户输入区域名称,然后单击按钮进行查询后,程序会调用相应接口获得指定区域的交通状况摘要。当网络出现异常或者服务繁忙的时候都会使访问网络的动作很耗时,这时,Android会提示一个程序无法响应的异常,该对话框会询问用户是继续等待还是强行退出程序,这样就大大的降低用户体验。所以我们需要参试以别的方式来实现:

2.1 创建子线程更新UI
显然如果你的程序需要执行耗时的操作的话,如果像上例一样由主线程来负责执行该操作是错误的。所以我们需要在onClick方法中创建一个新的子线程来负责调用相应借口来获得交通信息数据:
public void onClick(View v) {
//创建一个子线程执行从网络上获取交通信息的操作
new Thread() {
@Override
public void run() {
//获得用户输入的区域名称
String zone = editText.getText().toString();
//调用Google 交通API查询指定区域的交通情况
String traffic = getTrafficByZone(zone);
//把交通息显示在title上
setTitle(traffic);
}
}.start();
}

但是你会发现Android会提示程序由于异常而终止。为什么会出错呢?在LogCat中打印的日志信息就会发现这样的错误日志:
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
错误信息不难看出Android禁止其他子线程来更新由UI线程创建的UI组件。本例中显示交通信息的title实际是就是一个由UI thread所创建的TextView,所以参试在一个子线程中去更改TextView的时候就出错了。这显示违背了单线程模型的原则:Android UI操作并不是线程安全的, 并且这些操作必须在UI线程中执行。啥意思,就是说如果由多个线程都对UI组件进行操作,无法保证其正确行为。

什么是线程安全?
  如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
  线程安全问题都是由全局变量及静态变量引起的。
  若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

2.2 Message Queue
在单线程模型下,为了解决类似的问题,Android设计了一个Message Queue(消息队列),线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:
lMessage Queue
Message Queue用来存放通过Handler发布的消息。消息队列通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列。Android在第一启动程序时会默认会为UI thread创建一个关联的消息队列,用来管理程序的一些上层组件,activities,broadcast receivers 等等。你可以在自己的子线程中创建Handler与UI thread通讯。

2Handler
通过Handler你可以发布或者处理一个消息或者是一个Runnable的实例。没个Handler都会与唯一的一个线程以及该线程的消息队列管理。当你创建一个新的Handler时候,默认情况下,它将关联到创建它的这个线程和该线程的消息队列。也就是说,如果你通过Handler发布消息的话,消息将只会发送到与它关联的这个消息队列,因而也只能处理该消息队列中的消息。
主要的方法有:
a)public final boolean sendMessage(Message msg)
把消息放入该Handler所关联的消息队列,放置在所有当前时间前未被处理的消息后。
b)public void handleMessage(Message msg)
关联该消息队列的线程将通过调用Handler的handleMessage方法来接收和处理消息,通常需要子类化Handler来实现handleMessage。

3Looper
Looper扮演着一个Handler和消息队列之间通讯桥梁的角色。程序组件首先通过Handler把消息传递给Looper,Looper把消息放入队列。Looper也把消息队列里的消息广播给所有的Handler,Handler接受到消息后调用handleMessage进行处理。
a)可以通过Looper类的静态方法Looper.myLooper得到当前线程的Looper实例,如果当前线程未关联一个Looper实例,该方法将返回空。
b)可以通过静态方法Looper. getMainLooper方法得到主线程的Looper实例
线程,消息队列,Handler,Looper之间的关系可以通过一个图来展示:

现在将把交通信息的案例通过消息队列来重新实现:

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

@Override
public void onClick(View v) {
//创建一个子线程去做耗时的网络操作
new Thread() {
@Override
public void run() {
//活动用户输入的区域名称
String zone = editText.getText().toString();
//调用Google 交通API查询指定城市的交通情况
String trafficInfo = getTrafficInfoByZone(zone);
//创建一个Message对象,并把得到的交通信息赋值给Message对象
Message message = Message.obtain();
message.obj = trafficInfo;
//通过Handler发送携带有交通情况的消息
messageHandler.sendMessage(message);
}
}.start();
}

//子类化一个Handler
class MessageHandler extends Handler {
public MessageHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
//处理收到的消息,把交通信息显示在title上
setTitle((String) msg.obj);
}
}

现在程序已经可以成功运行,因为Handler的handleMessage方法是由关联有该消息队列的UI 线程调用的,从而回避了线程安全问题。

更多相关文章

  1. Android(安卓)Learning:多线程与异步消息处理机制
  2. 金三银四热潮下。Android高级工程师面试题整理
  3. android 实现一个按钮按下时总触发一个事件
  4. Android(安卓)高质量开发之崩溃优化
  5. mars Android视频第14讲中代码出现的错误分析——Handler中的rem
  6. 读书笔记-Android开发艺术探索-第11章-Android的线程和线程池
  7. Android(安卓)AsyncTask 的使用
  8. 取得Wear OS和Android对话:通过可穿戴数据层交换信息
  9. android vold初始化及sd卡挂载流程

随机推荐

  1. 云服务器为什么要设置防火墙?怎么设置防火
  2. 静态延迟绑定及静态单例模式加载以及单例
  3. 原生分页,文件上传后端要做哪些拦截,.事
  4. 基于VUE的后台管理系统
  5. MyCms 自媒体 CMS v3.0,资源推送优化,新增
  6. 如何绘制动漫人物头发?简单手绘动漫女生发
  7. 登陆验证码实例、接口初识
  8. Docker 安装 mysql8
  9. Linux 发行版更新软件源
  10. 使用 Packer 创建自定义镜像