[转载]Android UI 的更新及其线程模型
让我们通过一个交通状况查询Activity来讨论下Android 的UI 界面更新问题:
当用户输入区域名称,然后单击按钮进行查询后,程序会调用相应接口获得指定区域的交通状况摘要。当网络出现异常或者服务繁忙的时候都会使访问网络的动作很耗时,这时,Android会提示一个程序无法响应的异常,该对话框会询问用户是继续等待还是强行退出程序,这样就大大的降低用户体验。所以我们需要参试以别的方式来实现:
2.1创建子线程更新UI
显然如果你的程序需要执行耗时的操作的话,如果像上例一样由主线程来负责执行该操作是错误的。所以我们需要在onClick方法中创建一个新的子线程来负责调用相应借口来获得交通信息数据:
publicvoidonClick(Viewv){
//创建一个子线程执行从网络上获取交通信息的操作
newThread(){
@Override
publicvoidrun(){
//获得用户输入的区域名称
Stringzone=editText.getText().toString();
//调用Google交通API查询指定区域的交通情况
Stringtraffic=getTrafficByZone(zone);
//把交通息显示在title上
setTitle(traffic);
}
}.start();
}
但是你会发现Android会提示程序由于异常而终止。为什么会出错呢?在LogCat中打印的日志信息就会发现这样的错误日志:
android.view.ViewRoot$CalledFromWrongThreadException:Onlytheoriginalthreadthatcreatedaviewhierarchycantouchitsviews.
错误信息不难看出Android禁止其他子线程来更新由UI线程创建的UI组件。本例中显示交通信息的title实际是就是一个由UIthread所创建的TextView,所以参试在一个子线程中去更改TextView的时候就出错了。这显示违背了单线程模型的原则:AndroidUI操作并不是线程安全的,并且这些操作必须在UI线程中执行。啥意思,就是说如果由多个线程都对UI组件进行操作,无法保证其正确行为。
什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
2.2MessageQueue
在单线程模型下,为了解决类似的问题,Android设计了一个MessageQueue(消息队列),线程间可以通过该MessageQueue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:
lMessageQueue
MessageQueue用来存放通过Handler发布的消息。消息队列通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列。Android在第一启动程序时会默认会为UIthread创建一个关联的消息队列,用来管理程序的一些上层组件,activities,broadcastreceivers等等。你可以在自己的子线程中创建Handler与UIthread通讯。
2Handler
通过Handler你可以发布或者处理一个消息或者是一个Runnable的实例。没个Handler都会与唯一的一个线程以及该线程的消息队列管理。当你创建一个新的Handler时候,默认情况下,它将关联到创建它的这个线程和该线程的消息队列。也就是说,如果你通过Handler发布消息的话,消息将只会发送到与它关联的这个消息队列,因而也只能处理该消息队列中的消息。
主要的方法有:
a)publicfinalbooleansendMessage(Messagemsg)
把消息放入该Handler所关联的消息队列,放置在所有当前时间前未被处理的消息后。
b)publicvoidhandleMessage(Messagemsg)
关联该消息队列的线程将通过调用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之间的关系可以通过一个图来展示:
现在将把交通信息的案例通过消息队列来重新实现:
privateEditTexteditText;
privateHandlermessageHandler;
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
editText=(EditText)findViewById(R.id.weather_city_edit);
Buttonbutton=(Button)findViewById(R.id.goQuery);
button.setOnClickListener(this);
//得到当前线程的Looper实例,由于当前线程是UI线程也可以通过Looper.getMainLooper()得到
Looperlooper=Looper.myLooper();
//此处甚至可以不需要设置Looper,因为Handler默认就使用当前线程的Looper
messageHandler=newMessageHandler(looper);
}
@Override
publicvoidonClick(Viewv){
//创建一个子线程去做耗时的网络操作
newThread(){
@Override
publicvoidrun(){
//活动用户输入的区域名称
Stringzone=editText.getText().toString();
//调用Google交通API查询指定城市的交通情况
StringtrafficInfo=getTrafficInfoByZone(zone);
//创建一个Message对象,并把得到的交通信息赋值给Message对象
Messagemessage=Message.obtain();
message.obj=trafficInfo;
//通过Handler发送携带有交通情况的消息
messageHandler.sendMessage(message);
}
}.start();
}
//子类化一个Handler
classMessageHandlerextendsHandler{
publicMessageHandler(Looperlooper){
super(looper);
}
@Override
publicvoidhandleMessage(Messagemsg){
//处理收到的消息,把交通信息显示在title上
setTitle((String)msg.obj);
}
}
现在程序已经可以成功运行,因为Handler的handleMessage方法是由关联有该消息队列的UI 线程调用的,从而回避了线程安全问题。原文转载: Android UI 的更新及其线程模型