Android之Handler消息处理机制
Handler的作用
Handler消息机制在Android中的应用非常广泛,很多组件的底层实现都是靠Handler来完成的,所以掌握Handler消息机制的原理还是非常重要的。Handler的主要功能有两点:
1.它可以在不同的线程之间传递消息
我们都知道Andorid中规定只有主线程里可以进行UI操作,在其他线程中绝不可以进行与UI相关的操作,否则会报错。还有就是在主线程中也不可以进行耗时操作,否则会阻塞主线程,报ANR。基于以上这两点,我们就可以使用Handler在子线程中处理耗时操作,然后把返回的结果传给主线程,再进行UI方面的更新处理等。
2.它可以延时传递消息。
如果我们想要处理一个延时操作,一般可以通过Thread.sleep(times)使线程休眠几秒,然后再处理数据。但是再Android中是绝不能允许在主线程红进行休眠操作阻塞线程的,所以我们可以通过Handler来发送延迟消息的方式实现延时操作。
消息处理机制
Android的消息处理机制表面上只是使用了Handler,但其实还用到了其他的东西,比如说ThreadLocal,Looper,MessageQuery等,这些组合在一起使用,才构成了整个Android消息机制。
我们先回顾一下Handler的使用:我们以 “在子线程处理完耗时操作,然后通过Handler发送消息到主线程更新UI”为例。首先要先在主线程创建一个Handler对象,重写它的handlerMessage()方法,处理更新UI的操作。然后我们在子线程进行处理耗时操作,当执行完成后,我们创建一个Message对象,将需要传递的数据存入Message对象中,再调用Handler对象的sendMessage()方法将该Message对象发送出去。之后我们就能在Handler的handlerMessage()方法中接收到该Message对象了,然后取出其中的数据,进行更新UI的操作即可,这样整个过程就算完成了。
上面说的例子是从子线程发送消息回主线程,然后再主线程中处理消息。如果是通过主线程发送消息到子线程,然后在子线程处理消息,那又应该如何做呢?我们尝试直接在子线程中创建Handler对象,重写handlerMessage()方法,然后在主线程中发送消息,但程序运行后会报如下错误:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
意思是“不能够在一个没有执行过Looper.prepare()方法的线程中去创建Handler对象”,到这里我们终于能够看到Looper的影子了。那么这里就有一个问题,为什么在主线程中没有要求我们先调用Looper.prepare()方法呢,或者说与普通线程相比,主线程有什么特别的吗?
其实这是因为在主线程中,系统已经调用过该方法了,所以不需要我们再去手动调用。代码如下,主线程在刚开始创建的时候它的Looper就已经调用了prepareMianLooper()方法,所以我们能直接创建Handler。
public final class ActivityThread {
public static void main(String[] args) { ... Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread();
thread.attach(false); if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
} if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
} // End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
}
}
下面开始介绍消息机制中几个重要的类。
ThreadLocal
ThreadLocal类通过泛型可以存储任何类型的对象,它内部主要的方法为set()和get()方法。它通过set()方法存储对象,通过get()方法拿到存储的对象。ThreadLocal类的特点是它会以线程为作用域来存储数据,比如我们创建一个ThreadLocal对象来存储Integer类型数据,首先我们在主线程中给这个ThreadLocal对象赋值为10,在子线程1中给这个对象赋值为20,在子线程2中不进行赋值操作,然后分别打印ThreadLocal中存储的数据的值。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//在主线程中赋值为10
mThreadLocal.set();
//打印结果
Log.e("test","Thread*main mThreadLocal:"+mThreadLocal.get()); new Thread("Thread*1"){
@Override
public void run() {
super.run();
//在线程1中赋值为20
mThreadLocal.set();
//打印结果
Log.e("test","Thread*1 mThreadLocal:"+mThreadLocal.get());
}
}.start();
new Thread("Thread*2"){
@Override
public void run() {
super.run();
//在线程2中不进行赋值操作,打印结果
Log.e("test","Thread*2 mThreadLocal:"+mThreadLocal.get());
}
}.start();
}
最终得到的结果如下:
- ::42.422 -/com.weimore.demo1 E/test: Thread*main mThreadLocal:
- ::42.425 -/com.weimore.demo1 E/test: Thread* mThreadLocal:
- ::42.425 -/com.weimore.demo1 E/test: Thread* mThreadLocal:null
可见在不同的线程中,对同一个ThreadLoccal对象进行赋值,但最后的结果是不同的。ThreadLocal以线程为作用域来存储对象,保证了对象在不同线程中的数据独立性和唯一性。当然,要想真正了解ThreadLocal是如何做到分线程存储数据的,还是得看set()和get()方法的源码。
在set()方法中,首先会获得当前线程对象,然后调用getMap()方法,在getMap()方法中其实就是获取当前线程中的成员变量threadlocals,它是一个ThreadLocalMap对象,其实就是以Map的形式存储数据。如果该对象存在,则将数据存入,否则就创建该对象,然后再存入数据。所以通过set()方法,我们知道了,数据其实最终是保存在当前线程的一个成员变量threadlocals中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
而get()方法其实就是从获取当前所在线程的threadlocals对象,然后从中取出存储的数据。如果之前没有通过ThreadLocal存储过数据,则返回null。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
因此,我们知道了,原来通过ThreadLocal存储数据,最终其实存储在线程的成员变量中的,所以我们在不同的线程中取得的数据不是同一个对象,所以赋值过后,得到的结果会不同。
ThreadLocal类在消息机制中主要是负责存储Looper的,系统通过ThreadLocal来存储和获得Looper对象,从而保证每个线程的Looper是独立且唯一的。
MessageQuery
MessageQuery虽然叫做消息队列,但其实内部是以单链表的方式存储消息的。MessageQuery是在Looper初始化时被创建的,它负责和Looper,Handler一起协作来维持整个消息机制的运作。MessageQuery中重要的方法有两个:enqueueMessage(),next()。
enqueueMessage()方法的作用是插入一个消息到链表中,当Handler调用sendMessage()方法时会调用enqueueMessage()方法。而next()方法的作用就是循环地从链表中返回和移除消息并交由Handler去处理消息,该方法会Looper的loop()方法中被调用。
Handler
Handler的使用我们已经非常熟悉了,Handler中比较常用的方法就是post()或postDelayed()方法,还有sendMessage()和sendMessageDelayed()方法等,其实这四个方法最后都是调用sendMessageDelayed()方法,而sendMessageDelayed()方法又是调用sendMessageAtTime(方法。在sendMessageAtTime()中我们发现了如下代码:
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
可以看到这里调用了enqueueMessage()方法,并将mQueue对象传入其中,可以看到这个mQueue是一个MessgaeQuery对象,那么它是从何而来?难道Handler的内部也有一个MessagerQuery队列?我们查看Handler初始化的方法,发现这个mQueue是从当前线程的Looper中得到的。
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == ) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
} mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
那么Handler的enqueueMessage()方法到底做了什么呢?
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
我们看它的源码,发现在该方法中Handler将自己存储在了Message对象的成员变量target中,然后调用用了消息队列的enqueueMessage()方法将Message对象传入。前面已经说过,MessageQuery中的enqueueMessage()负责将消息存入消息队列中,而这里就是该方法具体被调用的地方。也就是说我们在使用Handler的sendMessage()方法时,其实是将Handler自身存入了消息中,然后再将消息存入了Looper中的消息队列中。
Handler中还有一个比较重要的方法就是dispatchMessage()方法。该方法会在Looper的方法中被调用,它的主要作用是将从消息队列中取出的消息发送到Handler的handlerMessage()方法中。如下:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
Looper
Looper算是消息机制的核心类,它负责创建消息队列,以及开启循环。当消息队列中有消息存在时,就将消息从消息队列中取出,并交由Handler处理。因此一个线程中如果想要使用Handler,就必须先创建Looperd对象,否则无法开启消息循环,更别说发送接收消息。又因为一个线程中不能存在多个消息队列,所以每个线程中Looper只允许被创建一次。而主线程中因为已经在内部创建过Looper对象了,所以可以直接使用Handler,如果在主线程再创建Looper实例的化,就会报Looper重复创建的异常。
Looper类中主要的方法有以下几个:prepare(),getMainLooper(),loop(),myLooper()。
其中getMainLooper()方法可以获取到主线程的Looper对象,myLooper()方法可以获取到当前线程的Looper对象,我们可以通过对比这两个方法得到的Looper对象是否为同一个对象,从而判断当前线程是否是主线程。
在Looper.prepare()方法中,主要负责创建Looper对象和创建消息队列。我们找到prepare()方法的源码:
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));
}
可以看到,首先要判断当前线程是否已拥有Looper对象。这里的sThreadLocal其实就是ThreadLocal类的对象。如果发现ThreadLocal对象在当前线程中已经存储过Looper了,说明Looper被重复创建了,则抛出“only one Looper may be created per thread”异常,如果ThreadLocal中还未存储Looper,则创建Looper,并存储到ThreadLocal中。
我们再接着看Looper的构造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper的构造方法中会创建一个MessageQuery(消息队列),并将当前线程赋值给mThread变量。这里我们可以知道消息队列确实是由Looper创建的,如果不创建Looper,消息队列就不存在。
我们接着看Looper的最后一个也是最重要的方法loop()。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(); 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 long traceTag = me.mTraceTag;
if (traceTag != && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != ) {
Trace.traceEnd(traceTag);
}
} 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();
}
}
可以看到在loop()方法中存在一个无限for循环,它会不断的调用MessageQuery的next()方法,从消息队列中取出消息,而在下面还调用了msg.target的diapatchMessage()方法用来处理取出的消息。前面分析Handler时说到过,这个msg.target其实就是Handler自身,所以其实这里就是调用Handler的dispatchMessage()方法。上面也说了Handler的despatchMessage()方法最终会将消息发送到handlerMessage()中去处理。这里要注意的是,我们用Handler发送消息时可能是在别的线程中发送的,但最后消息是在Looper的loop()方法中被取出然后处理的,最后的handlerMessage()方法一定是在Looper所在的线程中被执行的。
由Handler引发的内存泄漏
我们先来看一下平常我们在Activity中创建Handler可能会造成内存泄漏的写法:
public class MainActivity extends AppCompatActivity { private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//发送延迟消息
handler.sendEmptyMessageDelayed(, * );
} }
像上面这种情况就非常由可能会造成内存泄漏。我们可以看到,上面在Activity中创建Handler,其实是使用了匿名内部类的方法创建的Handler对象。在Java中,非静态内部类(包括匿名内部类)是会持有外部类的引用的,所以这时的Handler就持有了Activity的引用。
根据前面我们了解的Android的消息机制,明白了在主线程中创建Handler后,Handler发送的消息会被发送到主线程的Looper所创建的消息队列中,这时主线程的MessageQuery就持有了Message的引用,另外由于Message中的target变量其实就是Handler自身,所以Message其实也持有了Handler的引用。这样就导致了也许Handler正在发送或处理延迟消息时,Activity被销毁了,但Activity的引用被Handler所持有,所以造成了内存泄漏。
ActivityThread -> Looper -> MessageQuery -> Message -> Handler ->Activity
解决这种内存泄漏的方法有几种:
首先最简单的,就是将 Handler 声明为静态内部类,这样Handler就不会持有Activity的引用了,当然如果你确实需要Activity的引用来处理某些东西,那就使用WeakReference,让Handler持有Activity的弱引用,这样也不会造成内存泄漏。还有一种方法就是在Activity被销毁的时候调用Handler.removeCallbacksAndMessages()方法,移除消息队列中的所有消息和回调,这样就不会使Handler被MessageQuery持有引用,也就不会造成内存泄漏了。
Android之Handler消息处理机制的更多相关文章
- Android应用开发学习笔记之多线程与Handler消息处理机制
作者:刘昊昱 博客:http://blog.csdn.net/liuhaoyutz 和JAVA一样,Android下我们可以通过创建一个Thread对象实现多线程.Thread类有多个构造函数,一般通 ...
- 从Handler+Message+Looper源代码带你分析Android系统的消息处理机制
PS一句:不得不说CSDN同步做的非常烂.还得我花了近1个小时恢复这篇博客. 引言 [转载请注明出处:http://blog.csdn.net/feiduclear_up CSDN 废墟的树] 作为A ...
- Android多线程----异步消息处理机制之Handler详解
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...
- Android应用程序消息处理机制(Looper、Handler)分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6817933 Android应用程序是通过消息来 ...
- Android应用程序消息处理机制笔记
看老罗的Android源码情景分析学习的时候,边抄边理解再总结.希望能为面试提供点帮助吧. 1.Android应用程序是通过消息来驱动,Android应用程序每一个线程在启动时,都可以首先在内部创建一 ...
- Handler消息处理机制详解
之前一直只知道handler如何使用,不知道其中的工作原理,趁着新版本提测阶段比较空闲,及时做一个总结. 先看一下Google官方文档关于handler的解释: A Handler allows yo ...
- 详解 Handler 消息处理机制(附自整理超全 Q&A)
Android 为什么要用消息处理机制 如果有多个线程更新 UI,并且没有加锁处理,会导致界面更新的错乱,而如果每个更新操作都进行加锁处理,那么必然会造成性能的下降.所以在 Android 开发中,为 ...
- android学习-异步消息处理机制
消息处理机制主要对象:Looper,Handler,Message(还有MessageQueue和Runnable) Looper不断从MessageQueue消息队列中取出一个Message,然后传 ...
- 【Android】Handler消息机制
Handler消息机制主要涉及Looper.Handler.MessageQueue.Message.其中,Looper主要负责获取消息,Handler负责发送消息及处理消息,MessageQueue ...
随机推荐
- 「JSOI2014」矩形并
「JSOI2014」矩形并 传送门 我们首先考虑怎么算这个期望比较好. 我们不难发现每一个矩形要和 \(n - 1\) 个矩形去交,而总共又有 \(n\) 个矩形,所以我们把矩形两两之间的交全部加起来 ...
- mac 终端连接服务器报错
今天在连接虚拟机服务器时突然报了一个 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!的错误.  会出现这个错误的原因是在第一次进行SSH连接时,会生 ...
- 为小学生出四则运算题目.java
import java.util.Scanner; import java.util.Random; public class test{ public static int s1 = new Ran ...
- Python 使用pillow 操作图像
文档:https://pillow.readthedocs.io/en/stable/index.html 计算机图像基础 颜色和RGBA值 计算机程序通常将图像中的颜色表示为 RGBA 值.RGBA ...
- kali安装vm tools正确操作
参考博文:https://blog.csdn.net/qq_39536876/article/details/79501471 前言:每次在执行完 ./vmware-install.pl 重启后,总是 ...
- JSTL fn:replace()函数替换 换行符
转自:http://blog.163.com/chenjie_8392/blog/static/439339842010513128139/ 近日在使用textarea时,输入了回车,为了将texta ...
- Day11 - M - Nim or not Nim? HDU - 3032
Nim is a two-player mathematic game of strategy in which players take turns removing objects from di ...
- Day11-G - Calendar Game HDU - 1079
Adam and Eve enter this year’s ACM International Collegiate Programming Contest. Last night, they pl ...
- Batch批量替换hosts
hosts文件替换 工作需要,要修改很多计算机的hosts文件,采用bat批量完成 解决的问题: 1.pc工作在非管理员权限,右键管理员权限太麻烦,因此采用执行中申请管理员权限的方式 2.hosts和 ...
- 夯实Java基础(十六)——枚举类的使用
1.枚举类简介 枚举是仅容许特定数据类型值的有限集合.例如我们平时生活中星期一到星期日这七天就是一个特定的有限集合,一年四季的春夏秋冬也同样是的,它们都是枚举.枚举和我们数学中的集合非常相似,如果我们 ...