Android系统分析之带着问题看Handler

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 学习链接

1、换个姿势,带着问题看Handler

2、Android Handler:手把手带你深入分析 Handler机制源码

3、本人另一篇文章:Android系统分析之Android的消息机制

4、Android系统源代码情景分析(非常推荐这本书,把问题从Java层到C++层讲透,而且表达清晰)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值