Android面试:Handler八大问题汇总
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android面试:Handler八大问题汇总相关的知识,希望对你有一定的参考价值。
前言
Handler
机制几乎是android
面试时必问的问题,虽然看过很多次handler
源码,但是有些面试官问的问题却不一定能够回答出来,趁着机会总结一下面试中所覆盖的handler
知识点。
1、讲讲 Handler 的底层实现原理?
下面的这幅图很完整的表现了整个handler
机制。
要理解handler的实现原理,其实最重要的是理解Looper
的实现原理,Looper
才是实现handler
机制的核心。任何一个handler
在使用sendMessage
或者post
时候,都是先构造一个Message
,并把自己放到message中
,然后把Message
放到对应的Looper
的MessageQueue
,Looper
通过控制MessageQueue
来获取message
执行其中的handler
或者runnable
。 要在当前线程中执行handler
指定操作,必须要先看当前线程中有没有looper
,如果有looper
,handler
就会通过sendMessage
,或者post
先构造一个message
,然后把message
放到当前线程的looper
中,looper
会在当前线程中循环取出message
执行,如果没有looper
,就要通过looper.prepare()
方法在当前线程中构建一个looper
,然后主动执行looper.loop()
来实现循环。
梳理一下其实最简单的就下面四条:
1、每一个线程中最多只有一个Looper
,通过ThreadLocal
来保存,Looper
中有Message
队列,保存handler
并且执行handler
发送的message
。
2、在线程中通过Looper.prepare()
来创建Looper
,并且通过ThreadLocal
来保存Looper
,每一个线程中只能调用一次Looper.prepare()
,也就是说一个线程中最多只有一个Looper
,这样可以保证线程中Looper
的唯一性。
3、handler
中执行sendMessage
或者post
操作,这些操作执行的线程是handler
中Looper
所在的线程,和handler
在哪里创建没关系,和Handler
中的Looper
在那创建有关系。
4、一个线程中只能有一个Looper
,但是一个Looper
可以对应多个handler
,在同一个Looper
中的消息都在同一条线程中执行。
2、Handler机制,sendMessage和post(Runnable)的区别?
要看sendMessage
和post
区别,需要从源码来看,下面是几种使用handler
的方式,先看下这些方式,然后再从源码分析有什么区别。 例1、 主线程中使用handler
//主线程
Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
if (msg.what == 1) {
//doing something
}
return false;
}
});
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
上面是在主线程中使用handler
,因为在Android
中系统已经在主线程中生成了Looper
,所以不需要自己来进行looper
的生成。如果上面的代码在子线程中执行,就会报
Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()
如果想着子线程中处理handler
的操作,就要必须要自己生成Looper
了。
例2 、子线程中使用handler
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler=new Handler();
handler.post(new Runnable() {
@Override
public void run() {
}
});
Looper.loop();
}
});
上面在Thread
中使用handler
,先执行Looper.prepare
方法,来在当前线程中生成一个Looper
对象并保存在当前线程的ThreadLocal
中。 看下Looper.prepare()
中的源码:
//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
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
可以看到prepare
方法中会先从sThreadLocal
中取如果之前已经生成过Looper
就会报错,否则就会生成一个新的Looper
并且保存在线程的ThreadLocal
中,这样可以确保每一个线程中只能有一个唯一的Looper
。
另外:由于Looper
中拥有当前线程的引用,所以有时候可以用Looper
的这种特点来判断当前线程是不是主线程。
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
boolean isMainThread() {
return Objects.requireNonNull(Looper.myLooper()).getThread() ==
Looper.getMainLooper().getThread();
}
sendMessage vs post
先来看看sendMessage
的代码调用链:
enqueueMessage
源码如下:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
return queue.enqueueMessage(msg, uptimeMillis);
}
enqueueMessage
的代码处理很简单,msg.target = this;
就是把当前的handler
对象给message.target
。然后再讲message
进入到队列中。
post代码调用链:
调用post
时候会先调用getPostMessage
生成一个Message
,后面和sendMessage
的流程一样。下面看下getPostMessage
方法的源码:
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
可以看到getPostMessage
中会先生成一个Messgae
,并且把runnable
赋值给message
的callback.
消息都放到MessageQueue
中后,看下Looper
是如何处理的。
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
return;
}
msg.target.dispatchMessage(msg);
}
Looper
中会遍历message
列表,当message
不为null
时调用msg.target.dispatchMessage(msg)
方法。看下message
结构:
也就是说msg.target.dispatchMessage
方法其实就是调用的Handler中的dispatchMessage
方法,下面看下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();
}
因为调用post
方法时生成的message.callback=runnable
,所以dispatchMessage
方法中会直接调用 message.callback.run();
也就是说直接执行post
中的runnable
方法。 而sendMessage
中如果mCallback
不为null
就会调用mCallback.handleMessage(msg)
方法,否则会直接调用handleMessage
方法。
总结 post
方法和handleMessage
方法的不同在于,post
的runnable
会直接在callback
中调用run
方法执行,而sendMessage
方法要用户主动重写mCallback
或者handleMessage
方法来处理。
3、Looper会一直消耗系统资源吗?
首先给出结论,Looper
不会一直消耗系统资源,当Looper
的MessageQueue
中没有消息时,或者定时消息没到执行时间时,当前持有Looper
的线程就会进入阻塞状态。
下面看下looper
所在的线程是如何进入阻塞状态的。looper
阻塞肯定跟消息出队有关,因此看下消息出队的代码。
消息出队
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
if(hasNoMessage)
{
nextPollTimeoutMillis =-1;
}
}
}
上面的消息出队方法被简写了,主要看下面这段,没有消息的时候nextPollTimeoutMillis=-1
;
if(hasNoMessage)
{
nextPollTimeoutMillis =-1;
}
看for循环里面这个字段所其的作用:
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
Binder.flushPendingCommands();
这个方法的作用可以看源码里面给出的解释:
/**
* Flush any Binder commands pending in the current thread to the kernel
* driver. This can be
* useful to call before performing an operation that may block for a long
* time, to ensure that any pending object references have been released
* in order to prevent the process from holding on to objects longer than
* it needs to.
*/
也就是说在用户线程要进入阻塞之前跟内核线程发送消息,防止用户线程长时间的持有某个对象。再看看下面这个方法:nativePollOnce(ptr, nextPollTimeoutMillis);
当nextPollingTimeOutMillis=-1
时,这个native
方法会阻塞当前线程,线程阻塞后,等下次有消息入队才会重新进入可运行状态,所以Looper
并不会一直死循环消耗运行内存,对队列中的颜色消息还没到时间时也会阻塞当前线程,但是会有一个阻塞时间也就是nextPollingTimeOutMillis>0
的时间。
当消息队列中没有消息的时候looper肯定是被消息入队唤醒的。
消息入队
boolean enqueueMessage(Message msg, long when) {
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.");
}
synchronized (this) {
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;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
上面可以看到消息入队之后会有一个
if (needWake) {
nativeWake(mPtr);
}
方法,调用这个方法就可以唤醒线程了。另外消息入队的时候是根据消息的delay
时间来在链表中排序的,delay
时间长的排在后面,时间短的排在前面。如果时间相同那么按插入时间先后来排,插入时间早的在前面,插入时间晚的在后面。
4、android的Handle机制,Looper关系,主线程的Handler是怎么判断收到的消息是哪个Handler传来的?
Looper
是如何判断Message
是从哪个handler
传来的呢?其实很简单,在1
中分析过,handler
在sendMessage
的时候会构建一个Message
对象,并且把自己放在Message
的target
里面,这样的话Looper
就可以根据Message
中的target
来判断当前的消息是哪个handler
传来的。
5、Handler机制流程、Looper中延迟消息谁来唤醒Looper?
从3中知道在消息出队的for
循环队列中会调用到下面的方法。
nativePollOnce(ptr, nextPollTimeoutMillis);
如果是延时消息,会在被阻塞nextPollTimeoutMillis
时间后被叫醒,nextPollTimeoutMillis
就是消息要执行的时间和当前的时间差。
6、Handler是如何引起内存泄漏的?如何解决?
在子线程中,如果手动为其创建Looper
,那么在所有的事情完成以后应该调用quit
方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper
以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper
。
Looper.myLooper().quit()
那么,如果在Handler
的handleMessage
方法中(或者是run方法)处理消息,如果这个是一个延时消息,会一直保存在主线程的消息队列里,并且会影响系统对Activity
的回收,造成内存泄露。
具体可以参考Handler
内存泄漏分析及解决
总结一下,解决Handler
内存泄露主要2点
1 、有延时消息,要在Activity
销毁的时候移除Messages
2、 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity
使用弱引用。
7、handler机制中如何确保Looper的唯一性?
Looper
是保存在线程的ThreadLocal
里面的,使用Handler
的时候要调用Looper.prepare()
来创建一个Looper
并放在当前的线程的ThreadLocal
里面。
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));
}
可以看到,如果多次调用prepare
的时候就会报Only one Looper may be created per thread
,所以这样就可以保证一个线程中只有唯一的一个Looper
。
8、Handler 是如何能够线程切换,发送Message的?
handler
的执行跟创建handler
的线程无关,跟创建looper
的线程相关,加入在子线程中创建一个Handler
,但是Handler
相关的Looper
是主线程的,这样,如果handler
执行post
一个runnable
,或者sendMessage
,最终的handle Message
都是在主线程中执行的。
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler=new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,"hello,world",Toast.LENGTH_LONG).show();
}
});
Looper.loop();
}
});
thread.start();
最后
小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。
以上是关于Android面试:Handler八大问题汇总的主要内容,如果未能解决你的问题,请参考以下文章