深入理解Handler

Handler应该是Android开发过程中使用最频繁的类了,但你真的理解Handler了吗?本文深入剖析Handler内部的实现机制,以及分析使用过程中常出现的内存泄漏的问题。本文针对使用过Handler的用户,没有再介绍Handler的使用。

Handler的用途

与Handler的相识相知,一般是通过子线程更新UI的Exception创造的机会。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Handler可以用于切换到主线程更新UI,但是它的作用绝不仅于此。源码中Handler类的注释说的很简单、明确。摘录如下,翻译水平有限:

A Handler allows you to send and process Message and Runnable objects associated with a thread's MessageQueue. Each Handler instance is associated with a single thread and that thread's message queue. When you create a new Handler, it is bound to the thread / message queue of the thread that is creating it -- from that point on, it will deliver messages and runnables to that message queue and execute them as they come out of the message queue.

Handler通过线程的MessageQueue来发送和处理Message、Runnable。每一个Handler实例关联一个特定的线程和这个线程的消息队列。当创建一个新的Handler的时候,它会绑定到创建它的线程的消息队列,从这一刻起,该Handler就会分发messages和runnables到这个消息队列,并在他们出队的时候执行他们。

There are two main uses for a Handler: (1) to schedule messages and runnables to be executed as some point in the future; and (2) to enqueue an action to be performed on a different thread than your own.

Handler的主要用途有2个:(1)延时执行(在将来某个时间调度)messages和runnables;(2)切换线程来排队处理一个动作(action)。

归结起来,Handler的用途有三个关键词:延时、切换线程、排队任务。更新UI主要是应用了Handler切换线程的功能(当然此时的排队处理也是附带在其中的),排队有时也是很重要的一个特性,例如一种场景,某些后台耗时任务需要顺序执行,此时就可以绑定一个Handler到子线程,然后发送任务,这些任务就可以顺序执行了。

那么,Handler是如何实现延时和线程切换的呢?延时是通过sleep的方式吗?

Handler的内部机制

我们从Handler的send**方法和post方法的调用开始,顺藤摸瓜,来探究其内部是如何实现线程切换和延时执行的?

所有的send**方法和post**方法最终都会调用下面两个方法中的一个:

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {

     MessageQueue queue = mQueue;

    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);

}

public final boolean sendMessageAtFrontOfQueue(Message msg) {

     MessageQueue queue = mQueue;

    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, 0);

}

可以看出这两个方法几乎是一样的,最终都会调用enqueueMessage方法:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {

    msg.target = this;

    if (mAsynchronous) {

         msg.setAsynchronous(true);

    }

    return queue.enqueueMessage(msg, uptimeMillis);

}

enqueueMessage方法没有做实际的工作,直接转到了MessageQueue的enqueueMessage方法,把一些异常判断去掉,保留基本的逻辑如下:

boolean enqueueMessage(Message msg, long when) {

    ……

     ……

     synchronized (this) {

          ……

          msg.markInUse();

          msg.when = when;

          Message p = mMessages;

          boolean needWake;

          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;

                 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.

          if (needWake) {

                 nativeWake(mPtr);

          }

      }

       return true;

}

这就是把消息插入队列,可以看到尽管MessageQueue叫做消息队列,但是它的内部实现是并不是队列,而是一个单链表的数据结构,mMessages就是链表的头Head。上面的enqueueMessage就是实现了链表的插入操作,不需要做过多的解释了。

现在消息已经放在消息队列中了,那么谁会到消息队列取消息呢?这里就不卖关子了,就是Handler中持有的Looper,Looper中的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;

    // Make sure the identity of this thread is that of the local process,

    // and keep track of what that identity token actually is.

    Binder.clearCallingIdentity();

    final long ident = Binder.clearCallingIdentity();

    for (;;) {

         Message msg = queue.next(); // might block

         if (msg == null) {

         // No message indicates that the message queue is quitting.

        return;

    }

     // This must be in a local variable, in case a UI event sets the logger

     final Printer logging = me.mLogging;

     if (logging != null) {

          logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);

     }

     final long traceTag = me.mTraceTag;

     if (traceTag != 0) {

          Trace.traceBegin(traceTag, msg.target.getTraceName(msg));

     }

     try {

          msg.target.dispatchMessage(msg);

     } finally {

         if (traceTag != 0) {

         Trace.traceEnd(traceTag);

     }

    }

    if (logging != null) {

          logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

     }

     // Make sure that during the course of dispatching the

    // identity of the thread wasn't corrupted.

    final long newIdent = Binder.clearCallingIdentity();

    if (ident != newIdent) {

        Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x"

              + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass).getName() + " " + msg.callback + " what=" + msg.what);

     }

      msg.recycleUnchecked();

   }

}

