异步消息处理机制:Handler
1.Handler是什么?
在开发中更新视图都需要在主线程中更新,子线程是不支持更新视图操作的。所以当我们做一些耗时操作的时候可以不能马上得到反馈刷新UI,比如下载文件或者下载图片这些操作都比较耗时,我们一般会重新创建一个子线程异步处理耗时操作,这样就不会堵塞主线程导致卡顿的情况。异步处理成功后如果这个时候我们需要更新视图操作就不能直接更新了,这个时候Handler就起到了作用。我们可以用Handler、Message、 Looper、 MessageQueue这套异步消息处理机制来处理这种情况,线程中发送消息通知,主线程来处理消息刷新视图。
简单来说,Handler是更新UI界面的机制,也是消息处理的机制,可以发送消息,也可以处理消息。
2.为什么要有Handler?
Android在设计的时候,封装了一套消息创建、传递、处理机制,如果不遵循这样的机制就没办法更新UI信息,就会抛出异常。
3.Handler怎么用?
a)、传递message:
用于接受子线程发送的数据,并用此数据配合主线程更新 UI。有以下方法:
//post类方法允许把一个Runnable对象放到主线程的队列中
post(Runnable);
postAtTime(Runnable,long);
postDelayed(Runnable,long);
b)、传递Runnable对象:
用于通过 Handler 绑定的消息队列,安排不同操作的执行顺序,主要有以下方法:
sendEmptyMessage(int);
sendMessage(Message);
sendMessageAtTime(Message, long);
sendMessageDelayed(Message, long);
sendMessage 类方法,允许把一个带数据的 Message 对象放到队列中,等待更新。
1、使用 Handler 在子线程中向 UI 线程发送一个消息进行 UI 的更新
2、创建一个 Message,Message msg = new Message(); msg.arg1 = 88;
3、handler.sendMessage(msg); msg.obj = xxx;
可以传递一个对象
4、当然不一定要用 new 一个 Message,也可以复用系统的 message 对象 Message msg = handler.obtainMessage();
c)、传递Callback对象:
Callback 用于截获 handler 发送的消息,如果返回 true 就截获成功不会向下传递了。
public Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Toast.makeText(getApplicationContext(), "HandleMessage 1", Toast.LENGTH_SHORT).show();
return true;
}
}) {
public void handleMessage(Message msg) {
Toast.makeText(getApplicationContext(), "handleMessage 1", Toast.LENGTH_SHORT).show();
};
}
第一个有返回值的 handlerMessage 方法是 Callback 的回调,如果返回true,则不执行下面的 handlerMessage 方法,从而达到拦截 handler 发送的消息的目的,如果返回 false,则会继续执行 handlerMessage 方法。
4.Android为什么只能通过Handler机制更新UI?
最根本的问题解决多线程并发的问题;
假设如果在一个Activity中,有多个线程去更新UI,并且都没有加锁机制,那么就会更新界面混乱;如果对更新UI 的操作都加锁处理就会性能下降。对于上述问题的考虑,Android提供了一套更新UI的机制,我们只需要遵循这样的机制就好了。不用关心多线程的问题,更新UI的操作,都是在主线程的消息队列当中轮询处理的。
5.Handler机制的原理?
a)、Message:
Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。
b)、Handler:
Handler是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法,而发出的消息经过一系列的辗转处理后,最终会传递到Handler的handleMessage()方法中。每个线程中可以有多个Handler实例。
c)、MessageQueue:
MessageQueue是一个消息队列,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
d)、Looper:
Looper是每个线程中的MessageQueue的管家,调用Looper.loop()方法后,就会进入到一个无限死循环当中,然后每当发现MessageQueue中存放一条消息,就会将它取出,并传递到Handler的handlerMessage()方法中。如果没有消息就阻塞。每个线程中也只会有一个Looper对象。
总结:
在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handlerMessage()方法中。由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会在主线程中运行,在这里可以安心地进行UI操作了。
6.主线程?
UI thread 通常就是main thread,ActivityThread 默认创建main线程,main中默认创建Looper,Looper默认创建MessageQueue,可以定义Handler的子类别来接收Looper所送取出后并发出的消息。threadLocal保存线程的变量信息,方法包括:set,get
7.主线程的消息循环机制是什么(死循环如何处理其他事务)?
事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:
public static void main(String[] args) {
...
//创建Looper和MessageQueue对象,用于处理主线程的消息
Looper.prepareMainLooper();
//创建ActivityThread对象
ActivityThread thread = new ActivityThread();
//建立Binder通道 (创建新线程)
thread.attach(false);
Looper.loop(); //消息循环运行
throw new RuntimeException("Main thread loop unexpectedly exited");
}
Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:一旦退出消息循环,那么程序也就可以退出了。从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。
thread.attach(false)方法函数中便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。
比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。
主线程的消息是App进程中的其他线程通过Handler发送给主线程
system_server进程
system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。
App进程
App进程则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程: ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程
Binder
Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。
小结:
ActivityThread通过ApplicationThread和AMS进行进程间通讯,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityThread中去执行,即切换到主线程中去执行,这个过程就是主线程的消息循环模型。(注意:ActivityThread实际上并非线程,不像HandlerThread类,ActivityThread并没有真正继承Thread类)
8.ActivityThread的动力是什么?
进程:
每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。
线程:
线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法(完全公平调度算法),保证每个task都尽可能公平的享有CPU时间片。
结论:承载ActivityThread的主线程就是由Zygote fork而创建的进程。
9.如何自定义与线程相关的Handler?
class MyThread extends Thread {
public Handler handler;
@Override
public void run() {
Looper.prepare(); //new 一个Looper对象
handler = new Handler() { //拿到当前线程的 Looper 对象
@Override
public void handlerMessage(Message msg) {
System.out.println("current thread:" + Thread.currentThread());
}
};
Looper.loop();//开始死循环处理消息
};
}
10.HandlerThread是什么?
HandlerThread 继承于 Thread,所以它本质就是个 Thread。与普通的 Thread 的差别就在于,它有个 Looper 成员变量。这个 Looper 其实就是对消息队列以及队列处理逻辑的封装,简单来说就是消息队列+消息循环。由于HandlerThread的run方法是一个无限循环,因此当不需要使用的时候通过quit或者quitSafely方法来终止线程的执行。
private Handler mHandler = null;
private HandlerThread mHandlerThread = null;
private void sendRunnableToWorker(Ruannable run) {
if (mHandlerThread == null) {
mHandlerThread = new HandlerThread("WorkerThread");
// 给工作者线程低优先级
mHandlerThread.setPriority(Thread.MIN_PRIORITY);
mHandlerThread.start();
}
if (mHandler == null) {
mHandler = new Handler(mHandlerThread.getLooper());
}
mHandler.post(run);
}
11.Android中更新UI有哪几种方式?
Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的
1、主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI。
2、用Activity对象的runOnUiThread方法。
3、创建Handler,传入getMainLooper。
4、View.post(Runnable r) 。
12.非UI线程真的不能更新UI吗?
不一定,之所以子线程不能更新界面,是因为Android在线程的方法里面采用checkThread进行判断是否是主线程,而这个方法是在ViewRootImpl中的,这个类是在onResume里面才生成的,因此,如果这个时候子线程在onCreate方法里面生成更新UI,而且没有做阻塞,就是耗时多的操作,还是可以更新UI的。
13.主线程怎么通知子线程?
可以利用HandlerThread进行生成一个子线程的Handler,并且实现handleMessage方法,然后在主线程里面也生成一个Handler,然后通过调用sendMessage方法进行通知子线程。同样,子线程里面也可以调用sendMessage方法进行通知主线程。耗时操作应该放在子线程中,如:图片加载、网络访问等。
14.子线程中Toast、showDialog的方法?
子线程不能更新Toast的原因就和Handler有关了,据我们了解,每一个Handler都要有对应的Looper对象,那么如果在子线程中加入Looper对象,就不报错了。
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
通过查阅Toast的show()方法源码,可以总结出:
1.Toast本质是通过window显示和绘制的(操作的是window),而主线程不能更新UI是因为ViewRootImpl的checkThread方法在Activity维护的View树的行为。
2.Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();(但是不建议这么做,因为它会使线程无法执行结束,导致内存泄漏。Dialog也是如此)
15.Looper死循环为什么不会导致应用卡死,会消耗大量资源吗?
线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。ActivityThread的main方法中我们会看到主线程也是通过Looper方式来维持一个消息循环。
public static void main(String[] args) {
...
Looper.prepareMainLooper();//创建Looper和MessageQueue对象,用于处理主线程的消息
ActivityThread thread = new ActivityThread();
thread.attach(false);//建立Binder通道 (创建新线程)
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
//如果能执行下面方法,说明应用崩溃或者是退出了...
throw new RuntimeException("Main thread loop unexpectedly exited");
}
分析:
对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。通过创建新线程的方式,解决了虽是死循环却可以处理其他事务。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,Looper.loop()本身不会导致应用卡死。
主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
16.如何处理Handler使用不当导致的内存泄漏?
如使用Looper.prepare(); ...Looper.loop();
这种方式创建Looper实际上这是非常危险的一种做法。在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(Looper.myLooper().quit();)
如果在Handler的handleMessage()方法中(或run方法)处理消息,如果这是一个延时消息,就会一直保存在主线程的消息队列中,并且会影响系统对Activity的回收,造成内存泄漏。
解决方法:
1.有延时消息,要在Activity销毁的时候移除Messages。
2.匿名内部类导致的泄漏改为静态匿名内部类,并且对上下文或者Activity使用弱引用。
上一篇:Android基础(6)—滑动组件之ListView和RecyclerView
下一篇:Android基础(8)—数据持久化技术
精彩内容不够看?更多精彩内容,请到微信搜索 “危君子频道” 订阅号,每天更新,欢迎大家关注订阅!