消息机制 Handler
对handler机制的基本作用、用法、时序流程进行介绍,针对handler机制中的内存泄漏问题讲解:一篇读懂Android Handler机制
Android-Handler机制详解
全面解析 | Android之Handler机制
需要掌握的:
-
handler机制的核心目的,基本类,基本用法,基本时序流程?
-
handler机制中容易出的内存泄漏问题是什么?怎么解决?
原因:
(1)Java中非静态内部类和匿名内部类都会隐式持有当前类的外部引用
(2)我们在Activity中使用非静态内部类初始化了一个Handler, 此Handler就会持有当前Activity的引用。
(3)我们想要一个对象被回收,那么前提它不被任何其它对象持有引用,所以当我们Activity页面关闭之后,存在引用关系:“未被处理 / 正处理的消息 -> Handler实例 -> 外部类”,如果在Handler消息队列还有未处理的消息 / 正在处理消息时导致Activity不会被回收,从而造成内存泄漏
解决方案:
(1)将Handler的子类设置成静态内部类,使用WeakReference弱引用持有Activity实例
(2)当外部类结束生命周期时,清空Handler内消息队列 -
messageQueue的真实数据结构是什么?不同的send一条数据的方法对应的数据结构操作的区别是啥?
是什么
Handler机制是Android中基于单线消息队列模式的一套线程消息机制。
安卓中用于实现线程间通信的消息机制。
为什么要使用Hander?
- 切换代码执行的线程
android限制了不能在非UI创建线程去操作UI,同时不能在主线程执行耗时任务,所以我们一般是在子线程执行网络请求等耗时操作请求数据,然后再切换到主线程来更新UI。这个时候就必须用到Handler来切换线程了。 - 按顺序规则地处理消息,避免并发
基于单线消息队列模型,顺序执行不同线程发送过来的不同消息,避免了并发处理可能带来的冲突问题。 - 阻塞线程,避免让线程结束
Android需要一直等待用户的操作再去刷新UI,而Handler机制就解决了这个问题,但消息队列中没有任务的时候,他就会把线程阻塞,等到有新的任务的时候,再重新启动处理消息。 - 延迟处理消息
如何使用Handler
-
创建Looper
主线程:Looper.prepareMainLooper()在应用启动流程中就已经准备完成了,不需要手动准备;
子线程:Looper.prepare() -
使用Looper创建Handler
如果使用内部类继承Handler的方式创建Handler的话,注意要将内部类声明为静态内部类,否则会有内存泄漏问题。
原因是非静态内部类会持有外部类的引用,而Handler发出的Message会持有Handler的引用。如果这个Message是个延迟的消息,此时activity被退出了,但Message依然在“流水线”上,Message->handler->activity,那么activity就无法被回收,导致内存泄露。 -
启动Looper
Looper.loop(); -
使用Handler发送信息
handler.sendMessage(msg);
handler.sendMessageDelayed(msg,delayTime);
handler.post(runnable);
handler.postDelayed(runnable,delayTime);
消息处理总体流程
- 用户使用线程的Looper构建Handler之后,通过Handler的send和post方法发送消息
- 消息会加入到MessageQueue中,等待Looper获取处理
- Looper会不断地从MessageQueue中获取Message然后交付给对应的Handler处理
Handler机制关键类
ThreadLocal
ThreadLocal是Java中一个用于线程内部存储数据的工具类。ThreadLocal是用来存储数据的,但是每个线程只能访问到各自线程的数据。不同线程之间访问到的数据不一样。
public static void main(String[] args){
ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
System.out.println(stringLocal.get());
new Thread(){
System.out.println(stringLocal.get());
}
}
结果:
java
null
ThreadLocal的作用
ThreadLocal的特性适用于同样的数据类型,不同的线程有不同的备份情况,如我们这篇文章一直在讲的Looper。每个线程都有一个对象,但是不同线程的Looper是不一样的,这个时候就特别适合使用ThreadLocal来存储数据,这也是为什么这里要讲ThreadLocal的原因。
ThreadLocal内部代码分析
见原帖
ThreadLocal总结
ThreadLocal适合用于在不同线程作用域的数据备份。
ThreadLocal机制通过在每个线程维护一个ThreadLocalMap,其key为ThreadLocal,value为ThreadLocal对应的泛型对象,这样每个ThreadLocal就可以作为key将不同的value存储在不同Thread的Map中,当获取数据的时候,同个ThreadLocal就可以在不同线程的Map中得到不同的数据。
ThreadLocalMap类似于一个改版的HashMap,内部也是使用数组和Hash算法来存储数据,使得存储和读取的速度非常快。
同时使用ThreadLocal需要注意内存泄露问题,当ThreadLocal不再使用的时候,需要通过remove方法把value移除。
Message消息
消息体属性
Message是负责承载消息的类,主要是关注他的内部属性:
// 用户自定义,主要用于辨别Message的类型
public int what;
// 用于存储一些整型数据
public int arg1;
public int arg2;
// 可放入一个可序列化对象
public Object obj;
// Bundle数据
Bundle data;
// Message处理的时间。相对于1970.1.1而言的时间
// 对用户不可见
public long when;
// 处理这个Message的Handler
// 对用户不可见
Handler target;
// 当我们使用Handler的post方法时候就是把runnable对象封装成Message
// 对用户不可见
Runnable callback;
// MessageQueue是一个链表,next表示下一个
// 对用户不可见
Message next;
循环利用Message
当我们获取Message的时候,官方建议是通过Message.obtain()方法来获取,当使用完之后使用recycle()方法来回收循环利用。而不是直接new一个新的对象:
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
Message维护了一个静态链表,链表头是sPool,Message有一个next属性,Message本身就是链表结构。sPoolSync是一个object对象,仅作为解决并发访问安全设计。当我们调用obtain来获取一个新的Message的时候,首先会检查链表中是否有空闲的Message,如果没有则新建一个返回。
Message的回收
当我们使用完成之后,可以调用Message的recycle方法进行回收:
public void recycle() {
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
recycleUnchecked();
}
如果这个Message正在使用则会抛出异常,否则则调用recycleUnchecked进行回收:
void recycleUnchecked() {
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
这个方法的逻辑也非常简单,把Message中的内容清空,然后判断链表是否达到最大值(50),然后插入链表中。
Message总结
Message的作用就是承载消息,他的内部有很多的属性用于给用户赋值。同时Message本身也是一个链表结构,无论是在MessageQueue还是在Message内部的回收机制,都是使用这个结构来形成链表。同时官方建议不要直接初始化Message,而是通过Message.obtain()方法来获取一个Message循环利用。一般来说我们不需要去调用recycle进行回收,在Looper中会自动把Message进行回收,后面会讲到。
MessageQueue消息队列
概述
每个线程都有且只有一个MessageQueue,他是一个用于承载消息的队列,内部使用链表作为数据结构,所以待处理的消息都会在这里排队。
MessageQueue还涉及到一个关键概念:线程休眠。当MessageQueue中没有消息或者都在等待中,则会将线程休眠,让出cpu资源,提高cpu的利用效率。进入休眠后,如果需要继续执行代码则需要将线程唤醒。当方法暂时无法直接返回需要等待的时候,则可以将线程阻塞,即休眠,等待被唤醒继续执行逻辑。
消息出队
Message next() {
// 如果looper已经退出了,这里就返回null
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
...
// 阻塞时间
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 阻塞对应时间
nativePollOnce(ptr, nextPollTimeoutMillis);
// 对MessageQueue进行加锁,保证线程安全
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
...
if (msg != null) {
if (now < msg.when) {
// 下一个消息还没开始,等待两者的时间差
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获得消息且现在要执行,标记MessageQueue为非阻塞
mBlocked = false;
// 链表操作
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// 没有消息,进入阻塞状态
nextPollTimeoutMillis = -1;
}
...
}
}
next方法目的是获取MessageQueue中的一个Message,如果队列中没有消息的话,就会把方法阻塞住,等待新的消息来唤醒。主要步骤如下:
如果Looper已经退出了,直接返回null
进入死循环,直到获取到Message或者退出
循环中先判断是否需要进行阻塞,阻塞结束后,对MessageQueue进行加锁,获取Message
如果MessageQueue中没有消息,则直接把线程无限阻塞等待唤醒;
如果MessageQueue中有消息,则判断是否需要等待,否则则直接返回对应的message。
可以看到逻辑就是判断当前时间Message中是否需要等待。其中nextPollTimeoutMillis表示阻塞的时间,-1表示无限时间,只有通过唤醒才能打破阻塞。
消息入队
MessageQueue.class
boolean enqueueMessage(Message msg, long when) {
// Hanlder不允许为空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
// 对MessageQueue进行加锁
synchronized (this) {
// 判断目标thread是否已经死亡
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
// 标记Message正在被执行,以及需要被执行的时间,这里的when是距离1970.1.1的时间
msg.markInUse();
msg.when = when;
// p是MessageQueue的链表头
Message p = mMessages;
boolean needWake;
// 判断是否需要唤醒MessageQueue
// 如果有新的队头,同时MessageQueue处于阻塞状态则需要唤醒队列
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
...
// 根据时间找到插入的位置
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
...
}
msg.next = p;
prev.next = msg;
}
// 如果需要则唤醒队列
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
首先判断message的目标handler不能为空且不能正在使用中;
对MessageQueue进行加锁;
判断目标线程是否已经死亡,死亡则直接返回false;
初始化Message的执行时间以及标记正在执行中;
然后根据Message的执行时间,找到在链表中的插入位置进行插入;
同时判断是否需要唤醒MessageQueue。有两种情况需要唤醒:当新插入的Message在链表头时,如果messageQueue是空的或者正在等待下个任务的延迟时间执行,这个时候就需要唤醒MessageQueue。
Looper
概述
Looper可以说是Handler机制中的一个非常重要的核心。Looper相当于线程消息机制的引擎,驱动整个消息机制运行。Looper负责从队列中取出消息,然后交给对应的Handler去处理。如果队列中没有消息,则MessageQueue的next方法会阻塞线程,等待新的消息的到来。每个线程有且只能有一个“引擎”,也就是Looper,如果没有Looper,那么消息机制就运行不起来,而如果有多个Looper,则会违背单线操作的概念,造成并发操作。
每个线程仅有一个Looper,由不同Looper分发的Message运行在不同的线程中。Looper的内部维护一个MessageQueue,当初始化Looper的时候会顺带初始化MessageQueue。
Looper使用ThreadLocal来保证每个线程都有且只有一个相同的副本。
prepare()初始化Looper
Looper.class
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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));
}
每个线程使用Handler之前,都必须调用Looper.prepare()方法来初始化当前线程的Looper。参数quitAllowed表示该Looper是否可以退出。主线程的Looper是不能退出的,不然程序就直接终止了。我们在主线程使用Handler的时候是不用初始化Looper的,为什么?因为Activiy在启动的时候就已经帮我们初始化主线程Looper了,这点在后面再详细展开。所以在主线程我们可以直接调用Looper.myLooper()获取当前线程的Looper。
prepare方法重点在sThreadLocal.set(new Looper(quitAllowed));,可以看出来这里使用了ThreadLocal来创建当前线程的Looper对象副本。如果当前线程已经有Looper了,则会抛出异常。sThreadLocal是Looper类的静态变量,前面我们介绍过了ThreadLocal了,这里每个线程调用一次prepare方法就可以初始化当前线程的Looper了。
接下来再看到Looper的构造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
逻辑很简单,初始化了一个MessageQueue,再把当前的线程的Thread对象赋值给mThread。
myLooper()获取本线程的Looper
获取当前线程的Looper对象。这个方法就是直接调用ThreadLocal的get方法:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
loop()循环获取消息
当Looper初始化完成之后,他是不会自己启动的,需要我们自己去启动Looper,调用Looper的loop()方法即可:
public static void loop() {
// 获取当前线程的Looper
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;
...
for (;;) {
// 获取消息队列中的消息
Message msg = queue.next(); // might block
if (msg == null) {
// 返回null说明MessageQueue退出了
return;
}
...
try {
// 调用Message对应的Handler处理消息
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}
...
// 回收Message
msg.recycleUnchecked();
}
}
loop()方法就是Looper这个“引擎”的核心所在。首先获取当前线程的Looper对象,没有则抛出异常。然后进入一个死循环:不断调用MessageQueue的next方法来获取消息,然后调用message的目标handler的dispatchMessage方法来处理Message。
前面我们了解过了MessageQueue,next方法是可能会进行阻塞的:当MessageQueue为空或者目前没有任何消息需要处理。所以Looper就会一直等待,阻塞在里,线程也就不会结束。当我们退出Looper的时候,next方法会返回null,那么Looper也就会跟着结束了。
同时,因为Looper是运行在不同线程的逻辑,其调用的dispatchMessage方法也是运行在不同的线程,这就达到了切换线程的目的。
quit/quitSafely 退出
quit是直接将Looper退出,quitSafely是将MessageQueue中的不需要等待的消息处理完成之后再退出,看一下代码:
public void quit() {
mQueue.quit(false);
}
// 最终都是调用到了这个方法
void quit(boolean safe) {
// 如果不能退出则抛出异常。这个值在初始化Looper的时候被赋值
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
// 退出一次之后就无法再次运行了
if (mQuitting) {
return;
}
mQuitting = true;
// 执行不同的方法
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// 唤醒MessageQueue
nativeWake(mPtr);
}
}
我们可以发现最后都调用了quitSafely方法。这个方法先判断是否能退出,然后再执行退出逻辑。如果mQuitting==true,那么这里会直接方法,我们会发现mQuitting这个变量只有在这里被执行了赋值,所以一旦looper退出,则无法再次运行了。之后执行不同的退出逻辑,我们分别看一下:
private void removeAllMessagesLocked() {
Message p = mMessages;
while (p != null) {
Message n = p.next;
p.recycleUnchecked();
p = n;
}
mMessages = null;
}
private void removeAllFutureMessagesLocked() {
final long now = SystemClock.uptimeMillis();
Message p = mMessages;
if (p != null) {
// 如果都在等待,则全部移除,直接退出
if (p.when > now) {
removeAllMessagesLocked();
} else {
Message n;
// 把需要等待的Message全部移除
for (;;) {
n = p.next;
if (n == null) {
return;
}
if (n.when > now) {
break;
}
p = n;
}
p.next = null;
do {
p = n;
n = p.next;
p.recycleUnchecked();
} while (n != null);
}
}
}
Looper总结
Looper作为Handler消息机制的“动力引擎”,不断从MessageQueue中获取消息,然后交给Handler去处理。Looper的使用前需要先初始化当前线程的Looper对象,再调用loop方法来启动它。
同时Handler也是实现切换的核心,因为不同的Looper运行在不同的线程,他所调用的dispatchMessage方法则运行在不同的线程,所以Message的处理就被切换到Looper所在的线程了。当looper不再使用时,可调用不同的退出方法来退出他,注意Looper一旦退出,线程则会直接结束。
Handler
创建Handler
一般情况下我们使用Handler有两种方式: 继承Handler并重写handleMessage方法,直接创建Handler对象并传入callBack。
创建Handler必须显示指明Looper参数,而不能直接使用无参构造函数。
Handler handler = new Handler(Looper.myLooper());
发送消息
public final boolean post(@NonNull Runnable r);
public final boolean postDelayed(@NonNull Runnable r, long delayMillis);
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis);
public final boolean postAtFrontOfQueue(@NonNull Runnable r);
public final boolean sendMessage(@NonNull Message msg);
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis);
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis);
public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg)
其中postxxx()方法会将runnable封装到Message消息体的callback字段中,最终同sendMessagexxx()方法一样,都是调用sendMessageDelayed()方法,进而调用sendMessageAtTime()方法。
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull 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);
}
sendMessageDelayed把小于0的延迟时间改成0,然后调用sendMessageAtTime。这个方法主要是判断MessageQueue是否已经初始化了,然后再调用enqueueMessage方法进行入队操作:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
// 这里把target设置成自己
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
// 异步handler设置标志位true,后面会讲到同步屏障
if (mAsynchronous) {
msg.setAsynchronous(true);
}
// 最后调用MessageQueue的方法入队
return queue.enqueueMessage(msg, uptimeMillis);
}
可以看到Handler的入队操作也是很简单,把Message的target设置成本身,这样这个Message最后就是由自己来处理。最后调用MessageQueue的入队方法来入队,这在前面讲过就不再赘述。
处理消息
上面讲Looper处理消息的时候,最后就是调用handler的dispatchMessage方法来处理。
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
他的逻辑也不复杂。首先判断Message是否有callBack,有的话就直接执行callBack的逻辑,这个callBack就是我们调用handler的post系列方法传进去的Runnable对象。否则判断Handler是否有callBack,有的话执行他的方法,如果返回true则结束,如果返回false则直接Handler本身的handleMessage方法。
内存泄漏问题
当我们使用继承Handler方法来使用Handler的时候,要注意使用静态内部类,而不要用非静态内部类。因为非静态内部类会持有外部类的引用,而从上面的分析我们知道Message在被入队之后他的target属性是指向了Handler,如果这个Message是一个延迟的消息,那么这一条引用链的对象就迟迟无法被释放,造成内存泄露。
一般这种泄露现象在于:我们在Activity中发送了一个延迟消息,然后退出了activity,但是由于无法释放,这样activity就无法被回收,造成内存泄露。
Handler总结
Handler作为消息的处理和发送者,是整个消息机制的起点和终点,也是我们接触最多的一个类,因为我们称此消息机制为Handler机制。Handler最重要的就是发送和处理消息,只要熟练掌握这两方面的内容就可以了。同时注意内存泄露问题,不要使用非静态内部类去继承Handler。
HandlerThread
为什么要使用HandlerThread?
解决子线程中Looper还没完成初始化,外部就调用该线程Handler的sendMessage方法导致程序报错问题。
源码
public class HandlerThread extends Thread {
// 依次是:线程优先级、线程id、线程looper、以及内部handler
int mPriority;
int mTid = -1;
Looper mLooper;
private @Nullable Handler mHandler;
// 两个构造器。name是线程名字,priority是线程优先级
public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
}
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}
// 在Looper开始运行前的方法
protected void onLooperPrepared() {
}
// 初始化Looper
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
// 通知初始化完成
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
// 获取当前线程的Looper
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// 如果尚未初始化则会一直阻塞知道初始化完成
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
// 利用Object对象的wait方法
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
// 获取handler,该方法被标记为hide,用户无法获取
@NonNull
public Handler getThreadHandler() {
if (mHandler == null) {
mHandler = new Handler(getLooper());
}
return mHandler;
}
// 两种不同类型的退出,前面讲过不再赘述
public boolean quit() {
Looper looper = getLooper();
if (looper != null) {
looper.quit();
return true;
}
return false;
}
public boolean quitSafely() {
Looper looper = getLooper();
if (looper != null) {
looper.quitSafely();
return true;
}
return false;
}
// 获取线程id
public int getThreadId() {
return mTid;
}
}
HandlerThread使用
HandlerThread的使用范围很有限,开个子线程不断接受消息处理耗时任务。所以他的使用方法也是比较固定:
HandlerThread ht = new HandlerThread("handler");
Handler handler = new Hander(ht.getLooper());
handler.sendMessage(msg);
获取到他的Looper,外部自定义Handler来使用即可。
常见问题
主线程为什么不需要初始化Looper?
因为应用在启动的过程中就已经初始化主线程Looper了。
每个java应用程序都是有一个main方法入口,Android是基于Java的程序也不例外。Android程序的入口在ActivityThread的main方法中:
public static void main(String[] args) {
...
// 初始化主线程Looper
Looper.prepareMainLooper();
...
// 新建一个ActivityThread对象
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
// 获取ActivityThread的Handler,也是他的内部类H
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
Looper.loop();
// 如果loop方法结束则抛出异常,程序结束
throw new RuntimeException("Main thread loop unexpectedly exited");
}
为什么主线程的Looper是一个死循环,但是却不会ANR?
因为当Looper处理完所有消息的时候会进入阻塞状态,当有新的Message进来的时候会打破阻塞继续执行。
这其实没理解好ANR这个概念。ANR,全名Application Not Responding。当我发送一个绘制UI 的消息到主线程Handler之后,经过一定的时间没有被执行,则抛出ANR异常。Looper的死循环,是循环执行各种事务,包括UI绘制事务。Looper死循环说明线程没有死亡,如果Looper停止循环,线程则结束退出了。Looper的死循环本身就是保证UI绘制任务可以被执行的原因之一。同时UI绘制任务有同步屏障,可以更加快速地保证绘制更快执行。
Handler如何保证MessageQueue并发访问安全?
循环加锁,配合阻塞唤醒机制。
我们可以发现MessageQueue其实是“生产者-消费者”模型,Handler不断地放入消息,Looper不断地取出,这就涉及到死锁问题。如果Looper拿到锁,但是队列中没有消息,就会一直等待,而Handler需要把消息放进去,锁却被Looper拿着无法入队,这就造成了死锁。Handler机制的解决方法是循环加锁。在MessageQueue的next方法中:
Message next() {
...
for (;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
...
}
}
}
我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。
那在入队的时候会不会因为队列已经满了然后一边在等待消息处理一边拿着锁呢?这一点不同的是MessageQueue的消息没有上限,或者说他的上限就是JVM给程序分配的内存,如果超出内存会抛出异常,但一般情况下是不会的。
Looper退出后是否可以重新运行?
不可以。
线程的存活是靠Looper调用的next方法进行阻塞实现的。如果Looper退出后,那么线程会马上结束,也不会再有第二次运行的机会了。即使线程还没结束再一次调用loop(),Looper内部有一个mQuitting变量,当他被赋值为false之后就无法再被赋值为true。所以就无法再重新运行了。
Handler的阻塞唤醒机制是怎么回事?
Handler的阻塞唤醒机制是基于Linux的阻塞唤醒机制。
这个机制也是类似于handler机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。和Looper监听MessageQueue,Handler添加message是比较类似的。
能不能让一个Message加急被处理?/ 什么是Handler同步屏障?
看原帖
什么是IdleHandler?
看原帖
面试问题
Handler
Handler的实现原理?
Handler 的工作原理基于 Looper 和 MessageQueue。当一个消息(Message)被 Handler 发送时,Handler 会将这个消息放入与当前线程关联的 MessageQueue 中。Looper 是一个无限循环,它会不断从 MessageQueue 中取出消息并分发给相应的 Handler 进行处理。
子线程中能不能直接new一个Handler,为什么主线程可以?
不能,因为Handler 的构造方法中,会通过Looper.myLooper()获取looper对象,如果为空,则抛出异常,主线程则因为已在入口处ActivityThread的main方法中通过 Looper.prepareMainLooper()获取到这个对象,并通过 Looper.loop()开启循环,在子线程中若要使用handler,可先通过Loop.prepare获取到looper对象,并使用Looper.loop()开启循环。
Handler postDealy后消息队列有什么变化?进队延时更短的消息,消息队列怎样处理 队内消息?
postDelayed传入的时间,会和当前的时间SystemClock.uptimeMillis()做加和,而不是单纯的只是用延时时间。延时消息会和当前消息队列里的消息头的执行时间做对比,如果比头的时间靠前,则会做为新的消息头,不然则会从消息头开始向后遍历,找到合适的位置插入延时消息。 postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper; Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;直到阻塞时间到或者下一次有Message进队;
Handler导致的内存泄露原因及其解决方案?
见内容介绍部分。
Handler 有哪些发送消息的方法?
见内容介绍部分。
Handler的post与sendMessage的区别和应用场景?
区别在于post实现起来比较简单。而sendMessage()则需要自己重写handleMessage()方法。
两者在本质上都没有什么区别。post方法适合单一的场景,实现起来比较方便。sendMessage()适合需要作条件判断的场景。
post一类的方法发送是Runnable对象,但是最后还是会被封装成Message对象,将Runnable对象赋值给Message对象中的callback字段,然后交由sendMessageAtTime()方法 发送出去。在处理消息时,会在dispatchMessage()方法里首先被handleCallback(msg)方法执行,实际上就是执行Message对象里面的Runnable对象的run方法。
sendMessage一类方法发送的消息直接是Message对象,处理消息时,在dispatchMessage里优先级会低于handleCallback(msg)方法,是通过自己重写的handleMessage(msg)方法执行。
Handler发送延迟消息的原理是什么?
我们通过postDelayed()和sendMessageDelayed()方法来发送延迟消息,其实最终是通过方法中指定的延时时长 + 当前时间戳,计算出来该消息确定的分发时间,然后通过sendMessageAtTime()方法插入到MessageQueue中,等待到这个时间点之后才会分发处理该消息,所以延迟消息是立即发送,然后延后分发处理。
消息延迟的原理是,首先Looper在发送消息到MessageQueue的时候,会按照消息设定的分发时间先后排序放在链表中,然后通过nativePollOnce()方法让线程在休眠一段时间,等到第一个消息的处理时间到达的时候,唤醒线程处理消息,从而实现延迟消息。
如何在子线程中弹窗Toast?
在子线程中调用Looper.prepare()方法,并调用Looper.loop()方法,这样就会在子线程中创建一个Looper对象和MessageQueue消息队列,而loop()让当前子线程开始监听消息,这样我们在子线程中显示Toast的时候,UI绘制的消息才会发送到子线程的队列中,在消息分发的时候进行UI绘制。
但是需要注意的是,任务执行完毕之后,需要手动调用Looper.quitSafely()方法退出循环,否则子线程一直不会结束退出。
子线程能不能更新UI?为什么Android系统不建议子线程访问UI?
android 为什么子线程不能更新UI
android 子线程为什么不能更新UI
Android为什么不能在子线程更新UI
通过Handler如何实现线程的切换?
Handler 如何实现线程间切换
Handler是如何实现线程之间的切换的
Handler 如何与 Looper 关联的?
Handler和Looper是怎样绑定到一起的?
当Activity有多个Handler的时候,怎么样区分当前消息由哪个Handler处理?
根据消息的分发机制,Looper 不会区分 Handler,每个 Handler 会被添加到 Message 的 target 字段上面,Looper 通过调用 Message.target.handleMessage() 来让 Handler 处理消息。
ANR和Handler的联系?
ANR相关-Handler
Handler机制 与 ANR异常
Handler怎么做到的一个线程对应一个Looper,如何保证只有一个MessageQueue? ThreadLocal在Handler机制中的作用?
Handler面试那些事
HandlerThread是什么 & 好处 &原理 & 使用场景?
HandlerThread原理、使用实例、源码详细解析
IdleHandler及其使用场景?
Message
Message对象创建的方式有哪些 & 区别?
1.Message msg = new Message(); 每次需要Message对象的时候都创建一个新的对象,每次都要去堆内存开辟对象存储空间
2.Message msg = Message.obtain(); obtainMessage能避免重复 Message创建对象。它先判断消息池是不是为空,如果非空的话就从消息池表头的Message取走,再把表头指向 next。 如果消息池为空的话说明还没有Message被放进去,那么就new出来一个Message对象。消息池使用 Message 链表结构实现,消息池默认最大值 50。消息在loop中被handler分发消费之后会执行回收的操作,将该消息内部数据清空并添加到消息链表的表头。
3.Message msg = handler.obtainMessage(); 其内部也是调用的obtain() 方法。
Message.obtain()怎么维护消息池的?
如上
在构建Message对象的时候,为什么建议通过Message.obtain()方法获取Message对象?
obtain()方法可以从全局消息池中获得一个空的Message对象,这样可以有效的减少Message对象创建时消耗的系统资源。同时,通过obtain()的重载方法还可以获得一些Message的拷贝,或对Message对象进行一些初始化。
MessageQueue是什么数据结构?
链表。
MessageQueue
MessageQueue.next 在没有消息的时候会阻塞,如何恢复?
这里采用的是Linux的epoll机制,通过监控文件描述符eventfd,当消息队列MessageQueue中有可执行消息的时候,同时会向eventfd中写入数据,从而唤醒主线程进行消息的分发处理。
MessageQueue的enqueueMessage()方法如何进行线程同步的?
boolean enqueueMessage(Message msg, long when) {
//1. 内部是单链表的插入操作
synchronized (this) {
......
}
return true;
}
1)单链表的插入操作
2)如果消息队列被阻塞回调用nativeWake去唤醒。
3)用synchronized代码块去进行同步。
MessageQueue的next()方法内部原理?
/**
* 功能:读取并且删除数据
* 内部无限循环,如果消息队列中没有消息就会一直阻塞。
* 一旦有新消息到来,next方法就会返回该消息并且将其从单链表中移除
*/
Message next() {
int nextPollTimeoutMillis = 0;
for (;;) {
/**======================================================================
* 1、精确阻塞指定时间。第一次进入时因为nextPollTimeoutMillis=0,因此不会阻塞。
* 1-如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
* 2-如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
* 3-如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时),如果期间有程序唤醒会立即返回。
*====================================================================*/
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 当前时间
final long now = SystemClock.uptimeMillis();
Message msg = mMessages;
/**=======================================================================
* 2、当前Msg为消息屏障
* 1-说明有重要的异步消息需要优先处理
* 2-遍历查找到异步消息并且返回。
* 3-如果没查询到异步消息,会continue,且阻塞在nativePollOnce直到有新消息
*====================================================================*/
if (msg != null && msg.target == null) {
// 遍历寻找到异步消息,或者末尾都没找到异步消息。
do {
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
/**================================================================
* 3、获取到消息
* 1-消息时间已到,返回该消息。
* 2-消息时间没到,表明有个延时消息,会修正nextPollTimeoutMillis。
* 3-后面continue,精确阻塞在nativePollOnce方法
*===================================================================*/
if (msg != null) {
// 延迟消息的时间还没到,因此重新计算nativePollOnce需要阻塞的时间
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 返回获取到的消息(可以为一般消息、时间到的延迟消息、异步消息)
return msg;
}
} else {
/**=============================
* 4、没有找到消息或者异步消息
*==============================*/
nextPollTimeoutMillis = -1;
}
/**===========================================
* 5、没有获取到消息,进行下一次循环。
* (1)此时可能处于的情况:
* 1-没有获取到消息-nextPollTimeoutMillis = -1
* 2-没有获取到异步消息(接收到同步屏障却没找到异步消息)-nextPollTimeoutMillis = -1
* 3-延时消息的时间没到-nextPollTimeoutMillis = msg.when-now
* (2)根据nextPollTimeoutMillis的数值,最终都会阻塞在nativePollOnce(-1),
* 直到enqueueMessage将消息添加到队列中。
*===========================================*/
if (pendingIdleHandlerCount <= 0) {
// 用于enqueueMessage进行精准唤醒
mBlocked = true;
continue;
}
}
}
}
小结(原理:分为三种情况进行处理):
1)如果是一般消息,会去获取消息,没有获取到就会阻塞(native方法),直到enqueueMessage插入新消息。获取到直接返回Msg。
2)如果是同步屏障,会去循环查找异步消息,没有获取到会进行阻塞。获取到直接返回Msg。
3)如果是延时消息,会计算时间间隔,并进行精准定时阻塞(native方法)。直到时间到达或者被enqueueMessage插入消息而唤醒。时间到后就返回Msg。
同步屏障是什么,有什么作用?
我们一般用到Handler消息是同步消息,其实Handler有三种消息:同步消息、异步消息和屏障消息SyncBarrier,屏障消息也会被插入到MessageQueue中。同步消息和异步消息的target在传入MessageQueue的时候会保证不为空,以便在消息分发的时候知道该消息应该分发给谁,而屏障消息的target是空的,这也是Handler中判断一个消息是否为屏障消息的标准。MessageQueue.next()方法中如果当前消息是屏障消息,则会跳过后面所有的同步消息,找到屏障消息后第一个异步消息进行分发处理。
需要注意的是开发者没有办法直接向MessageQueue中插入消息屏障(当然通过反射机制是可以的)。
在View绘制的时候,View绘制的Message是优先于其他的消息,在有View绘制消息的时候,系统会同时向MessageQueue中插入一个消息屏障,从而使该消息优先处理,避免耗时的Message阻塞UI的绘制。
Looper
一个线程可以有几个Handler,几个Looper,几个MessageQueue对象?
一个线程可以有多个Handler,只有一个Looper对象,只有一个MessageQueue对象。Looper.prepare()函数中知道,。在Looper的prepare方法中创建了Looper对象,并放入到ThreadLocal中,并通过ThreadLocal来获取looper的对象, ThreadLocal的内部维护了一个ThreadLocalMap类,ThreadLocalMap是以当前thread做为key的,因此可以得知,一个线程最多只能有一个Looper对象,在Looper的构造方法中创建了MessageQueue对象,并赋值给mQueue字段。因为Looper对象只有一个,那么Messagequeue对象肯定只有一个。
主线程的Looper第一次调用loop方法,什么时候,哪个类?
主线程的 Looper 在 Android 应用程序启动时由 ActivityThread.main()方法创建,并调用 Looper.loop() 方法开始运行。
Looper.prepareMainLooper(); //获取了主线程的Looper
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
Looper.loop(); //不断轮询消息
/*
真相只有一个,是的在创建主线程的时候Android已经帮我们调用了Looper.prepareMainLooper()
和Looper.loop()方法,所以我们在主线程能直接创建Handler使用。
*/
Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
https://docs.pingcode.com/ask/ask-ask/100334.html?p=100334
主线程是通过Looper.loop()方法进入了循环状态,因为这样主线程才不会像我们创建的一般线程那样,当可执行代码结束之后,线程的生命周期就结束退出了。
当主线程的MessageQueue中没有消息的时候,会阻塞在Messagequeue.next()方法中的nativePollOnce()方法中,此时主线程会释放CPU资源进入休眠状态,直到新消息到来的时候。所以主线程大部分时间是处于休眠状态的,并不会消耗大量的CPU资源。
这里采用的是Linux的epoll机制,通过监控文件描述符eventfd,当消息队列MessageQueue中有可执行消息的时候,同时会向eventfd中写入数据,从而唤醒主线程进行消息的分发处理。
Looper.quit/quitSafely的区别?
见上面介绍内容。
quit() 和 quitSafely() 的本质就是让消息队列的 next() 返回 null,以此来退出Looper.loop()。
quit() 调用后直接终止 Looper,不在处理任何 Message,所有尝试把 Message 放进消息队列的操作都会失败,比如 Handler.sendMessage() 会返回 false,但是存在不安全性,因为有可能有 Message 还在消息队列中没来的及处理就终止 Looper 了。
quitSafely() 调用后会在所有消息都处理后再终止 Looper,所有尝试把 Message 放进消息队列的操作也都会失败。
Handler消息机制中,一个looper是如何区分多个Handler的?
根据消息的分发机制,Looper 不会区分 Handler,每个 Handler 会被添加到 Message 的 target 字段上面,Looper 通过调用 Message.target.handleMessage() 来让 Handler 处理消息。
Looper 如何与 Thread 关联的?
并发编程——ThreadLocal原理分析,Looper如何和线程关联
Looper.loop()源码?
- 获取到 Looper 和消息队列;
- for 无限循环,阻塞于消息队列的 next() 方法;
- 取出消息后调用 msg.target.dispatchMessage(msg) 进行消息分发。
Looper和Handler是否一定处于一个线程?子线程中是否可以用MainLooper去创建Handler?
Looper 和 Handler 不需要再一个线程中,默认的情况下会从 ThreadLocal 中取当前线程对应的 Looper,但我们可以通过显式地指定一个 Looper 的方式来创建 Handler. 比如,当我们想要在子线程中发送消息到主线程中,那么我们可以:
Handler handler = new Handler(Looper.getMainLooper());