Android 面试笔记之Handler详解

本文深入解析Android中的Handler机制,包括Looper、MessageQueue的工作原理,Handler内存泄漏的原因及解决方案,子线程中Handler的使用,以及同步屏障的作用。帮助读者全面理解Android消息处理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android 面试之Handler详解

1. 一个线程有几个Handler?

可以有多个Handler 发送消息

2. 一个线程有几个Looper ? 如何保证?

2.1 一个线程有几个Looper ?
只能有一个,在创建Handler的时候指定Looper,该Looper创建的线程,就是处理消息的线程

mMyHandler = new MyHandler(thread.getLooper());
2.2 如何保证?
	每一个线程 都有一个ThreadLocal ( 运用了HashMap),用来保存 线程的 状态,标志位等 上下文环境 (大量的key-value 键值对)
2.2.1 如何保证一个key,只有一个value?
Hash算法  -- Hashcode 保证唯一性
2.2.2 如何保证 Looper 不可修改?
	在setLooper之前会检查,对应key的值是否为空,如果不为空,则抛出异常,保证唯一性

	Looper.java
	...
	// sThreadLocal 是static final 修饰的,整个app中只有一个,只会初始化一次 
	static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); 
 	...
	Looper.prepare() {
		if (sThreadLocal.get() != null) {
		            throw new RuntimeException("Only one Looper may be created per thread");
		        }
		        sThreadLocal.set(new Looper(quitAllowed));
		}

	sThreadLocal.set(new Looper(quitAllowed)); {
		...
		map.set(ThreadLocal, looper);  
		...
	}

	因为线程只有 ThreadLocal,所以保证了只能有一个Looper,也保证了<sThreadLocal, Looper>  两者一一对应,又唯一
2.3 Looper 和 MessageQueue 什么关系?
MessageQueue是在Looper里进行初始化的,并且是Final修饰的,一旦初始化不可改变  两者一一对应

Looper.java
...
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
小结
Thread -- ThreadLocalMap -- <sThreadLocal, Looper> -- Looper --MessageMessage

==> 一个Thread 只有一个MessageQueue
==> 一个Thread 只有一个Looper 

3. Handler内存泄漏原因? 为什么其他的内部类没有说过这个问题?

3.1 Handler内存泄漏原因?
	内部类持有外部类对象
3.1.1 为什么Handler会持有外部类对象?
1. 因为 Handler是通过匿名内部类创建的,而在java中 非静态内部类 和 匿名内部类 都默认持有外部类的引用,所以 匿名内部类 可以随意使用外部类的方法和属性。

2. 其次 在handler.enqueueMessage 时,msg.target = this; 使message持有了Handler对象引用,而Handler默认持有了 Activity.this引用,如果message做一些耗时操作,此时 Activity 销毁了,那么队列中的 等待执行的消息依然持有 Activity的引用。

3. 最后 Activity 无法被gc回收,从而导致了内存泄漏。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
	
Msg == > 持有 Handler 对象  ==> 持有Activity 对象

eg:如果这个消息一个小时候后执行,那么msg 会一直持有Activity对象,不能被JVM回收
3.2 如何解决泄漏问题?
1. 在 Actvitity. onDestroy的时候,将消息队列中的消息清空即可
	@Override
	protected void onDestroy() {
	    super.onDestroy();
	    mHandler.removeCallbacksAndMessages(null);
	}
	
2. 使用static修饰Handler,软引用 弱引用
	private static MyHandler mMyHandler;
	class MyHandler extends Handler {
		WeakReference<MainActivity> mContext;
		public MyHandler() {
			mContext = new WeakReference<>(MainActivity.this);
		}
	}
3.2.1 static关键字作用?为什么能够解决泄露问题?
	静态内部类,是不会持有外部类对象的
3.3 为什么其他的内部类没有说过这个问题?
	因为其他内部类不会持有Activity对象

4. 为何主线程可以new Handler? 如果想要在子线程中new Handler要做些什么准备?

4.1 为何主线程可以new Handler?
1. 因为 Android 应用在启动流程中会经过 ActivityThread.main方法,在main 方法中会调用 Looper.prepareMainLooper(); 以及 Looper.loop();方法对主线程Handler的Looper进行初始化。

2. 所以我们在 主线程中可以直接创建 Handler使用,而不需要在进行初始化Looper的操作。
4.2 如果想要在子线程中 new Handler 要做些什么准备?
	需要 进行 Looper.prepare 以及 Looper.loop(); 的调用,并且在初始化Handler的时候,将Looper传入

	不过不建议在子线程中进行 new Handler,建议使用 HandlerThread,已经封装好了
4.2.1 锁
wait and notifyAll:wait--释放锁,并且等待被唤醒 
	
notifyALl--通知之前wait的线程就绪,但不会释放锁,等synchronized代码块执行完,进行释放

内置锁(系统内置的锁):执行完synchronized代码块,由JVM来解锁
synchronized(object)

synchronized(this) 

eg1:
HandlerThread1中两个函数func1,func2

handlerThread1 = new HandlerThread1

