前言
在Android开发的多线程应用场景中,Handler机制十分常用。而在面试中,Handler机制又是面试官百问不厌的问题,由浅入深、非常难懂,这就导致了面试中很多朋友因为不懂Handler原理被淘汰。着实可惜!
下面,我将大厂面试题的方式来详解 Handler机制 的工作原理,干货较多,大家需静心阅读,方可吸收知识。
PS:关于我
本人是一个拥有6年开发经验的帅气Android攻城狮,记得看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。
另外耗时两年整理收集的Android一线大厂面试核心知识点出炉,【完整版】已更新在我的【Github】,有面试需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!
Github地址:【https://github.com/733gh/xiongfan】
1、Handler、Looper、MessageQueue、Thread 的对应关系
首先,Looper 中的 MessageQueue 和 Thread 两个字段都属于常量,且 Looper 实例是存在 ThreadLocal 中,这说明了 Looper 和 MessageQueue 之间是一对一应的关系,且一个 Thread 在其整个生命周期内都只会关联到同一个 Looper 对象和同一个 MessageQueue 对象
public final class Looper {
final MessageQueue mQueue;
final Thread mThread;
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
}
Handler 中的 Looper 和 MessageQueue 两个字段也都属于常量,说明 Handler 对于 Looper 和 MessageQueue 都是一对一的关系。但是 Looper 和 MessageQueue 对于 Handler 却可以是一对多的关系,例如,多个子线程内声明的 Handler 都可以关联到 mainLooper
public class Handler {
@UnsupportedAppUsage
final Looper mLooper;
final MessageQueue mQueue;
}
2、Handler 的同步机制
MessageQueue 在保存 Message 的时候,enqueueMessage
方法内部已经加上了同步锁,从而避免了多个线程同时发送消息导致竞态问题。此外,next()
方法内部也加上了同步锁,所以也保障了 Looper 分发 Message 的有序性。最重要的一点是,Looper 总是由一个特定的线程来执行遍历,所以在消费 Message 的时候也不存在竞态
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
···
}
return true;
}
@UnsupportedAppUsage
Message next() {
···
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
···
}
···
}
}
3、Handler 如何发送同步消息
如果我们在子线程通过 Handler 向主线程发送了一个消息,希望等到消息执行完毕后子线程才继续运行,这该如何实现?其实像这种涉及到多线程同步等待的问题,往往都是需要依赖于线程休眠+线程唤醒机制来实现的
Handler 本身就提供了一个runWithScissors
方法可以用于实现这种功能,只是被隐藏了,我们无法直接调用到。runWithScissors
首先会判断目标线程是否就是当前线程,是的话则直接执行 Runnable,否则就需要使用到 BlockingRunnable
/**
* @hide
*/
public final boolean runWithScissors(@NonNull Runnable r, long timeout) {
if (r == null) {
throw new IllegalArgumentException("runnable must not be null");
}
if (timeout < 0) {
throw new IllegalArgumentException("timeout must be non-negative");
}
if (Looper.myLooper() == mLooper) {
r.run();
return true;
}
BlockingRunnable br = new BlockingRunnable(r);
return br.postAndWait(this, timeout);
}
BlockingRunnable 的逻辑也很简单,在 Runnable 执行完前会通过调用 wait()
方法来使发送者线程转为阻塞等待状态,当任务执行完毕后再通过notifyAll()
来唤醒发送者线程,从而实现了在 Runnable 被执行完之前发送者线程都会一直处于等待状态
private static final class BlockingRunnable implements Runnable {
private final Runnable mTask;
//用于标记 mTask 是否已经执行完毕
private boolean mDone;
public BlockingRunnable(Runnable task) {
mTask = task;
}
@Override
public void run() {
try {
mTask.run();
} finally {
synchronized (this) {
mDone = true;
notifyAll();
}
}
}
public boolean postAndWait(Handler handler, long timeout) {
if (!handler.post(this)) {
return false;
}
synchronized (this) {
if (timeout > 0) {
final long expirationTime = SystemClock.uptimeMillis() + timeout;
while (!mDone) {
long delay = expirationTime - SystemClock.uptimeMillis();
if (delay <= 0) {
return false; // timeout
}
try {
//限时等待
wait(delay);
} catch (InterruptedException ex) {
}
}
} else {
while (!mDone) {
try {
//无限期等待
wait();
} catch (InterruptedException ex) {
}
}
}
}
return true;
}
}
虽然 runWithScissors
方法我们无法直接调用,但是我们也可以依靠这思路自己来实现 BlockingRunnable,折中实现这个功能。但这种方式并不安全,如果 Loop 意外退出循环导致该 Runnable 无法被执行的话,就会导致被暂停的线程一直无法被唤醒,需要谨慎使用
4、Handler 如何避免内存泄漏
当退出 Activity 时,如果 Handler 中还保存着待处理的延时消息的话,那么就会导致内存泄漏,此时可以通过调用Handler.removeCallbacksAndMessages(null)
来移除所有待处理的 Message
该方法会将消息队列中所有 Message.obj
等于 token 的 Message 均给移除掉,如果 token 为 null 的话则会移除所有 Message
public final void removeCallbacksAndMessages(@Nullable Object token) {
mQueue.removeCallbacksAndMessages(this, token);
}
5、Message 如何复用
因为 Android 系统本身就存在很多事件需要交由 Message 来交付给 mainLooper,所以 Message 的创建是很频繁的。为了减少 Message 频繁重复创建的情况,Message 提供了 MessagePool 用于实现 Message 的缓存复用,以此来优化内存使用
当 Looper 消费了 Message 后会调用recycleUnchecked()
方法将 Message 进行回收,在清除了各项资源后会缓存到 sPool 变量上,同时将之前缓存的 Message 置为下一个节点 next,通过这种链表结构来缓存最多 50 个Message。这里使用到的是享元设计模式
obtain()
方法则会判断当前是否有可用的缓存,有的话则将 sPool 从链表中移除后返回,否则就返回一个新的 Message 实例。所以我们在发送消息的时候应该尽量通过调用Message.obtain()
或者Handler.obtainMessage()
方法来获取 Message 实例
public final class Message implements Parcelable {
/** @hide */
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;
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();
}
@UnsupportedAppUsage
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
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++;
}
}
}
}
6、Message 复用机制存在的问题
由于 Message 采用了缓存复用机制,从而导致了一个 Message 失效问题。当 handleMessage
方法被回调后,Message 携带的所有参数都会被清空,而如果外部的 handleMessage
方法是使用了异步线程来处理 Message 的话,那么异步线程只会得到一个空白的 Message
val handler = object : Handler() {
override fun handleMessage(msg: Message) {
handleMessageAsync(msg)
}
}
fun handleMessageAsync(msg: Message) {
thread {
//只会得到一个空白的 Message 对象
println(msg.obj)
}
}
7、Message 如何提高优先级
Handler 包含一个 sendMessageAtFrontOfQueue
方法可以用于提高 Message 的处理优先级。该方法为 Message 设定的时间戳是 0,使得 Message 可以直接插入到 MessageQueue 的头部,从而做到优先处理。但官方并不推荐使用这个方法,因为最极端的情况下可能会使得其它 Message 一直得不到处理或者其它意想不到的情况
public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
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, 0);
}
8、检测 Looper 分发 Message 的效率
Looper 在进行 Loop 循环时,会通过 Observer 向外回调每个 Message 的回调事件。且如果设定了 slowDispatchThresholdMs
和 slowDeliveryThresholdMs
这两个阈值的话,则会对 Message 的分发时机和分发耗时进行监测,存在异常情况的话就会打印 Log。该机制可以用于实现应用性能监测,发现潜在的 Message 处理异常情况,但可惜监测方法被系统隐藏了
public static void loop() {
final Looper me = myLooper();
···
for (;;) {
Message msg = queue.next(); // might block
···
//用于向外回调通知 Message 的分发事件
final Observer observer = sObserver;
final long traceTag = me.mTraceTag;
//如果Looper分发Message的时间晚于预定时间且超出这个阈值,则认为Looper分发过慢
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
//如果向外分发出去的Message的处理时间超出这个阈值,则认为外部处理过慢
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
//开始分发 Message 的时间
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
//Message 分发结束的时间
final long dispatchEnd;
Object token = null;
if (observer != null) {
//开始分发 Message
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
//完成 Message 的分发,且没有抛出异常
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
//分发 Message 时抛出了异常
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
//如果 Message 的分发时间晚于预定时间,且间隔超出10毫秒,则认为属于延迟交付
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
···
}
}
9、主线程 Looper 在哪里创建
由 ActivityThread 类的 main()
方法来创建。该 main()
方法即 Java 程序的运行起始点,当应用启动时系统就自动为我们在主线程做好了 mainLooper 的初始化,而且已经调用了Looper.loop()
方法开启了消息的循环处理,应用在使用过程中的各种交互逻辑(例如:屏幕的触摸事件、列表的滑动等)就都是在这个循环里完成分发的。正是因为 Android 系统已经自动完成了主线程 Looper 的初始化,所以我们在主线程中才可以直接使用 Handler 的无参构造函数来完成 UI 相关事件的处理
public final class ActivityThread extends ClientTransactionHandler {
public static void main(String[] args) {
···
Looper.prepareMainLooper();
···
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}
10、主线程 Looper 什么时候退出循环
当 ActivityThread 内部的 Handler 收到了 EXIT_APPLICATION 消息后,就会退出 Looper 循环
public void handleMessage(Message msg) {
switch (msg.what) {
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;
}
}
11、主线程 Looper.loop() 为什么不会导致 ANR
这个问题在网上很常见,我第一次看到时就觉得这种问题很奇怪,主线程凭啥会 ANR?这个问题感觉本身就是特意为了来误导人
看以下例子。doSomeThing()
方法是放在 for 循环这个死循环的后边,对于该方法来说,主线程的确是被阻塞住了,导致该方法一直无法得到执行。可是对于应用来说,应用在主线程内的所有操作其实都是被放在了 for 循环之内,一直有得到执行,是个死循环也无所谓,所以对于应用来说主线程并没有被阻塞,自然不会导致 ANR。此外,当 MessageQueue 中当前没有消息需要处理时,也会依靠 epoll 机制挂起主线程,避免了其一直占用 CPU 资源
public static void main(String[] args) {
for (; ; ) {
//主线程执行....
}
doSomeThing();
}
所以在 ActivityThread 的 main 方法中,在开启了消息循环之后,并没有声明什么有意义的代码。正常来说应用是不会退出 loop 循环的,如果能够跳出循环,也只会导致直接就抛出异常
public static void main(String[] args) {
···
Looper.prepareMainLooper();
···
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
所以说,loop 循环本身不会导致 ANR,会出现 ANR 是因为在 loop 循环之内 Message 处理时间过长
12、子线程一定无法弹 Toast 吗
不一定,只能说是在子线程中无法直接弹出 Toast,但可以实现。因为 Toast 的构造函数中会要求拿到一个 Looper 对象,如果构造参数没有传入不为 null 的 Looper 实例的话,则尝试使用调用者线程关联的 Looper 对象,如果都获取不到的话则会抛出异常
public Toast(Context context) {
this(context, null);
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mToken = new Binder();
looper = getLooper(looper);
mHandler = new Handler(looper);
···
}
private Looper getLooper(@Nullable Looper looper) {
if (looper != null) {
return looper;
}
//Looper.myLooper() 为 null 的话就会直接抛出异常
return checkNotNull(Looper.myLooper(),
"Can't toast on a thread that has not called Looper.prepare()");
}
为了在子线程弹 Toast,就需要主动为子线程创建 Looper 对象及开启 loop 循环。但这种方法会导致子线程一直无法退出循环,需要通过Looper.myLooper().quit()
来主动退出循环
inner class TestThread : Thread() {
override fun run() {
Looper.prepare()
Toast.makeText(
this@MainActivity,
"Hello: " + Thread.currentThread().name,
Toast.LENGTH_SHORT
).show()
Looper.loop()
}
}
13、子线程一定无法更新 UI?主线程就一定可以?
在子线程能够弹出 Toast 就已经说明了子线程也是可以更新 UI 的,Android 系统只是限制了必须在同个线程内进行 ViewRootImpl 的创建和更新这两个操作,而不是要求必须在主线程进行
如果使用不当的话,即使在主线程更新 UI 也可能会导致应用崩溃。例如,在子线程先通过 show+hide 来触发 ViewRootImpl 的创建,然后在主线程再来尝试显示该 Dialog,此时就会发现程序直接崩溃了
class MainActivity : AppCompatActivity() {
private lateinit var alertDialog: AlertDialog
private val thread = object : Thread("hello") {
override fun run() {
Looper.prepare()
Handler().post {
alertDialog =
AlertDialog.Builder(this@MainActivity).setMessage(Thread.currentThread().name)
.create()
alertDialog.show()
alertDialog.hide()
}
Looper.loop()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_test.setOnClickListener {
alertDialog.show()
}
thread.start()
}
}
E/AndroidRuntime: FATAL EXCEPTION: main
Process: github.leavesc.test, PID: 5243
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6892)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1048)
at android.view.View.requestLayout(View.java:19781)
at android.view.View.setFlags(View.java:11478)
at android.view.View.setVisibility(View.java:8069)
at android.app.Dialog.show(Dialog.java:293)
ViewRootImpl 在初始化的时候会将当前线程保存到 mThread,在后续进行 UI 更新的时候就会调用checkThread()
方法进行线程检查,如果发现存在多线程调用则直接抛出以上的异常信息
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
final Thread mThread;
public ViewRootImpl(Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
mThread = Thread.currentThread();
···
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
}
14、为什么 UI 体系要采用单线程模型
其实这很好理解,就是为了提高运行效率和降低实现难度。如果允许多线程并发访问 UI 的话,为了避免竞态,很多即使只是小范围的局部刷新操作(例如,TextView.setText)都势必需要加上同步锁,这无疑会加大 UI 刷新操作的“成本”,降低了整个应用的运行效率。而且会导致 Android 的 UI 体系在实现时就被迫需要对多线程环境进行“防御”,即使开发者一直是使用同个线程来更新 UI,这就加大了系统的实现难度
所以,最为简单高效的方式就是采用单线程模型来访问 UI
15、如何跨线程下发任务
通常情况下,两个线程之间的通信是比较麻烦的,需要做很多线程同步操作。而依靠 Looper 的特性,我们就可以用比较简单的方式来实现跨线程下发任务
看以下代码,从 TestThread 运行后弹出的线程名可以知道, Toast 是在 Thread_1 被弹出来的。如果将 Thread_2 想像成主线程的话,那么以下代码就相当于从主线程向子线程下发耗时任务了,这个实现思路就相当于 Android 提供的 HandlerThread 类
inner class TestThread : Thread("Thread_1") {
override fun run() {
Looper.prepare()
val looper = Looper.myLooper()
object : Thread("Thread_2") {
override fun run() {
val handler = Handler(looper!!)
handler.post {
//输出结果是:Thread_1
Toast.makeText(
this@MainActivity,
Thread.currentThread().name,
Toast.LENGTH_SHORT
).show()
}
}
}.start()
Looper.loop()
}
}
16、如何判断当前是不是主线程
通过 Looper 来判断
if (Looper.myLooper() == Looper.getMainLooper()) {
//是主线程
}
if (Looper.getMainLooper().isCurrentThread){
//是主线程
}
17、如何全局捕获主线程异常
比较卧槽的一个做法就是通过嵌套 Loop 循环来实现。向主线程 Loop 发送 一个 Runnable,在 Runnable 里死循环执行 Loop 循环,这就会使得主线程消息队列中的所有任务都会被交由该 Runnable 来调用,只要加上 try catch 后就可以捕获主线程的任意异常了,做到主线程永不崩溃
Handler(Looper.getMainLooper()).post {
while (true) {
try {
Looper.loop()
} catch (throwable: Throwable) {
throwable.printStackTrace()
Log.e("TAG", throwable.message ?: "")
}
}
}
Github地址:【https://github.com/733gh/xiongfan】
网友评论