在Android编程的过程中,如果在Activity中某个操作会运行比较长的时间,比如:下载文件。这个时候如果在主线程中直接下载文件,会造成Activity卡死的现象;而且如果时间超过5秒,会有ANR报错。
在这种情况下, 可以使用Handler来处理。
涉及到的类主要有:Handler、Thread、Message、MessageQueue、Looper、HandlerThread
如果是针对上面的情况,可以只使用Handler、Message和Thread就可以解决。在Thread中处理下载文件的过程,并在结束后,向Handler发送消息(Message)。Handler在接收到消息后对界面进行修改。
如果只是不想在界面线程中进行下载文件的操作,那只需要创建一个新的线程来处理下载文件的过程就可以,为什么还要使用Handler呢?因为Android的界面线程是不安全的,所以如果直接在Thread中修改界面会报错,所以要使用Handler来处理。当在界面线程中获取Handler时,Handler将和界面线程在同一个线程中,所以通过Handler修改界面将不会报错。
1、Handler要重写handleMessage方法,用来处理修改界面的逻辑。
handler = new Handler() { @Override public void handleMessage(Message msg) { // TODO Auto-generated method stub super.handleMessage(msg); switch (msg.what) { case MessageType.ProgressType : int arg1 = msg.arg1; if (arg1 >= maxProgress) { Toast.makeText(MainActivity.this, "Progress is OK", Toast.LENGTH_SHORT); return; } else if (arg1 < 0) { arg1 = 0; } progressBar.setProgress(arg1); break; default : break; } } };
说明:
- 此处直接使用没有参数的Handler构造函数,表示获取当前线程的Looper,而主线程在创建的时候默认生成了一个Looper,所以在主线程的Activity中,可以直接使用Handler的无参构造函数。
- 如果在主线程中想要获取主线程的Looper,可以使用Looper.myLooper()方法获取。
- 在其他线程中要想获取主线程的Looper,需要使用Looper.getMainLooper()方法获取,在主线程中也可以使用这个方法获取。
- 在其他线程中,如果直接使用Looper.myLooper()方法,将获取到null。
2、Thread中要在下载完文件后(我使用了TimerTask和Timer来实现100毫秒进度加1),向Handler发送消息。
public class MyProgressTimerTask extends TimerTask { private Handler handler; private int count; public MyProgressTimerTask(Handler handler) { this.handler = handler; count = 0; } @Override public void run() { // TODO Auto-generated method stub count++; Message message = handler.obtainMessage(); message.what = MessageType.ProgressType; message.arg1 = count; handler.sendMessage(message); Looper looper = Looper.myLooper(); if (null == looper) { System.out.println(">>>>>>>>>>>>>>>>Looper.myLooper is null<<<<<<<<<<<<<<"); } } }
3、在点击开始按钮时,启动Timer;点击停止时,关闭Timer。也可以不使用Timer,而是使用Thread,在Thread中sleep一段时间,依然可以达到预期的效果。同时,两种方式都可以对Activity进行操作,不会有死机的感觉。
progressBar = (ProgressBar) findViewById(R.id.progressBar); maxProgress = progressBar.getMax(); btnStartProgress = (Button) findViewById(R.id.btnStartProgress); btnStartProgress.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub task = new MyProgressTimerTask(handler); timer = new Timer(); timer.schedule(task, 1000, 100); } }); btnStopProgress = (Button) findViewById(R.id.btnStopProgres); btnStopProgress.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub if (task != null) { task.cancel(); } if (timer != null) { timer.cancel(); } task = null; timer = null; } });
注意:在生成Message的时候推荐使用Handler.obtainMessage()和Message.obtain()方法,因为这两个方法(其实最终都是调用Message.obtain()方法)不是简单的new一个对象。分析代码可以看出,它会先判断消息池中是否有可用的消息(由sPool指示开始Message的Message链表),如果没有才会去创建新的Message。
主线程:
Activity的绘制是在主线程中完成的,而主线程在创建的时候,就会创建Looper对象,所以界面线程使用Handler的时候,不需要考虑创建Looper的问题。那是不是Activity等组件的所有操作都在主线程中进行呢?通过实验发现,基本可以这样理解,当然如果在组件中创建新的线程,在新的线程的操作当然不可能在主线程中进行(比如new一个Thread,在Thread中计算1——100000的和)。除了这样的操作外,可以理解为都在主线程中处理,即使这个组件是由new Thread调用startActivity创建的。
我写了这样一段代码。首先在启动的Activity中,启动一个新的线程,在这个新的线程中分别启动一个Activity、Service、BroadcastReceiver。在Activity的onCreate、onStart、onResume、onPause、onStop、onDestroy、onRestart、onActivityResult方法中打印当前线程,结果打印出的都是主线程。在Service的onCreate中和BroadcastReceiver的onReceive中也是相同的效果。所以基本可以说组件的操作都是在主线程中进行的。
因此,如果Handler主要用途是修改界面的话,那HandlerThread就不需要使用了。但是如果考虑到这样的场景,在新启动的线程A中,任务还是太多,又必须启动其他线程,而线程A又需要获得其他线程的返回结果。这时,使用HandlerThread可能就能很方便的解决问题了。Looper的使用应该很少了,HandlerThread的作用就是屏蔽掉Looper,直接使用Looper比较复杂,需要先使用Looper.prepare,然后在所有操作结束后调用Looper.loop使其循环读取消息队列中的消息。使用HandlerThread后,程序将极度简化。
参考资料 :
深入理解Android消息处理系统——Looper、Handler、Thread
Android HandlerThread 的使用及其Demo