handler 理解

  • 简述

    当接触一个新事物的时候我们要去了解它,认知过程一般是三个为题:

    1. 这个事物是什么?
    2. 它为什么要这么做?
    3. 它是怎样做到的?
  • 是什么?

    handler 就是Google设计的一个线程之间通讯的工具,实现线程之间的切换工作。

  • 为什么要这么做?

    这要从Google定制的规则来说起了,Google提出,UI更新一定要在UI线程中实现,这样做就是提高移动端的效率用使用体验。android 的UI线程是采用非安全线程(单线程模型)来实现的,
    这样保证了UI更新的流畅,提升了用户体验,但是非安全线程就会出现多个线程同时操作UI的情况,这种情况下UI会容易出现不可控的错误,所以Google
    让所有更新UI的动作都在主线程(ActivityThread),这样就不会出现一些不可控的错误,并且在UI线程中不能耗时,耗时的话就可能阻塞UI线程,
    UI得不到及时更新,出现卡顿,严重的直接出现"ANR"错误,说到底这一切的设计都是为了用户体验。

    这样就出现一个矛盾点:有时候确实需要做一些耗时的动作,完成动作后还需要更新UI,这些耗时动作只能在子线程中进行,更新UI又需要到UI线程,
    所以为了解决矛盾,Handler的设计就出现了。

  • 怎么做到的?

    打开handler源码就能看到handler的介绍:


    handler介绍.jpg

    大致意思是:

    Handler是用来结合线程的消息队列来发送、处理“Message对象”和“Runnable对象”的工具。
    每一个Handler实例之后会关联一个线程和该线程的消息队列。
    当你创建一个Handler的时候,从这时开始,它就会自动关联到所在的线程/消息队列,
    然后它就会陆续把Message/Runnalbe分发到消息队列,并在它们出队的时候处理掉。

    Handler有两个主要用途:

      1. 推送未来某个时间点将要执行的Message或者Runnable到消息队列。
      2. 在子线程把需要在另一个线程执行的操作加入到消息队列中去。
    

    为应用程序创建进程时,其主线程专用于运行消息队列,
    该队列负责管理顶级应用程序对象(活动,广播接收器等)及其创建的任何窗口。
    您可以创建自己的线程,并通过Handler与主应用程序线程进行通信。
    这是通过调用与以前相同的post或sendMessage方法完成的,但是来自您的新线程。
    然后,将在Handler的消息队列中调度给定的Runnable或Message,并在适当时进行处理。

    • Handler工作原理

handler 主要职责就是发送消息与处理消息,一下是发送消息最终调用源码。

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
           MessageQueue queue = mQueue;
           //判断MessageQueue是否为空
           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, uptimeMillis);
       }
   
       private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            //将 Handler 自身赋值给了 msg.target
           msg.target = this;
           if (mAsynchronous) {
               msg.setAsynchronous(true);
           }
           // 调用MessageQueue 的 enqueueMessage方法,将 Message 添加到消息队列
           return queue.enqueueMessage(msg, uptimeMillis);
       }

怎么做到的

 public void dispatchMessage(Message msg) {
        //检查 Message 的 callback 是否为 null
         if (msg.callback != null) {
             //不为 null 直接通过 handleCallback 来处理消息
             handleCallback(msg);
         } else {
             if (mCallback != null) {
                 if (mCallback.handleMessage(msg)) {
                     return;
                 }
             }
             //callback为空并且mCallback 为空的情况下调用handleMessage()方法,
             //这个方法通常就是我们实现的处理方法
             handleMessage(msg);
         }
     }
  • MessageQueue 消息队列工作原理
    MessageQueue 就是一个装Message的容器,这是最直接的理解,既然是容器,其职责肯定是添加和移除消息了。上面介绍handler工作原理的时候
    提到handler发送消息的最后是调用MessageQueue的enqueueMessage方法来将消息添加到消息队列里面的,以下是实际添加message方法源码:
boolean enqueueMessage(Message msg, long when) {
           if (msg.target == null) {
               throw new IllegalArgumentException("Message must have a target.");
           }
           if (msg.isInUse()) {
               throw new IllegalStateException(msg + " This message is already in use.");
           }
            //先持有MessageQueue.this锁
           synchronized (this) {
               if (mQuitting) {
                   IllegalStateException e = new IllegalStateException(
                           msg.target + " sending message to a Handler on a dead thread");
                   Log.w(TAG, e.getMessage(), e);
                   msg.recycle();
                   return false;
               }
   
               msg.markInUse();
               msg.when = when;
               Message p = mMessages;
               boolean needWake;
               
               //如果队列为空,或者当前处理的时间点为0(when的数值,when表示Message将要执行的时间点),
               //或者当前Message需要处理的时间点先于队列中的首节点,那么就将Message放入队列首部
               
               if (p == null || when == 0 || when < p.when) {
                   // New head, wake up the event queue if blocked.
                   msg.next = p;
                   mMessages = msg;
                   needWake = mBlocked;
               } else {
                   // Inserted within the middle of the queue.  Usually we don't have to wake
                   // up the event queue unless there is a barrier at the head of the queue
                   // and the message is the earliest asynchronous message in the queue.
                   needWake = mBlocked && p.target == null && msg.isAsynchronous();
                   Message prev;
                   
                   // 遍历队列中Message,找到when比当前Message的when大的Message,
                   //将Message插入到该Message之前,如果没找到则将Message插入到队列最后
                   for (;;) {
                       prev = p;
                       p = p.next;
                       if (p == null || when < p.when) {
                           break;
                       }
                       if (needWake && p.isAsynchronous()) {
                           needWake = false;
                       }
                   }
                   msg.next = p; // invariant: p == prev.next
                   prev.next = msg;
               }
   
               // We can assume mPtr != 0 because mQuitting is false.
               //判断是否需要唤醒,一般是当前队列为空的情况下,next那边会进入睡眠,需要enqueue这边唤醒next函数
               if (needWake) {
                   nativeWake(mPtr);
               }
           }
           return true;
   }
  • Looper的工作原理
    Looper 的作用就是不断的查询MessageQueue中是否有消息,如果有消息就取出消息给handler处理,如果没有消息就阻塞住。
    private Looper(boolean quitAllowed) {
            mQueue = new MessageQueue(quitAllowed);
            mThread = Thread.currentThread();
    }

Looper 的构造函数中创建了一个可MessageQueue 并且将当前的线程保存起来。Looper通过Looper.loop()开启循环来查看是否有消息。

   /**
    * Run the message queue in this thread. Be sure to call
    * {@link #quit()} to end the loop.
    * 通过调用quit() 去结束loop
    */
    
   public static void loop() {
           final Looper me = myLooper();
           if (me == null) {
               throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
           }
           //...
           final MessageQueue queue = me.mQueue;
           //...
           for (;;) {
               Message msg = queue.next(); // 调用 MessageQueue 的 next 方法获取 Message
               if (msg == null) {
                   // No message indicates that the message queue is quitting.
                   // msg == null 表示   MessageQueue 正在退出(调用了quit等方法)
                   return;
               }
               //....
                try {
                   // msg.target 就是发送消息的Handler,因此这里将消息交回 Handler
                   msg.target.dispatchMessage(msg);
                   end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
               } finally {
                   if (traceTag != 0) {
                       Trace.traceEnd(traceTag);
                   }
               }
               //....
           }
       }

注意到loop()中有一个死循环,死循环结束的条件是msg==null,怎样让msg==null呢?调用MessageQueue的quit方法使msg为空。

是否想过一个问题,主线程Looper中运行一个死循环为什么不会出现ANR?

首先要明白一点,Java的线程是允许休眠的。

