更新UI的时候主线程必须是目标线程,如何掌握这个主动性?是通过Looper和HandlerThread实现的。Android中每一个线程都跟着一个Looper,Looper可以帮助线程维护一个消息队列,Looper对象的执行需要初始化Looper.prepare方法,使用Looper.loop方法启动消息队列管理机制,退出时还要使用Looper.release方法释放资源,下面代码为在Android中创建一个Thread类线程:
以下是代码片段:
class LooperThread extends Thread {
public Handler mHandler;
public void run {
Looper.prepare;
mHandler = new Handler {
public void handleMessage(Message msg) {
/* process incoming messages here */
}
};
Looper.loop;
}
}
Android使用Looper机制才能接收消息和处理计划任务,上面的代码编写起来实在是麻烦,所以Android提供了一个线程类——HanderThread类,HanderThread类继承了Thread类,它封装了Looper对象,使我们不用关心Looper的开启和释放的细节问题。HandlerThread对象中可以通过getLooper方法获取一个Looper对象引用。
不管是主线程(一般是我们的UI线程)还是子线程,只要有Looper的线程,别的线程就可以向这个线程的消息队列中发送消息和计划任务,然后做相应的处理。
我们通过debug方式看看案例chapter8_5线程情况,在程序的39行和56行设置断点,39行是handleMessage方法用于接收消息的目标线程,56行是发出消息的线程。程序以debug模式运行,在断点56行挂起了,如图8-14所示。
▲图8-14 debug模式运行图一
从图8-14可以看出,发送消息的程序隶属于Thread-8线程,它不是主线程。接着运行程序挂起在39行,如图8-15所示。
在图8-15中我们可以看到,接收消息的处理方法隶属于main线程,即主线程(UI线程),在8-15中还可以看到Looper对象。
比较发送消息和提交计划任务的不同之后,再以同样的方式debug一下程序chapter8_6代码,在程序chapter8_6的42行和50行设定断点,42行执行计划任务的目标线程,并以debug模式运行,如图8-16所示。
▲图8-16 debug模式运行图三
如图8-16所示,程序挂起在50行,提交计划任务请求的程序隶属于main线程(UI线程),接着运行程序挂起在42行,如图8-17所示。
▲图8-17 debug模式运行图四
图8-17所示程序挂起在42行,run方法是接收计划任务的程序,它也隶属于main线程(UI线程)。事实上chapter8_6程序只有一个UI主线程,它们在UI主线程中提交计划任务请求,再由UI线程作为目标线程接收计划任务执行。
但是在chapter8_5和chapter8_6的程序代码中并没有看到有关Looper和HandlerThread代码,这是因为默认情况下系统会创建一个无参构造方法的Handler对象,利用这种方法Handler可以自动与当前运行线程(UI线程)的Looper关联。
如果使用HandlerThread类替代Thread类创建线程对象,就可以直接使用HandlerThread线程中的Looper了,再使用这个Looper对象构造Handler对象,这样接收消息的目标线程就不是UI线程了。但就本例而言这种处理会有问题,我们先看看代码清单8-8,完整代码请参考chapter8_6工程中 chapter8_6_2代码部分。
【代码清单8-8】
public class chapter8_6_2 extends Activity {
private String TAG = "chapter8_6_2";
private Button btnEnd;
private TextView labelTimer;
private boolean isRunning = true;
private Handler handler;
private int timer = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnEnd = (Button) findViewById(R.id.btnEnd);
btnEnd.setOnClickListener(new OnClickListener {
@Override
public void onClick(View v) {
isRunning = false;
}
});
labelTimer = (TextView) findViewById(R.id.labelTimer);
HandlerThread thread = new HandlerThread("MyHandlerThread");
thread.start;
handler = new Handler(thread.getLooper);
Runnable r = new Runnable {
public void run {
if (isRunning) {
labelTimer.setText("逝去了 " +timer + " 秒");
timer++;
handler.postDelayed(this, 1000);
}
}
};
handler.postDelayed(r, 1000);
}
}
chapter8_6_2中用new HandlerThread("MyHandlerThread")创建HandlerThread线程,通过thread.start方法启动该线程。HandlerThread的getLooper方法可以获得与HandlerThread线程对象关联的Looper对象。再用Looper对象构建new Handler(looper)。我们再通过debug看看chapter8_6_2的线程情况,在程序chapter8_6_2的46行和53行设定断点,并以debug模式运行,如图8-18所示。
如图8-18所示,程序挂起在53行,提交计划任务请求的程序隶属于main线程(UI线程),接着运行程序挂起在46行,如图8-19所示。
如图8-19所示,程序挂起在46行,run方法是接收计划任务的程序,它隶属于MyHandlerThread线程(非UI线程)。这样接收计划任务的线程体run就放到一个子线程中了,可见Looper是关键,Looper在哪个线程,哪个线程就可以接收消息和计划任务了。
▲图8-18 debug模式运行图五
▲图8-19 debug模式运行图六
事实上chapter8_6_2代码执行是有问题的,会出现错误“Only the original thread that created a view hierarchy can touch its views.”。这个错误应该不陌生,它是由于在非主线程(UI线程)中更新了UI,这是因为chapter8_6_2中把两个线程的职责掉换了,就这个案例本身而言这样修改没有意义,只是想通过这个例子让大家熟悉Looper和HandlerThread的使用。
小结
通过计时器案例向大家介绍了Java线程和Android中的线程。关键是Android线程,它与一般的Java多线程处理方式是不同的,其中重点是消息发送和计划任务,接受消息发送和计划任务的处理是目标线程,它是通过Looper机制维护消息队列,如果应用中有包含更新UI处理,则要把更新UI的处理代码放置在目标线程中,这个时候还要保障更新UI的线程是主线程。