Android读书学习笔记之Handler消息传递机制
背景:
出于性能优化考虑,Android的UI操作并不是线程安全的,这意味着如果有过个线程并发操作UI组件,则可能导致线程安全问题。为了解决这个问题,Android制定了一条简单的规则:只允许UI线程修改Activity的UI组件。
Android平台只允许UI线程修改Activity里的UI组件,这样就会导致新启动的线程无法动态改变界面组件的属性值。但在实际Android应用开发中,尤其是涉及动画的游戏开发中,需要让新启动的线程周期性改变界面组件的属性值,这就需要借助于Handler的消息传递机制来实现了的。
一.Handler类简介
handler类的主要作用有两个:
在新启动的线程中发送消息;
在主线程中获取,处理消息。
实例:自动播放动画
本实例通过一个新线程来周期性地修改ImageView所显示的图片,通过这种方式来开发一个动画效果。
public class MainActivity extends Activity
{
// 定义周期性显示的图片的ID
int[] imageIds = new int[]
{
R.drawable.java,
R.drawable.javaee,
R.drawable.ajax,
R.drawable.android,
R.drawable.swift
};
int currentImageId = 0;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final ImageView show = (ImageView) findViewById(R.id.show);
final Handler myHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
// 如果该消息是本程序所发送的
if (msg.what == 0x1233)
{
// 动态地修改所显示的图片
show.setImageResource(imageIds[currentImageId++
% imageIds.length]);
}
}
};
// 定义一个计时器,让该计时器周期性地执行指定任务
// Timer对象可调度TimerTask对象,TimerTask对象的本质就是启动一条新线程。
new Timer().schedule(new TimerTask()
{
@Override
public void run()
{
// 发送空消息
myHandler.sendEmptyMessage(0x1233);
}
}, 0, 1200);
}
}
二.Handler,Loop,MessageQueue的工作原理
Message:Handler接收和处理的消息对象
Looper:每个线程只能拥有一个Looper.它的loop方法负责读取MessageQueue中的消息,会不断的从MessageQueue中取出消息,读到信息之后就把消息交给发送该消息的Handler进行处理。
MessageQueue:消息队列,由Looper负责管理,它采用先进先出的方式来管理Message。程序创建Looper对象时,会在它的构造器中创建MessageQueue对象。
Handler: 他能把消息发送给Looper管理的MessageQueue,并负责处理Looper分给它的消息。
private Looper(){
mQueue = new MessageQueue();
mRun = true;
mThread = Thread.currentThread();
}
如果希望Handler正常工作,必须在当前线程中有一个Looper对象,因为MessageQueue是由Looper负责管理的,也就是说,为了保证当前线程中有Looper对象,可以分如下两种情况处理。
1.在主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可通过Handler来发送消息,处理消息了。
2.程序员自己启动的子线程,必须自己创建一个Looper对象,并启动它,创建Looper对象调用它的prepare方法即可。
prepare方法保证每个线程最多只有一个Looper对象。
/frameworks/base/core/java/android/os/Looper.java
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
loop()方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。
源代码如下:
/frameworks/base/core/java/android/os/Looper.java
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
for (;;) {
//获取消息队列中的下一个消息,如果没有消息,将会阻塞
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//使用final修饰该标识符,保证在分发消息的过程中线程标识符不会被修改。
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
那么在线程中使用Handler的步骤:
1.调用Looper的prepare方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。
2.有了Looper之后,创建Handler子类的实例,重写handleMessage方法,该方法负责处理来自其他线程的消息。
3.调用Looper的loop方法启动looper。
实例:使用新线程计算质数
允许用户输入一个数值上限,当用户单击“计算”按钮时,该应用会将该上限数值发送到新启动的线程中,让该线程来计算该范围内的所有质数。
之所以不直接在UI线程中计算该范围内的所有质数,因为UI线程需要响应用户动作,如果在UI线程中执行一个耗时操作,将会导致UI线程被阻塞,从而让应用程序失去响应。
MainActivity.java
public class MainActivity extends Activity
{
static final String UPPER_NUM = "upper";
EditText etNum;
CalThread calThread;
// 定义一个线程类
class CalThread extends Thread
{
public Handler mHandler;
public void run()
{
Looper.prepare();
mHandler = new Handler()
{
// 定义处理消息的方法
@Override
public void handleMessage(Message msg)
{
if(msg.what == 0x123)
{
int upper = msg.getData().getInt(UPPER_NUM);
List<Integer> nums = new ArrayList<Integer>();
// 计算从2开始、到upper的所有质数
outer:
for (int i = 2 ; i <= upper ; i++)
{
// 用i除以从2开始、到i的平方根的所有数
for (int j = 2 ; j <= Math.sqrt(i) ; j++)
{
// 如果可以整除,则表明这个数不是质数
if(i != 2 && i % j == 0)
{
continue outer;
}
}
nums.add(i);
}
// 使用Toast显示统计出来的所有质数
Toast.makeText(MainActivity.this, nums.toString()
, Toast.LENGTH_LONG).show();
}
}
};
Looper.loop();
}
}
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
etNum = (EditText)findViewById(R.id.etNum);
calThread = new CalThread();
// 启动新线程
calThread.start();
}
// 为按钮的点击事件提供事件处理方法
public void cal(View source)
{
// 创建消息
Message msg = new Message();
msg.what = 0x123;
Bundle bundle = new Bundle();
bundle.putInt(UPPER_NUM ,
Integer.parseInt(etNum.getText().toString()));
msg.setData(bundle);
// 向新线程中的Handler发送消息
calThread.mHandler.sendMessage(msg);
}
}
三.异步任务(AsyncTask)
Andorid默认约定当UI线程阻塞超过20s时将会引发ANR(Application Not Responding)异常。
为了避免UI线程失去响应的问题,Andorid建议将耗时操作放在新线程中完成,但新线程也可能需要动态更新UI组件,比如需要从网上获取一个网页,然后在TextView中将其源代码显示出来,此时就应该将连接网络,获取网络数据的操作放在新线程中完成。问题是:
获取网络数据之后,新线程不允许直接更新UI组件。
而异步任务则可进一步简化这种操作,适用于简单的异步处理,不需要借助线程和Handler即可实现。
实例:使用异步任务执行下载
本实例示范如何使用异步任务下载网络资源,界面布局很简单,只包含两个组件,一个文本框用于显示网络下载的页面代码;一个按钮用于激发下载任务。
MainActivity.java
package org.crazyit.handler;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class MainActivity extends Activity
{
private TextView show;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
show = (TextView) findViewById(R.id.show);
}
// 重写该方法,为界面的按钮提供事件响应方法
public void download(View source) throws MalformedURLException
{
DownTask task = new DownTask(this);
task.execute(new URL("http://www.crazyit.org/ethos.php"));
}
class DownTask extends AsyncTask<URL, Integer, String>
{
// 可变长的输入参数,与AsyncTask.exucute()对应
ProgressDialog pdialog;
// 定义记录已经读取行的数量
int hasRead = 0;
Context mContext;
public DownTask(Context ctx)
{
mContext = ctx;
}
@Override
protected String doInBackground(URL... params)
{
StringBuilder sb = new StringBuilder();
try
{
URLConnection conn = params[0].openConnection();
// 打开conn连接对应的输入流,并将它包装成BufferedReader
BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()
, "utf-8"));
String line = null;
while ((line = br.readLine()) != null)
{
sb.append(line + "\n");
hasRead++;
publishProgress(hasRead);
}
return sb.toString();
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(String result)
{
// 返回HTML页面的内容
show.setText(result);
pdialog.dismiss();
}
@Override
protected void onPreExecute()
{
pdialog = new ProgressDialog(mContext);
// 设置对话框的标题
pdialog.setTitle("任务正在执行中");
// 设置对话框显示的内容
pdialog.setMessage("任务正在执行中,敬请等待...");
// 设置对话框不能用“取消”按钮关闭
pdialog.setCancelable(false);
// 设置该进度条的最大进度值
pdialog.setMax(202);
// 设置对话框的进度条风格
pdialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
// 设置对话框的进度条是否显示进度
pdialog.setIndeterminate(false);
pdialog.show();
}
@Override
protected void onProgressUpdate(Integer... values)
{
// 更新进度
show.setText("已经读取了【" + values[0] + "】行!");
pdialog.setProgress(values[0]);
}
}
}
上面程序的download()方法很简单,它只是创建了DownTask(AsyncTask的子类)实例,并调用它的execute方法开始执行异步任务。
该程序的重点是实现AsyncTask的子类,实现该子类时实现了如下4个方法:
doInBackground():该方法的代码完成实际的下载任务;
onPreExecute():该方法的代码负责在下载开始的时候显示一个进度条;
onProgressUpdate():该方法的代码负责随着下载进度的改变更新进度条的进度值;
onPostExecute():该方法的代码负责当下载完成后,将下载的代码显示出来;