Android在主线程中是开启了一个阻塞式死循环,保证程序一直运行,什么是阻塞式死循环,简单的理解就是:
有事务就去处理,没有的话就休眠。阻塞式循环的实现是在Linux中实现的(epoll和inotify机制)。ANR的本质原因是因为MessageQueue中的事件没能够得到及时的处理,
并不是死循环取事件造成,所以死循环并不是ANR的必然原因。
如果我们自己在主线程写死循环会怎样?答案是,必然ANR,原因是我们的死循环进入了不会阻塞,不会释放cup资源,后面的事件无法处理,Looper的死循环属于可阻塞式死循环。

  • 看完handler源码我们应该很简单的回答下面问题了。
  1. 在UI线程中有几个Looper对象?有几个MessageQueue对象?有几个Handler对象?有几个Message对象?

    一个线程中只有一个Looper对象,一个MessageQueue对象,但是可以有多个Handler对象和多个Message对象。

  2. 怎么保证只有一个Looper对象的?

    在handler体系中Looper对象出现在的地方有三个,一是在ActivityThread类中使用Looper.prepareMainLooper()创建Looper对象,
    二是在hanlder类中使用Looper对象,三是在Looper的loop方法中使用Looper对象处理消息。而这三个Looper都是从ThreadLocal中取到,
    ThreadLocal是Looper类的静态字段,所以只有一个ThreadLocal对象。 prepare方法在UI线程被调用,所以只有在Ui线程才能从ThreadLocal对象中获取到looper对象。

  3. 怎么保证只有一个MessageQueue对象的?
    首先要明白,MessageQueue对象产生的地方,在Looper的构造方法中会产生MessageQueue对象,而上一题已经说明一个线程中只有一个Looper,所以对应的MessageQueue也只有一个。

  4. 为什么发送消息在子线程,而处理消息就变成主线程了,在哪儿跳转的?

    看源码我们知道,handler发消息最终是调用的MessageQueue中的 enqueueMessage 方法,这个方法就将子线程中的消息提交到了MessageQueue中了,而MessageQueue与Looper是依赖关系的,
    处于同一线程,这个线程又是UI线程,所以消息取出的时候就跳转到UI线程中了。

  5. looper对象只有一个,在分发消息时怎么区分不同的handler?

    Message类中有个字段target,这个字段是Handler类型的,Handler的enqueueMessage方法中有这么一句代码:msg.target = this;即把handler对象与Message绑定在了一起。
    Looper类的looper方法中分发消息的代码是:msg.target.dispatchMessage(msg)。在发送消息时handler对象与Message对象绑定在了一起。
    在分发消息时首先取出Message对象,然后就可以得到与它绑定在一起的Handler对象了。

  6. 能不能在子线程中创建Handler对象?

    子线程中是能创建handler的,就像之前提到的handler只是线程间的通讯工具,我们所说的UI线程也只是一个普通的线程,只是UI线程中已经存在Looper对象,
    在子线程中创建Handler 需要依赖MessageQueue,因为handler发送消息是依赖messageQueue中的enqueueMessage,而MessageQueue又是依赖于Looper的,
    所以在子线程中创建Handler只要保证子线程中存在Looper,从handler构造函数也可以看出如果Looper为空的话是会直接报异常的。

  7. 怎么在子线程中得到主线程中handler对象?

    handler其实与线程是无关的,handler依赖的Looper与线程有关,当Looper是主线程中的Looper,handler就会是主线程中的,handler有一个构造函数是需要传入一个Looper对象,
    这个对象就能确定handler所属线程。

  8. Handler导致内存泄漏的根本原因是什么?
    由上面的讲解知道主线程中的Looper,其生命周期应该是伴随整个应用的生命周期的,可以理解为与Application 生命周期一致,在使用handler的时候,handler 发送消息,其Message是
    是对handler有引用关系的 "Handler的enqueueMessage方法中有这么一句代码:msg.target = this,即把handler对象与Message绑定在了一起",而Message的存放取出与Looper有这紧密关系
    引用关系链:handler ---->> Message ------>> MessageQueue ----->> Looper ,主线程中Looper 的生命周期是整个应用的生命周期,如果我们的handler对activity 有引用,
    那么当activity结束生命周期的时候(activity应该被回收),由于Looper 还在执行message的相关操作,导致handler无法释放对activity的引用,最后activity一直无法释放出现内存泄漏(引用链的存在,GC不会去回收).

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容