两个线程中,分别执行handlerThread1.func1 和 handlerThread1.func2

结论:当线程1执行synchronized(this) 代码块时,线程2 等待

func1() {
	synchronized(this) 
	{

		///
	}

}

func2() {
	synchronized(this) 
	{

		///
	}

}

eg2:同上,将两个函数中的synchronized(this) 替换成 synchronized(object)

结论:如果两个线程锁的是同一个object,则相互关联,否则,不影响执行顺序 

5. 子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用? 主线程呢?

子线程中消息队列无消息时,睡眠等待,如没有消息,可调用Looper.quit,清空数据
5.1 主线程中不允许调用looper.quit函数,为什么?
因为主线程中大量用到了Handler消息处理,比如Activity的启动,UI交互,通信
5.2 MessageQueue 源码分析
数据结构:链表构成的 优先级队列

1. 入队的时候,根据执行时间排序,队列满的时候,阻塞,直到用户通过next取消息,通知MessageQueue可以进行入队

2. 出队的时候,Looper.loop启动轮询机制,当MessageQueue为空时,队列阻塞,等消息队列调用enqueueMessage时,通知队列可以取出消息,停止阻塞

6. 既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?

因为一个线程中 MessageQueue 只有一个,在 en’queueMessage 中运用了锁:synchronized(this),保证每次只能往队列中放一个,保证了线程安全

6.1 next 取消息时,为什么也要用到锁?不是会降低取消息速度吗?
因为要保证,在取消息时,没有其他消息插入,取消息 跟 插入消息 不能 同时进行
6.2 性能优化–线程

线程并不是越多越好,一般最大是 cpu数量的2倍 + 1

Glide Okhttp 都有自己的线程池?如何优化?-- 公用同一个线程池(使用反射机制,重新对框架的线程池赋值)

7. 我们使用 Message 时应该如何创建它?

使用obtain,复用--用到了享元设计模式

最大不要超过50个消息

如果不使用obtain,一直 new message,会造成 内存抖动 ==》GC == 》 卡顿

在自定义View中Draw(),也不能使用new,也会造成同样的内存问题

8. Looper 死循环为什么不会导致应用卡死

8.1 既然 Handler 消息全是 Loop 来的,为什么没有ANR问题?
之前不是说五秒钟不响应就会出现ANR问题吗?为什么休眠好长时间也不会ANR?

产生ANR的原因不是因为主线程休眠时,而是因为输入事件没有响应,输入事件没有响应就没法唤醒Looper,才加入了五秒限制
8.2 应用在没有消息的时候,是在休眠,释放线程,不会导致应用卡死,卡死是ANR,而Looper是睡眠

思考? 传送带模式

1. Handler如何实现线程间的跨越?

子线程发送消息,主线程取出消息

子线程:handler.sendMessage-- enqueueMessage--  queue.enqueueMessage(管理内存的)

主线程:ActivityThread 中调用 Looper.loop -- queue.next 取出消息 -- dispatchMessage分发消息 --handler.handlerMessage

在这里插入图片描述

2. Activity启动流程

Launcher 应用里点击 我们的应用图片,启动  zygote,创建一个JVM,JVM会去调用ActivityThread中的main函数
2.1 ActivityThread启动Activity

在这里插入图片描述

2.2 根Activity启动时序图

在这里插入图片描述

3. Handler通信机制,就是线程中管理内存的机制

4. 消息机制之同步屏障

4.1 如果有一个事件(message)必须尽快执行,那么Handler是如何通过消息屏障保证事件顺利执行的?

消息是根据执行时间进行先后排序,然后消息是保存在队列中,因而消息只能从队列的队头取出来。那么问题来了!需要紧急处理的消息怎么办?

其实是通过设置 同步屏障来解决的
4.2 那么同步屏障是一个什么东西呢?
4.2.1 先来解释一下 ,Handler 中 Message 分为 同步消息 跟 异步消息,一般来说 ,两种消息没什么区别,只有在设置 同步屏障的时候才会出现差异
4.2.2 一般 Message 都会有target,但是 设置同步屏障时的 Message 没有target,简而言之,同步屏障是一个 target为空的 Message
4.2.3 这种差异最终体现在 Message.next 的时候,上一段代码:
Message next() {
    //...

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        //...

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) { //碰到同步屏障
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                // do while循环遍历消息链表
                // 跳出循环时,msg指向离表头最近的一个异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    //...
                } else {
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        //将msg从消息链表中移除
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    //返回异步消息
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            //...
        }

        //...
    }
}

可以看到,如果设置了同步屏障,会优先处理 target为空的消息(异步消息),这算是Handler消息的一种优先级机制

4.3.4 设置同步屏障
mHandler.getLooper().getQueue().postSyncBarrier();
4.3.5 同步屏障应用
Android应用框架中为了更快的响应UI刷新事件在ViewRootImpl.scheduleTraversals中使用了同步屏障

参考资料

  1. 在腾讯课堂中,享学提供的课程
  2. 同步屏障:https://blog.youkuaiyun.com/asdgbc/article/details/79148180
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值