6.4 Handle对象的使用
对象Handle的主要作用是可以发送和处理消息队列,在Android中模仿了Windows操作系统中的Message原理来实现组件间的解耦,它可以接受子线程发送的Message对象,并用此Message对象中封装的数据在主线程中更新UI界面。
需要注意的是,在UI线程中启动Handler对象时,Handler与调用者Activity处于同一线程,也就是通常所说的UI线程。如果Handler里面做耗时的动作,UI线程会阻塞,另外由于Android的UI线程不是安全的,并且这些操作必须在UI线程中执行,如果不是在UI线程中操作View对象则系统报出异常。每个Handler实例都会绑定到创建它的线程中(一般是位于主线程)。
在Android中进行与UI通信的开发时,经常会使用Handler对象来控制UI程序的界面,它的作用可以理解为与其他线程协同工作,接收其他线程的消息并通过接收到的消息更新UI界面。
现在有这么一种情况,在一个UI界面上有一个按钮,当单击这个按钮的时候会进行网络连接,并把网络上的数据取下来显示到UI界面中一个TextView里,这时出现一个问题,就是如果这个网络连接的延迟过大,或根本连接不上,可能用时数秒甚至更长,那么程序的界面将处于一种假死状态,这样的效果很明显不符合体验性好的软件标准,这时理论上可以创建一个线程,在线程中取得网络上的数据,但下一步出现了问题!在用户自定义的线程中将取到的数据去更新UI则会报出异常,这个情况在第二章已经介绍过此实验,因为Android是单线程模型,不允许程序员在自定义的线程类中直接操作UI界面,为了解决这个问题,Android开发了Handler对象,由它来负责与子线程进行通信,从而让子线程与主线程之间建立起协作的桥梁,当然也就可以传递数据(大多使用Message对象传递),使Android的UI更新问题得到解决。
6.4.1 Handler对象的初步使用(1)
本示例就模拟从网络下载数据再显示到UI界面上的效果。新建名称为handler1的Android项目,文件Main.java的核心代码如下:
- public class Main extends Activity {
- private Button button1;
-
- private Handler handler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- Log.v("!", "Activity print status="
- + msg.getData().getString("status") + " thread name="
- + Thread.currentThread().getName());
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- Log.v("!", "Activity Thread name=" + Thread.currentThread().getName());
-
- button1 = (Button) this.findViewById(R.id.button1);
- button1.setOnClickListener(new OnClickListener() {
- public void onClick(View arg0) {
- GhyThread ghyThreadRef = new GhyThread(handler);
- ghyThreadRef.start();
- }
- });
-
- }
- }
自定义线程类GhyThread.java的核心代码如下:
- public class GhyThread extends Thread {
-
- public GhyThread(Handler handler) {
- super();
- this.handler = handler;
- }
-
- private Handler handler;
-
- @Override
- public void run() {
- super.run();
- try {
- int i = 0;
- while (i < 10) {
- i++;
- Log.v("!", "GhyThread threadName="
- + this.currentThread().getName() + " i=" + i);
- Thread.sleep(1000);
- }
- Bundle bundle = new Bundle();
- bundle.putString("status", "end");
-
- Message message = new Message();
- message.setData(bundle);
- handler.sendMessage(message);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
程序运行后的效果如图6.37所示。
|
图6.37 运行效果 |
6.4.1 Handler对象的初步使用(2)
从图6.37中可以看到,方法handleMessage()是运行在main主线程中的,也就是Handler被绑定到了主线程中。
为了进一步演示Handler绑定到主线程中的情况,新建一个名称为HandlerBindUIThread项目,Activity文件Main.java的代码如下:
- public class Main extends Activity {
-
- private Runnable run = new Runnable() {
- public void run() {
- try {
- Log.v("!", "run thread is=" + Thread.currentThread().getId()
- + " thread name=" + Thread.currentThread().getName());
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.v("!", "onCreate thread is=" + Thread.currentThread().getId()
- + " thread name=" + Thread.currentThread().getName());
- Log.v("!", "begin");
- long beginTime = System.currentTimeMillis();
- Handler hanlder = new Handler();
- hanlder.post(run);
- setContentView(R.layout.main);
- long endTime = System.currentTimeMillis();
- Log.v("!", "耗时:" + (endTime - beginTime) / 1000);
- }
- }
程序运行的结果如图6.38所示。
|
图6.38 程序运行结果 |
这是打印出来的结果,真正的运行流程是先打印出图6.38所示的日志信息,然后项目挂起10秒钟后再显示出界面,从图6.38中还可以看到都是在线程名称为main中运行,即属于同步的方式运行,具有"阻塞"的特点,有没有办法实现异步方式运行呢?也就是新开启一个线程运行,并且不耽误Activity界面的显示。这只要将Main.java的代码更改为如下形式就可以实现这种要求。
- public class Main extends Activity {
-
- private Runnable run = new Runnable() {
- public void run() {
- try {
- Log.v("!", "run thread is=" + Thread.currentThread().getId()
- + " thread name=" + Thread.currentThread().getName());
- Thread.sleep(10000);
- Log.v("!", "run end!");
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.v("!", "onCreate thread is=" + Thread.currentThread().getId()
- + " thread name=" + Thread.currentThread().getName());
- Log.v("!", "begin");
- long beginTime = System.currentTimeMillis();
- // Handler hanlder = new Handler();
- // hanlder.post(run);
- Thread thread = new Thread(run);
- thread.start();
- setContentView(R.layout.main);
- long endTime = System.currentTimeMillis();
- Log.v("!", "耗时:" + (endTime - beginTime) / 1000);
- }
- }
6.4.1 Handler对象的初步使用(3)
程序运行后界面也优先显示了出来,如图6.39所示。
|
(点击查看大图)图6.39 先显示界面 |
10秒后打印出了结束日志,结果如图6.40所示。
|
图6.40 打印结束日志 |
对象Handler的初步使用主要体现在构造函数的方法上,这些函数的功能说明如下。
(1)public Handler():无参的构造函数,将创建好的Handler实例绑定到代码所在的线程的消息队列上,因此一定要确定该线程开启了消息队列,否则程序将发生错误,使用这个构造函数创建的Handler实例需要重写Hanler类的handleMessage()方法,以便在之后的消息处理时调用。
(2)public Handler(Callback callback):接口Callback是Handler内部定义的一个接口,因此想要使用这个构造函数创建Handler对象,需要自定义一个类实现Callback接口,并重写接口中定义的handleMessage()方法,这个构造函数其实与无参的构造函数类似,也要确保代码所在的线程开启了消息队列,不同的是在之后处理消息时,将调用接口Callback的handleMessage()方法,而不是Handler对象的handleMssage()方法。
(3)public Handler(Looper looper):表示创建一个Handler实例并将其绑定在Looper所在的线程上,此时looper不能为null,一般也需要重写Hanler类的handleMessage()方法。
(4)public Handler(Looper looper,Callback callback):与(2)和(3)功能相结合。
还有几个知识点需要留意:
(1)调用Handler类中以send开头的方法可以将Message对象压入消息队列中,调用Handler类中以post开头的方法可以将一个Runnable对象包装在一个Message对象中,然后再压入消息队列,此时入队的Message其Callback字段不为null,值就是这个Runnable对象。
(2)调用Message对象的sendToTarget()方法可以将其本身(Message)压入与其target字段(即handler对象)所关联的消息队列中。
6.4.2 postDelayed方法和removeCallbacks方法的使用(1)
方法postDelayed的作用是延迟多少毫秒后开始运行,而removeCallbacks方法是删除指定的Runnable对象,使线程对象停止运行。
方法声明如下:
- public final boolean postDelayed (Runnable r, long delayMillis)
其中参数Runnable r在Handler对象所运行的线程中执行。
创建名称为handler2的Android项目,Main.java的核心代码如下:
- public class Main extends Activity {
- private Button button1;
- private Button button2;
-
- private Handler handler = new Handler();
-
- private int count = 0;
-
- private Runnable runnableRef = new Runnable() {
- public void run() {
- Log.v("2", Thread.currentThread().getName());
- count++;
- Log.v("!", "count=" + count);
- handler.postDelayed(runnableRef, 1000);
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- Log.v("1", Thread.currentThread().getName());
-
- button1 = (Button) this.findViewById(R.id.button1);
- button2 = (Button) this.findViewById(R.id.button2);
-
- button1.setOnClickListener(new OnClickListener() {
- public void onClick(View arg0) {
- Thread thread = new Thread(runnableRef);
- thread.start();
- Log.v("!!!!!!!!!!!!!", "end");
- }
- });
-
- button2.setOnClickListener(new OnClickListener() {
- public void onClick(View arg0) {
- handler.removeCallbacks(runnableRef);
- }
- });
-
- }
- }
程序运行后单击button1按钮开始循环,count累加1,运行结果如图6.41所示。
|
图6.41 循环加1效果 |
从打印结果可以发现,使用代码:
- handler.postDelayed(runnableRef, 1000)
上述代码运行的Runnable并没有新建一个线程,而是运行在main线程里。
当单击button2按钮时,停止这种累加1的功能。
关于循环执行某一个任务还可以使用Java SE自带的类来进行处理,新建名称为TimerTest项目,文件Main.java的代码如下:
- public class Main extends Activity {
- private int count = 0;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- TimerTask task = new TimerTask() {
- @Override
- public void run() {
- Log.v("!", "" + (++count));
- }
- };
- Timer timer = new Timer();
- timer.schedule(task, 1000, 1000);
- }
- }
打印的效果如图6.42所示。
|
图6.42 Timer循环执行某一任务 |
如果想在TimerTask中控制View控件,还需要用Handler对象以发送消息Message的方式来处理View的更新。
6.4.3 post方法的使用
方法post是将Message对象放入消息队列中,以待后面执行消息队列中的任务。
方法声明如下:
- public final boolean post (Runnable r)
其中参数Runnable r在Handler对象所运行的线程中执行。
新建名称为handler3的Android项目,文件Main.java的代码如下:
- public class Main extends Activity {
- private Button button1;
- private int count = 0;
-
- private Handler handler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- Log.v("!", "count=" + msg.getData().getString("count") + " 3"
- + Thread.currentThread().getName());
-
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- Log.v("1", "" + Thread.currentThread().getName());
-
- button1 = (Button) this.findViewById(R.id.button1);
- button1.setOnClickListener(new OnClickListener() {
- public void onClick(View arg0) {
- count = 0;
- handler.post(new Runnable() {
- public void run() {
- count++;
- while (count < 10) {
- try {
- Log.v("2", ""
- + Thread.currentThread().getName());
-
- Bundle bundle = new Bundle();
- bundle.putString("count", "" + count);
-
- Message message = new Message();
- message.setData(bundle);
-
- handler.sendMessage(message);
- count++;
- Thread.sleep(200);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
- });
-
- for (int i = 0; i < 10; i++) {
- try {
- Log.v("!", "i=" + (i + 1));
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
-
- }
- });
- }
- }
程序运行后单击Button按钮出现如图6.43所示的效果。
|
图6.43 运行效果 |
从本示例可以发现,Handler的post()方法对线程的处理也不是真正创建一个新的线程,而是直接调用了线程的run方法。
6.4.4 postAtTime方法的使用
方法postAtTime的作用是实现隔几秒后自动执行,本示例代码在项目handler4中。
- handler.postAtTime(new Runnable() {
- public void run() {
- count++;
- while (count < 10) {
- Bundle bundle = new Bundle();
- bundle.putString("count", "" + count);
-
- Message message = new Message();
- message.setData(bundle);
-
- handler.sendMessage(message);
-
- count++;
- }
- }
- }, SystemClock.uptimeMillis() + 5000);
本示例实现的效果是隔5秒后执行。
6.4.5 在线程对象的run方法中实例化Handler对象的注意事项(1)
在有些情况下,需要在线程中创建Handler对象然后发送消息。
创建名称为threadUseHandler的Android项目,创建自定义Handler对象GhyHandler.java,代码如下:
- package exthandler;
-
- import android.os.Handler;
- import android.os.Message;
- import android.util.Log;
-
- public class GhyHandler extends Handler {
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- Log.v("!", "usernameusername=" + msg.getData().getString("username"));
- }
- }
创建自定义线程类GhyThread.java,代码如下:
- package extthread;
-
- import android.os.Bundle;
- import android.os.Message;
- import exthandler.GhyHandler;
-
- public class GhyThread extends Thread {
-
- @Override
- public void run() {
- super.run();
-
- GhyHandler handler = new GhyHandler();
- Message message = handler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putString("username", "gaohongyan");
- message.setData(bundle);
- handler.sendMessage(message);
-
- }
- }
文件Main.java的代码如下:
- package threadUseHandler.test.run;
-
- import android.app.Activity;
- import android.os.Bundle;
- import extthread.GhyThread;
-
- public class Main extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- GhyThread ghyThreadRef = new GhyThread();
- ghyThreadRef.start();
-
- }
- }
6.4.5 在线程对象的run方法中实例化Handler对象的注意事项(2)
程序运行后出现错误如下:
- java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
出错的原因是当前的线程GhyThread.java并没有创建Looper对象,一个线程可以产生一个Looper对象,由Looper对象来管理线程里的Message Queue(消息队列),Message Queue按顺序处理队列中的Message对象,每一个线程里可含有一个Looper对象以及一个MessageQueue。
Handler在创建的时候可以指定Looper,这样通过Handler的sendMessage()方法发送出去的消息就会添加到指定Looper里面的MessageQueue里面去,但在不指定Looper的情况下,Handler绑定的是创建它的线程的Looper,如果这个线程的Looper不存在,程序将抛出"java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()"的异常,这就是上面代码出错的原因。
介绍到这儿,有必要对一些知识点进行一下总结:
(1)Message消息,可以理解为线程间通信的数据单元,通过将数据放入Message对象中以便达到线程间的通信。例如后台线程在处理数据完毕后需要更新UI,则可发送一条包含最新数据信息的Message给UI线程。
(2)Message Queue消息用来存放通过Handler发布的消息,按照先进先出执行。队列中的每一个Message都有一个when字段,这个字段用来决定Message应该何时处理,消息队列中的每一个Message根据when字段的大小由小到大排列,排在最前面的消息会首先得到处理,因此可以说消息队列并不是一个严格的先进先出的队列。
Message对象的target字段表示关联了哪个线程的消息队列,这个消息就会被压入哪个线程的消息队列中,Message类用于表示消息。Message对象可以通过arg1、arg2、obj字段和setData()携带数据,此外还具有很多字段。when字段决定Message应该何时处理,target字段用来表示将由哪个Handler对象处理这个消息,next 字段表示在消息队列中排在这个Message之后的下一个Message,callback字段如果不为null,表示这个Message包装了一个runnable对象,what字段表示code,即这个消息具体是什么类型的消息。每个what都在其handler的namespace中,只需要确保将由同一个handler处理的消息的what属性不重复就可以。
(3)Handler是Message的主要处理者,负责将Message添加到消息队列以及对消息队列中的Message进行处理。
(4)Looper循环器扮演Message Queue和Handler之间桥梁的角色,循环取出Message Queue里面的Message,并交付给相应的Handler进行处理。Looper类主要用来创建消息队列,每个线程最多只能有一个消息队列,在Android中UI线程默认具有消息队列,但非UI线程在默认情况下是不具备消息队列的,比如自定义的线程类。如果需要在非UI线程中开启消息队列,需要调用Looper.prepare()方法,该方法在执行过程中会创建一个Looper对象,而在源代码中的Looper构造函数中会创建一个MessageQueue实例,此后再为该线程绑定一个Handler实例,再调用Looper.loop()方法,就可以不断地从消息队列中取出消息和处理消息了。Looper.myLoop()方法可以得到线程的Looper对象,如果为null,说明此时该线程尚未开启消息队列。通过Loop.getMainLooper()可以获得当前进程的主线程的Looper对象。
如果想让该线程具有消息队列和消息循环,需要在线程中首先调用Looper.prepare()来创建消息队列,然后调用Looper.loop()进入消息循环,这样该线程就具有了消息处理机制,可以在Handler对象中进行消息处理。
下面来看看其实现方法,更改GhyThread.java的代码如下:
- public class GhyThread extends Thread {
-
- @Override
- public void run() {
- super.run();
-
- Looper.prepare();//准备创建1个Looper对象
-
- GhyHandler handler = new GhyHandler();
- Message message = handler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putString("username", "gaohongyan");
- message.setData(bundle);
- handler.sendMessage(message);
-
- Looper.loop();//执行消息队列中的Message对象
-
- }
- }
程序运行后正确地取出了username的值,如图6.44所示。
|
图6.44 成功打印username的值 |
6.4.6 以异步方式打开网络图片(1)
创建持有PNG图标资源的Web项目pngProject,布署到tomcat中,项目文件结构如图6.45所示。
|
图6.45 持有png图标的web项目 |
创建Android客户端应用程序项目synchronizedOpenNetPNG,由于是以异步方式访问远程PNG图片资源,所以创建自定义线程类OpenNetPNGThread.java,该类主要的功能就是通过远程PNG图片的URL返回Bitmap位图资源,核心代码如下:
- public class OpenNetPNGThread extends Thread {
-
- private String pngPath;
- private Handler handler;
- private int imageViewId;
-
- public OpenNetPNGThread(Handler handler, String pngPath, int imageViewId) {
- super();
- this.pngPath = pngPath;
- this.handler = handler;
- this.imageViewId = imageViewId;
- }
-
- @Override
- public void run() {
- super.run();
- try {
- Log.v("!", "启动线程" + Thread.currentThread().getId() + " "
- + Thread.currentThread().getName());
- URL url = new URL(pngPath);
- URLConnection connection = url.openConnection();
- InputStream isRef = connection.getInputStream();
- Bitmap bitmap = BitmapFactory.decodeStream(isRef);
-
- Bundle bundle = new Bundle();
- bundle.putInt("imageViewId", imageViewId);
- bundle.putParcelable("bitmap", bitmap);
-
- Message message = handler.obtainMessage();
- message.setData(bundle);
- handler.sendMessage(message);
-
- } catch (MalformedURLException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- }
- }
6.4.6 以异步方式打开网络图片(2)
创建自定义Handler对象PNGHandler.java,该类主要的作用是从Message中取出Bitmap资源来对ImageView进行更新,核心代码如下:
- public class PNGHandler extends Handler {
-
- private Context context;
-
- public PNGHandler(Context context) {
- super();
- this.context = context;
- }
-
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
-
- Bundle bundle = msg.getData();
- Bitmap bitmap = bundle.getParcelable("bitmap");
- int imageViewId = bundle.getInt("imageViewId");
-
- ImageView findImageView = (ImageView) ((Activity) context)
- .findViewById(imageViewId);
- findImageView.setImageBitmap(bitmap);
-
- }
- }
项目的核心Activity对象Main.java文件的主要代码如下:
- public class Main extends Activity {
-
- private PNGHandler[] handler = new PNGHandler[5];
- private String[] pngFileName = new String[5];
-
- private ImageView imageView1;
- private ImageView imageView2;
- private ImageView imageView3;
- private ImageView imageView4;
- private ImageView imageView5;
-
- private ImageView[] imageViewArray = new ImageView[5];
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- imageView1 = (ImageView) this.findViewById(R.id.imageView1);
- imageView2 = (ImageView) this.findViewById(R.id.imageView2);
- imageView3 = (ImageView) this.findViewById(R.id.imageView3);
- imageView4 = (ImageView) this.findViewById(R.id.imageView4);
- imageView5 = (ImageView) this.findViewById(R.id.imageView5);
-
- imageViewArray[0] = imageView1;
- imageViewArray[1] = imageView2;
- imageViewArray[2] = imageView3;
- imageViewArray[3] = imageView4;
- imageViewArray[4] = imageView5;
-
- pngFileName[0] = "http://10.0.2.2:8081/pngProject/a.png";
- pngFileName[1] = "http://10.0.2.2:8081/pngProject/b.png";
- pngFileName[2] = "http://10.0.2.2:8081/pngProject/c.png";
- pngFileName[3] = "http://10.0.2.2:8081/pngProject/d.png";
- pngFileName[4] = "http://10.0.2.2:8081/pngProject/e.png";
-
- for (int i = 0; i < handler.length; i++) {
- handler[i] = new PNGHandler(this);
- }
- for (int i = 0; i < handler.length; i++) {
- OpenNetPNGThread mythread = new OpenNetPNGThread(handler[i],
- pngFileName[i], imageViewArray[i].getId());
- mythread.start();
- }
-
- }
- }
6.4.6 以异步方式打开网络图片(3)
程序运行后的结果如图6.46所示。控件ImageView显示出5张图片资源,如图6.47所示。
|
图6.46 运行结果 |
|
图6.47 5张PNG资源显示在ImageView控件中 |