1 大纲
2 Handler问题三连:是什么?有什么用?为什么要用,不用行不行?
2.1 Handler是什么?
答:Handler是Android FrameWork架构中的一个基础组件;
2.2 Handler有什么用?
答:把子线程中的UI更新信息传递给主线程(UI线程),以此完成UI更新操作;
2.3 Handler为什么要用,不用行不行?
答:不行,Handler是Android在设计之初就封装的一套消息创建、传递、处理机制。Android要求在主线程(UI线程)中更新UI。
3 真的只能在主(UI)线程中更新UI吗?
答:Android要求在主线程(UI线程)中更新UI,是要求,不是规定,硬要在子线程中更新UI也是可以的。
3.1 为什么可在一个子线程中创建一个对话框,并可更新对话框的UI,而不能更新主线程的UI?
答:Android的UI更新(GUI)被设计成了单线程,子线程可更新子线程创建的UI、不能更新主线程创建的UI。案例如下:
private lateinit var textView: TextView
thread {
Looper.prepare()
val dialog = AlertDialog.Builder(this)
.apply {
setIcon(R.drawable.ic_launcher)
setTitle("子线程创建的对话框")
setCancelable(true)
setNegativeButton("子线程更新 主线程创建的UI", object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
// 抛出异常:android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
// 翻译后是:只有创建这个view的线程才能操作这个view;
textView.text = "子线程更新 主线程创建的UI ${Thread.currentThread().name}"
}
})
setPositiveButton("子线程更新 子线程创建的UI", object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
setTitle("子线程更新 子线程创建的UI ${Thread.currentThread().name}")
}
})
}.create()
dialog.show()
Looper.loop()
}
3.2 为什么直接在子线程中更新主线程创建的UI,没有报错;而如果延迟300毫秒后就报错了?
答:Android系统在onResume()时会检查:只有创建这个view的线程才能操作这个view,否则抛出异常。①ViewRootImp在onCreate()时还没创建,所以子线程中更新了 主线程创建的UI。②在onResume()时,ActivityThread的handleResumeActivity()执行后才创建ViewRootImp,然后调用requestLayout(),走到checkThread()检查时抛出异常。案例如下:
fun onCreate(savedInstanceState: Bundle?) {
thread {
// 直接在子线程中更新了 主线程创建的UI,却没有报错:
textView.text = "子线程更新UI ${Thread.currentThread().name}"
}
thread {
// 2、加上休眠300毫秒,程序就崩溃了
Thread.sleep(300)
textView.text = "子线程更新UI ${Thread.currentThread().name}"
}
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException ("Only the original thread that created a view hierarchy can touch its views.");
}
}
3.3 Android UI更新机制(GUI) 为何设计成了单线程的?
答:因为多线程对同一个UI控件操作,容易造成不可控的错误。而必须解决这种多线程安全问题,简单的做法是加锁,不是加一个,而是每层加锁(用户代码–GUI顶层–GUI底层),但是这意味着更多耗时以及UI更新效率低下,而如果每层共用一把锁的话,其实就是单线程。因此,Android没有采用线程锁,而是采用单线程消息队列机制,实现了一个伪锁。
4 真的不能在主(UI)线程中执行网络操作吗?
答:能。通常,网络请求在主线程进行,会抛出异常NetworkOnMainThreadException,因为Android 2.3引入用于检测两大问题:ThreadPolicy(线程策略) 和 VmPolicy(VM策略)。在onCreate()的setContentView()后加上permitNetWork(),把严苟模式的网络监测关了,就可以在主线程执行网络请求了。
5 Handler怎么用?
答:有两个方式,一是sendMessage() + handleMessage();二是post(runnable)。其中,post(Runnable r)调用的是sendMessageDelayed(getPostMessage®, 0)发送消息,只不过延迟秒数为0。案例如下:
// 方式-:sendMessage() + handleMessage()
// 步骤1:在主线程中 通过匿名内部类 创建Handler类对象
val mhandler = Handler() {
// 通过复写handlerMessage()从而确定更新UI的操作
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
// 需执行的UI操作
}
}
// 步骤2:创建消息对象
val msg = Message.obtain() // 实例化消息对象
msg.what = 1 // 消息标识
msg.obj = "AA" // 消息内容存放
// 步骤3:在工作线程中 通过Handler发送消息到消息队列中
mHandler.sendMessage(msg)
// 步骤4:开启工作线程(同时启动了Handler)
// 方式二是post(runnable)
// 步骤1:在主线程中创建Handler实例
val mhandler = Handler()
// 步骤2:在工作线程中 发送消息到消息队列中 & 指定操作UI内容,需传入1个Runnable对象
mHandler.post(Runnable {
// 需执行的UI操作
}
})
// 步骤3:开启工作线程(同时启动了Handler)
5.1 那Runnable是怎么变成Message的呢?
答:在getPostMessage()方法中,通过Message.obtain()获取一个新的Mesage对象,把Runnable变量的值赋值给callback属性。
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
5.2 其他两个种在子线程中更新UI的方法?
答:activity.runOnUiThread(),和view.post()与view.postDelay();
6 为什么建议使用Message.obtain()来创建Message实例?
答:因为随着事件不断发送,会频繁大量创建消息对象,带来内存消耗;因此使用Message.obtain()通过消息池复用消息对象,可以避免重复创建对象,节约内存。
6.1 obtain()是怎么复用消息对象的?
答:分析obtain()的逻辑(如下),加锁判断消息池是否为空?不为空,取消息池的链表表头消息对象返回,正在使用标记为0,吃容量-1;为空,创建一个新的消息对象返回。Message池其实是一个单链表。获取消息池逻辑如下图:
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();
}
6.2 消息对象时什么时候加到池中?
答:当消息对象被Looper分发完后,在loop()最后会调用msg.recycleUnchecked()函数,回收没有被使用的消息对象。具体逻辑是:标记设置为FLAG_IN_USE,表示正在使用,相关属性重置;加锁判断消息池是否满50,未满,采用单链表头插法把消息插入到链表表头。回收消息逻辑如下图:
void recycleUnchecked() {
// ......
flags = FLAG_IN_USE; // 表示正在使用
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) { // MAX_POOL_SIZE = 50
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
7 Handler涉及到的类有哪几个?
答:主要有6个,分别是如下:
图片来源于:1、换个姿势,带着问题看Handler
7 为什么子线程中不可以直接new Handler(),而主线程中可以?
答:在new Handler()时,调用Looper.myLooper()获取当前线程的Looper对象,若线程无Looper对象则抛出异常,异常和逻辑如下图所示。主线程在启动时在ActivityThread的main函数中,调用Looper.prepareMainLooper()创建了Looper和MessageQueue对象,完成了初始化。而子线程还需要额外调用Looper.prepare()和Looper.loop()开启轮询,否则会报错。
public Handler(Callback callback, boolean async) {
// 1. 指定Looper对象
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");
}
// 2. handler对象绑定Looper的消息队列对象(MessageQueue)
mQueue = mLooper.mQueue;
}
7.1 具体的Looper.prepareMainLooper()的初始化过程是?
定位到:ActivityThread.main()
public static void main(String[] args) {
//...
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
//...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
定位到:Looper.prepareMainLooper();
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
定位到:Looper → prepare函数
public static final 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对象,并存放在ThreadLocal变量中
}
定位到:Looper → Looper构造函数
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed); // 创建1个Looper对象时,创建一个与之绑定的消息队列对象
mRun = true;
mThread = Thread.currentThread(); // Looper与线程绑定
}
7.2 mQuitAllowed变量,直译「退出允许」,具体作用是?
答:用来防止开发者手动终止消息队列,停止Looper循环。定位到:MessageQueue → quit函数:
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
8 ThreadLocal是如何将Looper对象存放在线程里,并解决并发访问的冲突问题的?
8.1 ThreadLocal的经典场景?
答:ThreadLocal是线程局部变量。(1)场景1:ThreadLocal用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本,而不会影响其他线程的副本,确保了线程安全。
(2)场景2:ThreadLocal用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal直接获取到,避免了传参,类似于全局变量的概念。
8.2 1个线程中初始化多个handler,那会创建多少个Looper吗?
答:1个线程只能创建1个Looper。因为每个线程创建Handler前,调用Looper.prepare()开启轮询,内部通过sThreadLocal以ThreadLocal为key,存储一个Looper对象在当前线程中,当重复调用Looper.prepare()时,sThreadLocal.get()不为空时会抛出异常。逻辑如下:
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
}
// Looper.java
public static final void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread"); // 每个线程只能创建一个Looper
}
sThreadLocal.set(new Looper(quitAllowed)); // 创建1个Looper对象
}
答:同理,1个线程只能创建1个Looper,1个Looper只定义了1个final类型的sThreadLocal。
8.3 ThreadLocal是如何将Looper对象存放在线程里,并解决并发访问的冲突问题?
答:每个线程内部都维护了一个ThreadLocalMap,成员变量名是threadLocals,这个map的key是ThreadLocal。在存值时,通过当前线程取出线程的成员变量threadLocals,以ThreadLocal为key,存储1个Looper对象;在获取值时,通过当前线程取出线程的变量threadLocals,以ThreadLocal为key,获取此Looper对象。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。以下4步骤辅助理解:
(1)定位到:Looper→ prepare函数:
public static final 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对象,并存放在ThreadLocal变量中
}
(2)定位到:ThreadLocal → set函数:
public void set(T value) {
// 取出当前线程
Thread t = Thread.currentThread();
// 通过当前线程取出线程的成员(ThreadLocalMap)threadLocals(2)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 向ThreadLocalMap,以ThreadLocal为key,存储一个值 (---->源码看:2.5 再深入分析)
map.set(this, value);
} else {
// 创建Thread成员threadLocals(3)
createMap(t, value);
}
}
(3)定位到:ThreadLocal → getMap函数:
ThreadLocalMap getMap(Thread t) {
// 取出Thread的成员
return t.threadLocals;
}
(4)定位到:ThreadLocal → get函数:
public T get() {
// 取出当前线程
Thread t = Thread.currentThread();
// 不同线程有不同ThreadLocalMap,就是有不同的副本
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据key获取table中的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value;
}
}
return setInitialValue();
}
(5)定位到:ThreadLocalMap类,也就是Thread.threadLocals
// ThreadLocalMap类中最重要的就是上面的的Entry内部类
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
//...
}
在ThreadLocalMap中会有一个Entry类型的数组,名字叫table,可以把Entry理解为一个 map,其键值对为:
键,当前的ThreadLocal;
值,实际需要存储的变量;
8.4 ThreadLocalMap和WeakHashMap的区别?
答:如下图,ThreadLocalMap既然类似于Map,所以就和HashMap 一样,也会有包括set、get、rehash、resize等一系列标准操作。但是,虽然思路和HashMap 是类似的,但是具体实现会有一些不同。
比如其中一个不同点就是,HashMap在面对hash冲突的时候,采用的是拉链法。它会先把对象hash到一个对应的格子中,如果有冲突就用链表的形式往下链,如下图所示:
但是ThreadLocalMap解决hash冲突的方式是不一样的,它采用的是开放定址法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。
8.5 ThreadLocal的使用建议
答:(1)声明为全局静态final成员:在1个线程有1个ThreadLocal就够了,没必要创建多个。因为设置value时,以ThreadLocal为key,内容为value。如果创建了多个ThreadLocal,变换了引用,永远都找不着1个ThreadLocal对应的value;
(2)避免存储大量对象:因为ThreadLocalMap解决hash冲突的方式采用的是开放定址法,适合对象较少的场景;
(3)用完后及时移除对象:因为线程的生命周期是很长的,如果线程迟迟不会终止,那么可能ThreadLocal以及它所对应的value早就不再有用了,也不会被回收。用下面这张图来看一下具体的引用链路(实线代表强引用,虚线代表弱引用):Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例。这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个ThreadLocal的Value就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个Value了,此时也就发生了内存泄漏问题:
在这种情况下,我们应该调用ThreadLocal的remove方法删除对应的value对象,保证它们都能够被正常的回收,避免内存泄漏:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) m.remove(this);
}
8.6 主线程和子线程的Looper是同一个么?
答:主线程和子线程Looper不是同一个。因为主线程调用Looper.prepareMainLooper()创建主线程的Looper对象,子线程调用Looper.prepare()创建子线程的Looper对象,不是同一个。
8.7 Handler内部是如何获取到Looper对象的?
答:Handler调用Looper.myLooper(),再调用sThreadLocal.get(),通过当前线程取出线程的变量threadLocals,以ThreadLocal为key,获取此Looper对象。以下3步骤辅助理解:
(1)定位到:Handler→ 构造函数:
public Handler(@Nullable Callback callback, boolean async) {
mLooper = Looper.myLooper();
}
(2)定位到:Looper → myLooper函数:
// Looper.java
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
(3)定位到:ThreadLocal → get函数:
public T get() {
// 取出当前线程
Thread t = Thread.currentThread();
// 不同线程有不同ThreadLocalMap,就是有不同的副本
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据key获取table中的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value;
}
}
return setInitialValue();
}
9 主线程给子线程的Handler发送消息怎么写?
答:普通实现方法中,子线程初始化Handler,存在报空指针风险,因为:多线程并发的问题,当主线程执行到sendMessage时,子线程的Handler还没有初始化。因此,最优方法是:通过HandlerThread获取子线程的Looper,再在主线程初始化Handler,并传入子线程的Looper。
// 在主线程中给子线程的Handler发送信息
fun mainSendMessageToThread(view: View) {
val thread = LooperThread()
thread.start()
// 1.报空指针,因为:多线程并发的问题,当主线程执行到sendEmptyMessage时,子线程的Handler还没有初始化
// thread.mHandler!!.sendEmptyMessage(0x123)
// 2.解决方法是:主线程延时给子线程发消息,等待子线程的Handler完成初始化
Handler().postDelayed(Runnable {
thread.mHandler!!.sendMessage(obtainSyncMessage(0x123, "同步消息"))
}, 1000)
// 3.更优解决方法是:通过HandlerThread获取子线程的Looper,再在主线程初始化Handler,并传入子线程的Looper
initHandlerThread()
}
// 通过HandlerThread获取子线程的Looper,再在主线程初始化Handler,并传入子线程的Looper
private fun initHandlerThread() {
val handlerThread = HandlerThread("Thread-1")
handlerThread.start()
val handler = MyHandler(handlerThread.looper)
handler.sendMessage(obtainSyncMessage(0x123, "同步消息"))
}
/**
* 子线程的Handler接收信息
*/
private inner class LooperThread : Thread() {
var mHandler: Handler? = null
override fun run() {
Looper.prepare()
// 存在报空指针风险,因为:多线程并发的问题,当主线程执行到sendEmptyMessage时,子线程的Handler还没有初始化
mHandler = MyHandler(Looper.myLooper())
Looper.loop()
}
}
10 HandlerThread实现的核心原理?
答:①HandlerThread = 继承线程 + 封装Looper;②getLooper()加锁死循环wait()等待,而堵塞线程;③线程的run方法中,加锁等待当前线程的Looper对象创建成功,再notifyAll()通知getLooper()中的wait()等待,说Looper对象已经创建成功了;④等待唤醒后,getLooper()返回在run方法中创建的Looper对象。
图片来源于:1、换个姿势,带着问题看Handler
11 一个C++层的Looper在创建过程中,是怎么在内部创建一个管道的?
答:首先看,从Java到C++层创建Looper的过程,如下图。在Looper创建过程中,调用pipe创建一个管道,包含了读/写端文件描述符,为读文件描述符监听写文件描述符提供了条件;再创建epoll对象mEpollFd,向其添加读文件描述符,对管道的写文件操作进行监听。
12 Looper是怎么循环分拣队列里的消息的?
答:ActivityThread在main函数中调用Looper.prepareMainLooper完成主线程的Loper初始化,然后调用Looper.loop()开启消息循环等待接收(分拣)消息。消息循环如下UML图,这个过程分为4个步骤:
(1)第一步定位到:Looper → loop函数,首先获得当前线程的Looper对象和消息队列,然后循环不断地检查消息队列中是否有新消息需要处理,有就取出消息判空后分发给Handerl处理,没有就在消息队列的next()中进入睡眠状态,等待新消息。
public static void loop() {
Looper.loop()final Looper me = myLooper(); // 获得当前线程的Looper实例
final MessageQueue queue = me.mQueue; // 获取消息队列
for (;;) { // 死循环
Message msg = queue.next(); // 取出队列中的消息
if (msg == null) {
return;
}
msg.target.dispatchMessage(msg); // 将消息分发给Handler
}
}
(2)第二步定位到:MessageQueue -> next函数,遍历队列获取消息,如果是有效消息就返回处理;如果当前时间小于消息时间,计算堵塞等待时间,赋值给nextPollTimeoutMillis,表示最长堵塞等待时间,期间有新消息进来,可能会立即返回执行;没有消息了,设置nextPollTimeoutMillis=-1,当前线程进入休眠状态,直到它被其他线程唤醒为止。
(3)第三步定位到:MessageQueue → nativePollOnce函数,这是一个JNI方法,调用了pollOnce方法,再调用Looper的pollOnce(),通过for循环不断地调用成员函数polInner来检查当前线程是否有新的消息需要处理。
(4)第四步定位到:Looper → pollInner函数,关键是调用epoll_wait来监听pipe管道的读端文件描述符的IO写文件事件;如果没有发生IO写事件,那么当前线程就会在函数epoll_wait中进入休眠状态,等待时间由参数timeoutMillis决定;如果其他线程向当前线程的消息队列发送一个消息,就会向当前线程关联的管道写入数据,读端文件描述符将此数据读出来,唤醒线程。
13 当我们用Handler发送一个消息,发生了什么?
答:发送一个消息,Handler可以通过sendMessage和post发送消息,最后调用的都是sendMessageDelayed,再调用sendMessageAtTime,把当前系统时间+延时时间作为发送消息的时间。接着调用enqueueMessage,将message的target设置为当前Handler对象,再调用消息队列的enqueueMessage方法将消息插入到消息队列中。
(1)定位到MessageQueue -> enqueueMessage,如果消息队列为空、插入消息的处理时间等于0、插入消息的处理时间小于消息队列表头的处理时间,将消息插入在消息队列的头部,因为表头的消息发生了变化,所以需要唤醒目标线程;如果插入消息的处理时间大于消息队列表头的处理时间,将消息插入在消息队列的某一个位置上,因为表头的消息没变化,所以不需要唤醒目标线程。
(2)定位到MessageQueue -> nativeWake,这是一个JNI方法,再调用NativeMessageQueue的wake函数来唤醒目标线程处理消息队列的新消息。
(3)定位到Looper -> wake,调用write函数向管道的写端文件描述符mWakeWritePileFd,写入一个新数据-W字符,这时目标线程会监听到管道发生了一个IO写事件而被唤醒。 --》11 (4)
14 分发给Handler的消息是怎么处理的?
答:通过MessageQueue的queue.next()分练消息后,调用msg.target.dispatchMessage(msg)把消息分发给对应的Handler;
定位到Handler -> dispatchMessage,按以下顺序分发一个消息:第一步,如果有消息的callback,说明是post方法发送的Runnable,直接回调run方法;第二步,如果在Handler构造函数就传入了Callback,回调它的handleMessage处理消息;第三步,最后回调Handler的handleMessage来处理消息。
15 如果一个Activity多个Handler时,handler发送的消息利用dispatchMessage处理时如何区分?Message消息是否会混乱?
答:不会,一句话:谁发送的消息、谁处理。因为在发送消息后,都会调用Handler的enqueueMessage,将message的target设置为当前Handler对象;然后在消息在被处理时,关键看msg.target.dispatchMessage(msg),msg调用了自身绑定的target的dispatchMessage方法来处理消息,而这里的target正是msg在被发送时绑定的handler;
Android一个Activity多个Handler时,Message消息是否会混乱?
16 IdleHandler是什么?有什么作用?应用场景是什么?原理是什么?
答:IdleHandler是一个空闲消息处理器;作用是在线程空闲时处理额外的任务,且不会阻塞主线程;应用场景是:把一些UI线程执行的耗时逻辑放在IdleHandler中执行,以此来优化页面的启动时间。。
原理是:当一个线程的消息队列为空,或者保存在消息队列表头的消息的处理时间大于系统当前时间时,线程进入空闲状态;在准备进入休眠状态前,当等待执行的IdleHandler数大于0时,遍历所有的IdleHandler回调quequIdle方法处理空闲消息;如果quequIdle方法的返回值为fasle说明只执行一次就删除,true就会被执行多次;当进入空闲消息处理后,即使有新消息进入,需要等待所有空闲消息处理完后才被执行。
/**
* IdleHandler的原理
*/
@RequiresApi(Build.VERSION_CODES.M)
fun idleHandler(view: View) {
val handler = MyHandler(Looper.getMainLooper())
handler.sendMessageDelayed(obtainSyncMessage(0x140, "延迟一秒处理的消息1"), 1000)
handler.sendMessageDelayed(obtainSyncMessage(0x140, "延迟一秒处理的消息2"), 1000)
// 发送3个空闲消息,每个消息耗时1000ms,总3000ms;进入空闲消息处理后,上面的2个延时1000ms的消息必须在3个空闲消息处理完后才被执行
handler.looper.queue.addIdleHandler(mIdleHandler)
handler.sendMessage(obtainSyncMessage(0x140, "立即执行的同步消息")) // 优先在空闲消息前处理了
handler.looper.queue.addIdleHandler(mIdleHandler)
handler.looper.queue.addIdleHandler(mIdleHandler)
// 15:43:28.721 : 接收同步信息:立即执行的同步消息
// 15:43:29.722 : 空闲时做一些骚操作 0
// 15:43:30.722 : 空闲时做一些骚操作 1
// 15:43:31.723 : 空闲时做一些骚操作 2
// 15:43:31.728 : 接收同步信息:延迟一秒处理的消息1
// 15:43:31.728 : 接收同步信息:延迟一秒处理的消息2
}
var num: Int = 0
/**
* 由于onResume()和performTraversals()本身都是在Looper的事件循环中执行,所以IdleHandler的queueIdle()方法一定会在ui绘制完毕且MessageQueue无消息处理时执行。
* 因此,把一些ui线程执行的耗时逻辑放在IdleHandler中执行,以此来优化页面的启动时间
*/
private var mIdleHandler: IdleHandler = IdleHandler {
Thread.sleep(1000)
LogUtils.e(TAG, "空闲时做一些骚操作 ${num++}")
// true会保留,每到空闲都会执行;false执行一次后会remove
false
}
17 Looper在主线程中死循环,为啥不会ANR?
答:Looper通过loop函数获得当前线程的Looper对象和消息队列,然后调用queue.next()循环不断地检查消息队列中是否有新消息需要处理,没有消息时,主线程会堵塞在nativePollOnce方法中,不需要持续的占用CPU资源,也可以保证应用的不退出。
17.1 主线程的死循环一直运行是不是特别消耗CPU资源呢?
答:涉及到Linux pipe/epoll机制,在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法中。此时主线程会释放CPU资源进入休眠状态,调用epoll_wait来监听pipe管道的读端文件描述符的IO写文件事件;如果其他线程向主线程的消息队列发送一个消息,就会向主线程的pipe管道写端文件描述符写入数据,通过读端文件描述符将此数据读出来,唤醒主线程工作。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
17.2 怎么理解 Looper.loop()的阻塞 和 UI线程上执行耗时操作卡死?
答:Looper的阻塞,前提是没有输入事件,消息队列为空,Looper进入空闲状态,线程阻塞,释放CPU执行权,等待唤醒;UI线程上执行耗时操作卡死,前提是有输入事件,消息队列不为空,Looper正常轮询,线程没有阻塞,但是该事件执行时间过长(10秒?),而且此时其他事件(按键按下、屏幕点击…)都没有被处理(卡死),然后就ANR异常了。
17.3 主线程都堵住了,怎么响应用户操作和回调Activity声明周期相关的方法?
答:Application启动时,除了main线程,还有其他两个Binder线程:ApplicationThread和ActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。当系统接收到用户操作产生的通知时,会通过Binder方式跨进程通知 ApplicationThread;然后通过Handler机制,往ActivityThread的MessageQueue中插入消息,唤醒了主线程;再通过queue.next()拿到消息回调ActivityThread.H.handleMessage()方法完成事件分发,最后便会调用到声明周期方法。
1、换个姿势,带着问题看Handler
2、Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
3、Looper.loop为什么不会阻塞掉UI线程?
4、在 Android 开发中,主线程 Looper.loop 为什么不会造成死循环?
18 Handler泄露的原因及正确写法?
答:原因是 :非静态内部类会持有一个Activity的隐式引用,当Activity销毁后,Handler依然处理延时消息,导致Activity无法被GC释放。解决方案 :弱引用 + 当外部类结束生命周期时,清空Handler内消息队列。
private static class MyHandler extends Handler {
// 创建一个弱引用持有外部类的对象
private final WeakReference<MainActivity> content;
private MyHandler(MainActivity content) {
this.content = new WeakReference<MainActivity>(content);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity= content.get();
if (activity != null) {
switch (msg.what) {
case 0: {
activity.notifyUI();
}
}
}
}}
protected void onDestroy() {
super.onDestroy();
// 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
mHandler.removeCallbacksAndMessages(null);
}
19 Handler中的同步屏障机制是什么?同步屏障使用场景?
答:Handler发送的Message后,MessageQueue的enqueueMessage()按照时间戳升序将同步消息插入到队列中,而Looper则按照顺序,每次取出一个Message进行分发,一个处理完到下一个。有一个紧急的Message需要优先处理怎么破?Handler中加入了同步屏障机制,阻碍同步消息,只让异步消息通过,实现异步消息优先执行的功能。
19.1 同步屏障机制原理是什么?
答:(1)首先,调用MessageQueue的 postSyncBarrier方法来开启同步屏障,往消息队列合适的位置插入了同步屏障类型的Message(target属性为null);
(2)然后,发送一个 异步消息(Message) 到 主线程消息队列(MessageQueue)
(3)接着,在MessageQueue执行到next()函数时:遇到target为null的Message,说明是同步屏障,循环遍历找出一条异步消息,然后处理。在同步屏障没移除前,只会处理异步消息,处理完所有的异步消息后,就会处于堵塞。
(3)最后,如果想恢复处理同步消息,需要调用 removeSyncBarrier 方法移除同步屏障:
19.2 同步屏障使用场景是什么?
答:在API 28的版本+中,postSyncBarrier()已被标注hide,但依旧可在系统源码中找到相关应用,比如:
(1)为了更快地响应UI刷新事件,在ViewRootImpl的scheduleTraversals函数中就用到了同步屏障:
(2)在 run 方法中调用了 doTraversal 方法,在 doTraversal 方法中 调用 removeSyncBarrier 方法移除同步屏障;接着调用了 performTraversals() 方法,真正的开始 DecorView(View) 绘制流程:measure –> layout –> draw
1、Android Handler机制6之MessageQueue简介
2、揭秘 Android 消息机制之同步屏障:target==null ?
3、Handler机制——同步屏障案例
20 Android 11 Handler相关变更?
答:构造函数:Handler()废弃 → Handler(Looper.myLooper());Handler(Handler.Callback callback)废弃 → Handler(Looper.myLooper(), callback)。Looper.prepareMainLooper()废弃原因:主线程的Looper是由系统自动创建的,无需用户自行调用。
21 学习链接
2、Android Handler:手把手带你深入分析 Handler机制源码
3、本人另一篇文章:Android系统分析之Android的消息机制
4、Android系统源代码情景分析(非常推荐这本书,把问题从Java层到C++层讲透,而且表达清晰)