Looper的loop方法的工作过程也比较好理解,loop方法是一个死循环,唯一跳出循环的方式是MessageQueue的next方法返回了null。在此循环中,会不停的通过MessageQueue的next方法去取消息,然后通过msg.target.dispatchMessage(msg),msg.target即是Handler对象,所以msg会交给对应的Handler处理。如果MessageQueue中没有消息时,next方法会一直阻塞在那里,导致loop方法也一直阻塞。

由于,此处的loop方法运行在创建Handler时绑定的Looper(线程)上,这样就完成了将代码逻辑切换到指定的线程中去执行了。

回过头了,再来详细看一下MessageQueue的next方法和Handler的dispatchMessage方法。

Message next() {

     ……

     int pendingIdleHandlerCount = -1; // -1 only during first iteration

     int nextPollTimeoutMillis = 0;

    for (;;) {

        if (nextPollTimeoutMillis != 0) {

        Binder.flushPendingCommands();

     }

      nativePollOnce(ptr, nextPollTimeoutMillis);

     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 {

           prevMsg = msg;

           msg = msg.next;

        } while (msg != null && !msg.isAsynchronous());

     }

    if (msg != null) {

   if (now < msg.when) {

        // Next message is not ready.  Set a timeout to wake up when it is ready.

        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

    } else {

        // Got a message.

        mBlocked = false;

        if (prevMsg != null) {

             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;

 }

    // Process the quit message now that all pending messages have been handled.

   if (mQuitting) {

    dispose();

    return null;

  }

   // If first time idle, then get the number of idlers to run.

   // Idle handles only run if the queue is empty or if the first message

   // in the queue (possibly a barrier) is due to be handled in the future.

   if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {

        pendingIdleHandlerCount = mIdleHandlers.size();

    }

    if (pendingIdleHandlerCount <= 0) {

         // No idle handlers to run.  Loop and wait some more.

         mBlocked = true;

        continue;

     }

    if (mPendingIdleHandlers == null) {

        mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];

    }

     mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

  }

   // Run the idle handlers.

    // We only ever reach this code block during the first iteration.

     for (int i = 0; i < pendingIdleHandlerCount; i++) {

           final IdleHandler idler = mPendingIdleHandlers[i];

           mPendingIdleHandlers[i] = null; // release the reference to the handler

          boolean keep = false;

          try {

              keep = idler.queueIdle();

         } catch (Throwable t) {

            Log.wtf(TAG, "IdleHandler threw exception", t);

         }

        if (!keep) {

          synchronized (this) {

              mIdleHandlers.remove(idler);

          }

        }

    }

     // Reset the idle handler count to 0 so we do not run them again.

      pendingIdleHandlerCount = 0;

      // While calling an idle handler, a new message could have been delivered

      // so go back and look again for a pending message without waiting.

       nextPollTimeoutMillis = 0;

      }

}

可以看到,next方法是一个无限循环方法,如果没有消息,那么next会一直阻塞;如果有新的消息就会跳出循环,从单链表中删除这条消息,并返回它。可以看到,阻塞是通过native代码实现的,next方法里调用nativePollOnce实现阻塞,具体的也分为两种情况,有消息,但是当前消息还没到处理的时间,此时会阻塞固定的时间;还有一种情况是,消息队列已经没有消息,此时会阻塞无限长的时间,直到外部来激活它(enqueueMessage方法中的nativeWake方法),具体的细节可以参看大神罗升阳的博客Android应用程序消息处理机制(Looper、Handler)分析

此外,当当前没有消息需要处理,在进入阻塞前,会处理注册的IdleHandler接口,利用此接口也可以实现很多有价值的功能,具体可以参看Bugly公众号的一篇文章你知道Android的MessageQueue.IdleHandler吗

接下来再来看一下Handler的dispatchMessage方法:

public void dispatchMessage(Message msg) {

    if (msg.callback != null) {

         handleCallback(msg);

    } else {

         if (mCallback != null) {

         if (mCallback.handleMessage(msg)) {

             return;

         }

      }

       handleMessage(msg);

     }

}

这里就不用做过多的解释了,使用过Handler的同学都能明白,其中的handleMessage(msg)语句调用的就是我们重写的handleMessage方法。此处有一个需要讲解的点就是这里的mCallback有什么用处,Handler中的注释其实就说的很明白,此Callback接口的一个好处就是避免在实例化Handler时不得不实现其子类(也就是重写handleMessage方法),也可以通过设置callback而不去实现子类。

Handler的内部机制基本就是这样,其中有一个点说的不是很透彻,就是Looper.loop()中的final Looper me = myLooper(),是如何拿到当前线程的Looper的,简单的说,就是使用了Java中的ThreadLocal类的特性,它是一个线程内部的数据存储类,线程只能获取该线程存储的数据,而不会获取到其他线程存储的数据,后面准备单独写一篇文章来学习这个类,此处不再详细的说明,有兴趣的同学可以自行百度。

总结一下,Handler的内部机制由Handler、Looper、MessageQueue、Message共同实现了线程切换和延时执行的功能。下面的类图列出了相互的关系,各个类只列出了关键的几个public方法,切换线程的核心是目标线程中的Looper.loop方法不停的获取、处理消息队列的消息来实现的;延时执行时通过native代码的阻塞来间接实现的。

Handler的类图

其具体的流程可以简单的表示为下图:

Handler实现线程切换的流程

Handler使用过程中的内存泄漏问题

Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说,一个对象不被任何引用所指向,则该对象会在被GC发现的时候被回收;另外,如果一组对象中只包含互相的引用,而没有来自它们外部的引用(例如有两个对象A和B互相持有引用,但没有任何外部对象持有指向A或B的引用),这仍然属于不可到达,同样会被GC回收。

Android中使用Handler造成内存泄露的原因:

private Handler handler = new Handler() {     

    public void handleMessage(android.os.Message msg) {           

    if (msg.what == 1)  {               

        doSomeThing();           

     }       

} };

上面是一段简单的Handler的使用。当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。

使用Handler导致内存泄露的解决方法可以有以下两个方法:

方法一:通过程序逻辑来进行保护。

1.在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。

2.如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。

方法二:将Handler声明为静态类,内部使用弱引用持有外部类对象。

这是由于在Java 中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,静态的内部类不会持有外部类的引用。

静态类不持有外部类的对象,所以你的Activity可以随意被回收。由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference)。

这里推荐使用第二种方法,这里引出下面Handler使用的最佳实践。

最佳实践

将Handler声明为静态类后,实现如下:

private static class MyHandler extends Handler {

          private final WeakReferencemActivity;

            public MyHandler(HandlerActivity activity) {

              mActivity = new WeakReference(activity);

          }

            @Override         

           public void handleMessage(Message msg) {

                if (mActivity.get() == null) { 

                return; 

            }

            if (msg.what == 1)  {

                mActivity .get().doSomeThing(); 

          } 

        }

      }

除此之外,当Activity finish后 handler对象还是在Message中排队。 还是会处理消息,这些处理有必要?  正常Activitiy finish后,已经没有必要对消息处理,那需要怎么做呢?  解决方案也很简单,在Activity onStop或者onDestroy的时候,取消掉该Handler对象的Message和Runnable。

如果在一个大型的工程的,我们也可以实现一个基类,来规范Handler的使用,例如谷歌内置的LatinIME中实现了这样一个基类可以借鉴,所有使用Handler的地方,都继承此基类来实现具体子类。

public class LeakGuardHandlerWrapperextends Handler {   

     private final WeakReferencemOwnerInstanceRef;

    public LeakGuardHandlerWrapper(final T ownerInstance) {

    this(ownerInstance, Looper.myLooper());

    }

    public LeakGuardHandlerWrapper(final T ownerInstance, final Looper looper) {

        super(looper);

        if (ownerInstance == null) {

             throw new NullPointerException("ownerInstance is null");

        }

         mOwnerInstanceRef = new WeakReference<>(ownerInstance);

     }

    public T getOwnerInstance() {

         return mOwnerInstanceRef.get();

    }

}

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

推荐阅读更多精彩内容