拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android - Handler

一、概览是单线程消息队列。类似于过安检人Handler将行李Message放在安检机MessageQueue中传送带Looper通过不断循环将行李从安检机中取出最后由同一个人Handler取走处理。Handler通过 sendMessage() 方法发送消息 → MessageQueue通过 enqueueMessage() 方法将消息插入队列 → Looper通过 loop() 开启死循环不断轮询消息 → 通过 MessageQueue 的 next() 方法将消息取出 → 通过 Handler 的 dispatchMessage() 方法判断消息携带的是 callback 还是对象是 callback 就执行是对象就回调 handleMessage() 处理。1.1 MessageQueue 已被 DeliQueue 取代参考文章MessageQueue 一直依赖单一锁来管理主线程的任务队列这种设计经常导致锁竞争多线程同时操作队列时主线程可能会被后台线程阻塞从而导致掉帧和 UI 卡顿。而 DeliQueue 的出现说明在多核时代锁竞争已经成为不可忽视的性能瓶颈。1.1.1 问题真实案例Launcher 在拍完照回来后发生卡顿。分析发现主线程在等 MessageQueue 的锁而锁被一个后台线程持有。这个等待持续了 18ms——超过了 60Hz 刷新率下 16ms 的帧时间预算。一帧就这么丢了。更麻烦的是优先级反转。低优先级线程拿到了锁中优先级线程抢占了它的 CPU 时间片但锁还在低优先级手上高优先级的 UI 线程只能干等。三方僵持卡顿就来了。1.1.2 改进DeliQueue 无锁架构提升了性能并减少了掉帧现象但可能会导致那些利用反射访问 MessageQueue 私有字段和方法的客户端代码失效。把「写入」和「消费」彻底分开用不同的数据结构分别优化。写入端Treiber Stack、消费端Min-Heap、删除消息墓碑标记、硬件加持ARM LSE。二、Looper轮询器。每个线程中只有一个Looper切换线程和消息分发唤醒与挂起靠的是 Linux 中的 epoll 机制来实现。handler构造时可以选择是否传入 Looper 对象若不传则默认从当前线程获取若取不到则会抛出异常。这就是为什么在子线程里不能简单用消息循环的原因因为 handler 没有和 looper 建立起关联。2.1 prepareMainLooper()AMS 调用 ActivityThread.main() 进而调用 Looper.prepareMianLooper() 进而调用 parper()将主线程和 Looper 绑定然后调用 loop() 跑起来。后续 Activity 创建、输入、绘制回调都依赖该消息循环。2.2 prepare()用来将当前线程和Looper绑定。先通过 sThreadLocal.get() 看有没有 value这里是主线程是否绑定过Loopervalue不为 null 就报错不让再次赋值保证了Looper 的唯一性value为 null 就调用 sThreadLocal.set() 新建一个 Looper 对象构造函数中会创建MessageQueue对象传入赋值实现了Looper 和主线程绑定。同一线程不能调用两次会报错 RuntimeException。2.3 sThreadLocalLooper 中创建了一个 static final 的 ThreadLocal 成员变量。2.3.1 Thread该类中有一个成员变量 threadLocals 它是一个空的 ThreadLocalMap。即每个线程都有自己的ThreadLocalMap当不同线程访问代码被 ThreadLocal 操作的是它们各自持有的数据。2.3.2 ThreadLocal在一个线程中可以创建多个 ThreadLocal 对象作用是当作key存入value不管是调用 ThreadLocal 对象的 get() 还是 set() 内部都会先获取当前线程Thread这里是主线程然后获取 Thread 所持有的 ThreadLocalMap即Thread的成员变量threadLocals以该 ThreadLocal 对象这里是sThreadLocal为 key 往里面存取 value这里是Looper。由此通过 sThreadLocal 将 Looper 和主线程的绑定。2.3.3 ThreadLocalMap是 ThreadLocal 的静态内部类。Entry 继承于 WeakReferencekey 是 ThreadLocal 类型value是 Object 类型。2.4 myLooper()返回当前线程绑定的 Looper 对象如果未绑定过则返回null。2.5 loop()拿到当前线程的 Looper 对象此处为主线程通过无限 for 循环保证了不会因为代码执行完而退出 main() 函数导致程序进程结束。loopOnce()中调用 MessageQueue.next()从队列中取出 Message再调用 Message.target.dispatchMessage()分发给发送这个 Message 的 Handler 处理即发送和处理该 Message 的是同一个 Handler。四、Handler负责发送和接收处理Message。Handler 在构造时可以选择是否传入 Looper 对象若不传则默认从当前线程获取若取不到则会抛出异常。这就是为什么在子线程里不能简单用消息循环的原因因为需要 Handler 和 Looper 绑定。sendMessageAtTime( )发送消息的方法非常多 (post/send) 最终调用的都是这个uptimeMillis即系统开机时间uptimeMillis和传入的延迟时间 delayMillis 相加。handlerMessage( )由外部来重写该方法以此来处理收到的Message。dispatchMessage( )用于分发消息判断Message的callback不为null调用Runable()为null调用handleMessage()。4.1 Handler()Handler构造时需要传入一个 Looper 实现了Handler 和 Looper 绑定通过唯一的 Looper 拿到唯一的MessageQueue使得Handler 和 MessageQueue 绑定即同一个 Handler 在不同线程中发送 Message 是发到同一个 MessageQueue 中由于该 MessageQueue 所属的 Looper 所绑定的是主线程因此子线程中发送的 Message 最终是在主线程中处理。4.2 sendMessageAtTime()发送消息的方法非常多各种post/send最终调用的都是这个在确保存在 MessageQueue 后就调用 enqueueMessage() 将 Message 入列。4.3 enqueueMessage()Handler先将自己赋值给 Messgae 的 target 实现了Handler 和 Message 绑定即发送和处理该 Message 的是同一个 Handler然后调用 MessageQueue.enqueueMessage()将 Message 存入队列中。4.4 dispatchMessage()判断 Message 携带的是 callback 还是对象是 callback 就直接运行是对象就回调到 handleMessage() 中让我们在创建 Handler 对象的地方重写该方法处理。4.5 handleMessage()自定义的 Handler 子类需要重写来处理消息。回调中通过 when() 对 msg.what 来判断是哪个消息做对应处理。五、MessageQueue消息队列。每个线程中只有一个MessageQueue存放 Handler 发送来的Message。是单链表结构非线性非顺序的物理结构。采用见缝插针的存储方式不要求内存连续也不需要随机访问(只需要取头值)靠 Message中 的 next 指向下一个随机存储顺序访问。5.1 Looper.mQueueLooper构造是私有的其中会创建 MessageQueue 并赋值给 final 修饰的成员变量mQueue因此MessageQueue 也是唯一。5.2 enqueueMessage()enqueueMessage()里的同步锁让Handler 在不同线程中发送过来的 Message 同步入列并通过形参 when 来对队列中的 Message 进行排序。形参 when 给 Message 加了时间戳排序。5.3 next()next()里的同步锁与 enqueueMessage() 里的同步锁相互协作即便是在子线程发消息在主线程取消息实现了消息入列和出列的互斥性不能同时进行保证了有序性和线程安全。什么消息都没有的时候执行 nativePollOnce() 阻塞等待处于休眠状态节省CPU给其它地方。早期使用的是 wait/notifyAndroid2.3后来使用 epoll 机制为了可以同时处理 native 消息。当if(msg ! null msg.target null) 说明是消息屏障立马轮询优先执行异步消息。当 if(msg ! null) 为普通消息根据时间戳取头部执行。当非延时的 Message 都执行完的时候主线程没有重要的事情做的时候通过 for 循环处理 idleMessage。六、Message可以携带需要的数据在线程间传递。根据优先级分为异步消息 普通消息 idleMessage。消息屏障判断依据 msg.target null。发送屏障消息后会发送异步消息判断依据msg.isAsynchronous获取的是当前时间戳会加入到队列头部由于优先刷新UI。idleMessage不重要消息当所有延时为0的消息都处理完后主线程没有重要的事情做的时候即不影响主线程的时候才会去执行idleMessage。例如GC、AMS管理Activity的 stop destroy。Message的字段说明what: Int类型标识。target: Handler绑定Handler该 Message 是由同一个 Handler 发送和处理的。data: Bundle携带 Bundle 数据。arg1: Intarg2: Int携带 Int 数据。obj: Object携带可序列化数据。callback: Runnable携带 Runnable 数据Handler的post()就是将一个Runnable对象复制过来封装为Message对象。whenLong时间戳排序。next: Message指向下一个节点Message 通过 next 字段指向下一个Message从而串联成队列。enqueueMessage()、recycleUnchecked()、obtain() 操作的就是 next 的指向。6.1 Message.sPool回收后用来复用的 Message它是 static 的。MAX_POOL_SIZE 50 缓存池最多存50个超过就会 new 了。sPoolSize 0 用来记录池中数量缓存一个就1取出一个就-1。spoolSync用来做同步锁。6.2 obtain()构建 Message 实例可以通过 new 关键字推荐通过 obtain() 方法。APP中会有大量管理事件需要通过 Handler 来发送消息60Hz屏幕光UI刷新每秒就有60个Message更别其它提系统服务了使用缓存池为了避免频繁创建销毁 Message 对象内存抖动造成卡顿。6.3 recycleUnchecked()使用完的 Message 会被回收将字段携带的数据都清除掉后通过 next 将下一个节点指向当前 sPool再将自己赋值给sPool这样就像该 Message 插入到了缓存池队列头部。七、使用步骤在主线程里创建一个全局的 handler 对象并重写 handleMessage( ) 方法。在子线程里构造 message并使用 handler 将 message 发送出去。在主线程里的 handleMessage( ) 方法中判断是哪个 message 然后做相应处理。在生命周期 OnDestroy() 中调用removeCallbackAndMessage()清理。Activity { private val updateText 123 //定义Message唯一值 //【第一步】创建一个全局的Handler并重写handleMessage() private val handler object : Handler(Looper.getMainLooper()) { //【第三步】处理消息 override fun handleMessage(msg: Message) { when (msg.what) { //判断是哪个Message updateText - { msg.obj //获取携带的数据 } } } } //【第二步】创建Messgae并发送 fun show() { val str 一条消息 //方式一直接新建 val msg1 Message() msg1.what updateText msg1.obj str handler.sendMessage(msg1) //方式二避免频繁创建销毁Message推荐使用obtain() val msg2 Message.obtain() val msg3 handler.obtainMessage() //相对于上一个会自动将target字段设置为这个handler val msg4 handler.obtainMessage(updateText, str) //相对于上一个构造里就能携带what和obj字段 handler.sendMessage(msg4) //方式三利用post在子线程中更新UI利用postDelayed延时 handler.postDelayed({ //此处执行在Runnable中 }, 2000) //延迟2秒 } //【第三步】清理释放 OnDestroy() { handler.removeCallbackAndMessage() } }八、内存泄漏风险JavaKotlin无影响的原因详见闭包讲解8.1 原因非静态的内部类无法直接 new 对象需要先创建外部类对象再创建内部类对象因此内部类持有外部类的引用匿名内部类定义一个类的同时对其进行实例化同理。跳转 Activity 给 Intent 赋值的时候第一个参数会写外部类名.this 这就是持有外部类引用的很好例子。创建 Handler 对象的时候就是通过匿名内部类的方式非静态的 Handler 内部类无论是否是匿名创建便会持有外部 Activity 的引用。在 Activity 销毁时若此时消息队列中有未处理完的 Message特别是带有延时的那么 Handler 也仍然存在在enqueueMessage()的时候Handler被赋值给了Message.target所以Message持有Handler由于 Handler 持有外部 Activity 的引用那么Activity就无法正常回收GC可达性分析Activity中所有占内存的东西就成了内存泄露。8.2 解决方式一被static修饰后调用便不需要外部类的实例但是总不能把Activity中的控件全static才能操作我们通过使Handler持有Activity的一个弱引用来解决这个问题直接持有Activity的话我们便与之前的匿名内部类直接持有外部类的引用没区别了而持有了弱引用在Activity有用的情况下其会被AMS持有强引用GC不会回收而当其finish了便没有强引用对象持有了此时GC时便会回收该Activity。方式二在 onDestroy() 中调用 handler.removeCallbackAndMessage() 也可以避免内存泄漏。public class MainActivity extends Activity { // 声明成员变量 private MyHandler mHandler; // 定义消息类型 private static final int MSG_UPDATE_TEXT 1; private static final int MSG_SHOW_TOAST 2; //自定义一个静态的Handler private static class MyHandler extends Handler { private final WeakReferenceMainActivity mActivityRef; //构造中传入当前Activity并使用弱引用包装 public MyHandler(NonNull Looper looper, MainActivity activity) { super(looper); mActivityRef new WeakReference(activity); } Override public void handleMessage(NonNull Message msg) { // 去掉 super.handleMessage(msg) 因为基类中是空实现调用无意义。 // 每次处理消息对 activity 判空为空表示已被回收不执行任何操作。 Activity activity mActivityRef.get(); if (activity null) { return; } // 安全地操作UI和业务 switch (msg.what) { case MSG_UPDATE_TEXT: //TODO... break; case MSG_SHOW_TOAST: //TODO... break; default: break; } } } Override protected void onCreate(Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler new MyHandler(Looper.getMainLooper(), this); // 初始化 } Override protected void onDestroy() { super.onDestroy(); if (mHandler ! null) { mHandler.removeCallbacksAndMessages(null); } } }九、在子线程中创建HandlerHandlerThread要在子线程中创建 Handler 就在构造的时候传入子线程中的Looper进而需要拿到子线程的 ThreadLocal进而需要拿到子线程对象因此子线程不能写成匿名内部类需要定义成一个类。新创建一个子线程并不会自动建立 Looper需要在类中重写 run() 方法调用 Looper.prepare() 创建 Looper 和该线程绑定再调用 looper.loop() 将循环跑起来然后提供一个 getLooper() 方法供外部获取。外部先创建线程对象调用 start() 跑起来就初始化了Looper然后通过获取 getLooper() 的在创建 Handler 的时候传入。注意子线程在 run() 中创建Looper外部创建 Handler 时是在主线程中 getLooper() 传入的可能为null主线程拿子线程结果存在多线程并发问题。使用 sleep() 存在性能问题要使用同步锁机制。实际开发中不会手写而是使用HandlerThread。Looper在 run() 中创建在 getLooper() 中获取两个方法中的同步代码块使得创建和获取互斥保证了获取前 Looper 已经被初始化。9.1 getLooper()获取 Looper。同步代码块中的判断用来确保获取前 Looper 已经被初始化否则释放锁让线程进到 run() 方法的同步代码块中创建Looper。9.2 run()创建 Looper 并循环起来。同步代码块保证了在 getLooper( ) 中获取的安全性。可能有很多地方等着获取该 Looper 来创建 Handler因此要 notifyAll() 全部唤醒。9.3 使用记得通过 handlerThread.quitSafely() 处理完已排队消息后退出防线程泄漏。val handlerThread HandlerThread(demo) //创建HandlerThread子线程 handlerThread.start() //启动线程 val handler Handler(handlerThread.looper) { msg - //创建Handler //此处为handleMessage when (msg.what) { 123 - {} else - {} } false } handler.sendEmptyMessage(123) //发送消息十、面试10.1 Handler、Loop、MessageQuee关系?一个线程对应一个 Looper 对应一个 MessageQueue 对应无数个 Handler。10.2 非UI线程真的不能操作View吗并发访问会造成安全问题而上锁会变得复杂低效因此 UI 体系采用单线程模型系统只是限制了必须在同个线程内进行 ViewRootImpl 的创建和更新。更新 UI 会调用 View 的 requestLayout()然后开始递归查找 ViewParent最终找到顶层的DecorView。DecorView 的 ViewParent 是 ViewRootImpl 它的 requestLayout() 会调用 checkThread() 检查此线程是不是初始化自己的线程不是就抛异常。ViewRootImpl 初始化的时候会绑定当前线程而它是由在主线程的 ActivityThread 的 handleResumeActivity() 中初始化的。由调用链可知 ActivityThread 的 handleResumeActivity() 是在 Activity 生命周期 onResume() 之后执行的因此在 onCreate()、onStart()、onResume() 时 ViewRootImpl 还未初始化也就可以在子线程中更新了。public void click(View view) { new Thread(new Runnable() { public void run() { Looper.prepare(); boolean b1 Looper.myLooper() Looper.getMainLooper(); //fale当前线程不是主线程 Toast.makeText(Drawer99_Test_Activity.this,b1,b2,Toast.LENGTH_LONG).show(); Looper.loop(); } }).start(); }10.3 为什么不用wait/notify而用epoll早期使用的是 wait/notifyAndroid2.3后来使用 epoll 机制为了可以同时处理 native 消息。10.4 View.post() 和 Handler.post()的区别View.post() 最终调用的是 Handler.post()。10.5 如何进行线程切换的子线程持有的 Handler 如果绑定到的是主线程的 Looper (Handler创建的时候需要传入Looper) 的话那么子线程发送的 Message 就可以由主线程来消费以此来实现线程切换执行 UI 更新操作等目的。10.6 主线程 Looper.loop() 为什么不会导致 ANR无消息时阻塞等待epoll/nativePollOnce有消息时唤醒处理。loop() 循环本身不会导致 ANR会出现 ANR 是因为在 loop 循环之内 Message 处理时间过长死锁、同步 IO、重计算后续输入事件分发超时被判定无响应导致。让程序不退出的话写一个死循环那么main方法中的代码永远不会执行完这样程序就不会自己退出了。正是因为Looper.loop方法这个死循环它阻塞了主线程所以我们的app才不会退出。本质上Android就是事件驱动的程序界面刷新也好交互也好本质上都是事件这些事件最后通通被作为了Message发送到了MessageQueue中。由Looper来进行分发然后在进行处理。也就是我们的Android程序就是运行在这个死循环中的一旦这个死循环结束app也就结束了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门