「带着问题看Handler」

「带着问题看Handler」

一、写在前面

Android系统在设计之初被设计成单线程模型,其实不仅仅是Android,像Java Swing都使用了单线程模型,这也是有历史原因的。回到Android中,都知道只能在UI线程也即主线程中更新UI,并且规定了UI线程中不能够进行耗时操作(5S),像网络请求,I/O操作等。否则会出现ANR,程序无响应。作为开发这是最不愿意看的。那么实际开发中肯定也会有需要在子线程更新UI的场景,可以说Handler也是为了解决这一矛盾的-只能在UI线程中更新UI。官方注释中同样给出了Handler两大主要作用:

1.To schedule messages and runnables to be executed at some point in the future;

2.To enqueue an action to be performed on a different thread than your own.

二、带着问题看

Android上线源码阅读工具(Android Code Search)后查看源码相比于之前算是极大的方便了,可以看到最新版本的源码信息,同样也支持调用之间的跳转;几乎跟Android Studio相差无几了。如图,可以根据需要选择想要查看的源码版本:

Android_code.png

就自身而言,无论是Android的系统源码还是其他优秀开源库的源码的阅读,在准备阅读之前都会整理出自己想要了解的方向或者问题,这样带着问题去看,优先看与问题相关的内容。个人认识还是比较高效的,毕竟系统源码设计细节太过于复杂,深陷细节当中往往达不到整体把握的效果。那么常见的问题或者说作为上层App开发应该需要掌握哪些内用与细节呢?

三、常见问题

1.一个线程有几个Handler?

2.一个线程有几个Looper?如何保证?

3.一个线程可以有几个MessageQueue?

4.如何创建一个Message对象?

5.Handler内存泄漏的根本原因?内部类是不是都会导致内存泄漏?

6.主线程中如何实例化一个Handler对象,子线程中呢?

7.Looper死循环为什么不会导致应用ANR,会消耗资源吗?

8.子线程中维护Looper,消息队列无消息时如何处理?有什么用?

9.使用Handler的postDelay后消息队列的变化?

10.多个Handler如何保证线程安全?

11.什么是同步屏障,有什么作用,同步屏障与异步消息的具体使用场景?

12.Android IdleHandler应用场景?

13.可以多次创建Looper对象嘛?

14.MessageQueue作用,数据结构?

15.延迟消息的实现?

16.pipe/epoll机制?

简单的总结了一些需要掌握的问题,带着这些问题看源码的具体实现,做到心中有数。当然重点还是要关注HandlerMessageLooper这套机制的具体实现细节。基于源码Android-12.0.0_r4

1.Handler具体实现

消息的的处理类,主要的职责是发送消息到“消息队列”,并处理消息。

  • 构造函数:
@Deprecated
public Handler() {
  this(null, false);
}

@Deprecated
public Handler(@Nullable Callback callback) {
  this(callback, fasle);
}

public Handler(@NonNull Looper looper) {
  this(looper, null, false);
}

public Handler(@NonNull Looper looper, @Nullable Callback callback) {
  this(looper, callback, false);
}

@UnsupportedAppUsage
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
  mLooper = looper;
  mQueue = looper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
}
//.....

在最新的源码之中,关于构造函数中不带Looper参数的方法已经被打上@Deprecated的标签,官方已经不建议这样实例化出一个Handler对象(隐式的指定Looper对象),源码给出的解释:

Implicitly choosing a Looper during Handler construction can lead to bugs where operations are silently lost (if the Handler is not expecting new tasks and quits) crashes .

先不具体深究,继续往下看Handler其他比较重要的方法有哪些。

  • 方法处理handleMessage()
//Subclasses must implement this to receive messages.
public void handleMessage(@NonNull Message msg) {
  //空方法,实现类需要处理用来接收信息
}
//Callback interface you can use when instantiating a Handler to avoid 
//having to implement your own subclass of Handler.
public interface Callback {
  boolean handleMessage(@NonNull Message msg);
}

方法处理比较简单,一个Callback接口,当然可以在实例化Handler的作为参数传入,同时包含一个handleMessage的空方法,需要子类自己实现具体的消息处理细节。

  • 消息的分发
//Handle system messages here.
public void dispatchMessage(@NonNull Message msg) {
  if (msg.callback != null) {
      handleCallback(msg);
  } else {
      if (mCallback != null) {
         if (mCallback.handleMessage(msg)) {
             return;
         }
       }
    handleMessage(msg);
  }
}

public final boolean post(@NonNull Runnable r) {
  return  sendMessageDelayed(getPostMessage(r), 0);
}
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {
  return sendMessageAtTime(getPostMessage(r), uptimeMillis);
}
public final boolean postAtTime(@NonNull Runnable r, @Nullable Object token, long uptimeMillis) {
  return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
}
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
  return sendMessageDelayed(getPostMessage(r), delayMillis);
}
//@hide
public final boolean postDelayed(Runnable r, int what, long delayMillis) {
  return sendMessageDelayed(getPostMessage(r).setWhat(what), delayMillis);
}

public final boolean sendMessage(@NonNull Message msg) {
  return sendMessageDelayed(msg, 0);
}
public final boolean sendEmptyMessage(int what) {
  return sendEmptyMessageDelayed(what, 0);
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
   Message msg = Message.obtain();
   msg.what = what;
   return sendMessageDelayed(msg, delayMillis);
}
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
   Message msg = Message.obtain();
   msg.what = what;
   return sendMessageAtTime(msg, uptimeMillis);
}

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
   if (delayMillis < 0) {
      delayMillis = 0;
   }
   return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(@NonNull 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);
 }

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
    long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
    if (mAsynchronous) {
       msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
 }

无论是postXX()还是sendXX()分发消息最终都会回调到方法enqueueMessage,不同的是延时的时间的大小,当然这个延时的时间是根据时间戳来,系统未休眠时间节点与延时时间的和。当用户给定的时间跨度小于零时会被置为零。这里的时间取值同样决定了,如果想要实现精确的时间控制,完全依赖delayMillis是不够的,看代码:

if(delayMillis < 0) {
  delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
//milliseconds since boot, not counting time spent in deep sleep.
native public static long uptimeMillis();

继续分析,发现除了dispatchMessage(),其他分发消息的方法,消息最终都走到了queue.enqueueMessage()中,也即是被加入到消息队列当中,引出Handler机制中另一个重要成员-消息队列MessageQueue。那么这个dispatchMessage()为什么逻辑是不一样的,而且注释信息也比较简单Handle system messages here.。到这里对Handler已经有个大致的了解了,主要有几个作用:

1.Handler在实例化时,隐式的指定Looper对象官方已经不在推荐使用,具体是实现看Looper内部实现。

2.消息的处理handleMessage(),包含两种方式,实现内部接口Callback,或者实现空方法handleMessage()

3.消息的分发主要是两大类,dispatchMessage()postXX()sendXX();其中dispatchMessage比较特殊,而其他消息均被加入到消息队列MessageQueue中,具体实现细节在内部。

回到问题1.一个线程有几个Handler?, 根据源码,以主线程为例,实例化Handler对象并不会限制。也就是说,一个线程可以有多个Handler对象。

实例化Handler.png
2.MessageQueue具体实现

Low-level class holding the list of messages to be dispatched by a {@link Looper}. Messages are not added directly to a MessageQueue.

持有了一组消息Message,并且可以通过Looper来分发,其次Message并不是直接被加到队列当中去的。既然是“消息队列”,那么具体看看是怎样实现的。

  • 主要成员参数
public final class MessageQueue {
  @UnsupportedAppUsage
  private final boolean mQuitAllowed;
  
  @UnsupportedAppUsage
  Message mMessages;
  
  @UnsupportedAppUsage
  private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
  private boolean mQuitting;
  private boolean mBlocked;
  
  private native static long nativeInit();
  @UnsupportedAppUsage
  private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
  //......
}

MessageQueue中持有了Message mMessages,到这里即使不看Message中的具体实现,大致也可以猜到,“消息队列”实际上是以链表的形式将Message组合在一起,那为什么选择这样的结构?而不用数组呢?在Handler中,当通过发送消息时发现是可以指定延时时间的,而消息是被插入到消息队列当中。链表的性质就被体现出来了,插入快。如果是数组实现消息队列那么扩容时,会涉及数组内容的复制,效率上是不如链表。其次,消息的插入操作在Handler中很频繁。使用链表更加合适。回到问题14.MessageQueue作用,数据结构?:

MessageQueue使用了链表结构,应对Message被频繁的插入,有利于性能的提升。主要作用是将消息按照一定的顺序组合,提供给Looper分发。

  • 关键方法-添加消息到队列enqueueMessage()
 boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
      throw new IllegalArgumentException("Message must have a target.");
    }
    synchronized (this) {
      if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
      }
      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;
      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;
 }

方法流程不是很复杂,一步步分析。对于待添加的消息Message,首先就判断了Message.target是否为空,为空的话直接抛出异常。然后进入到同步方法中,判断消息是否正在被使用中以及MessageQueue是否已经退出。如果已经退出,那么消息会被回收并返回。那么插入消息的规则是什么呢?看代码:

Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
  //New head, wake up the event queue if blocked.
  msg.next = p;
  mMessages = msg;
}

假设当前消息队列mMessage为空,自然的待插入的消息Message会作为队列的头部即msg.next = p(null);如果p不为空,看第二个条件when == 0。由Handler知道,这个when其实就是被延迟的时间。但是延迟时间不是系统时间与delayMillis的和嘛,为什么会为0?回到Handler中:

public final boolean sendMessageAtFrontOfQueue(@NonNull 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;
     }
    //when 是可以等于0的,
    return enqueueMessage(queue, msg, 0);
}

但无论如何,结合when == 0 || when < p.when,也即是根据延迟的时间跨度来决定消息是否可以作为新的消息队列头部Head。总结得出:

1.消息队列为空,那么新消息直接作为头部Head。

2.消息队列不为空,延迟间隔为0,新消息作为头部。

3.消息队列不为空,延迟间隔大于0,且延迟时间间隔小于原队列p.when,那么新消息作为头部。

接下来看看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.

else {
  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;
}

先搞清楚几个节点,prevcurrentnext。当前消息队列为mMessages,被添加的消息msg,通过死循环遍历消息链表。而需要注意的是循环退出的条件if(p == null || when < p.when)这里同样分两种情况来讨论:

1.当没有遍历到链表的尾部时,即p不为null,此时比较的是延迟时间when如果when < p.when,那么msg应该被插入到p之前。

2.p == null时,那么即表示已经到链表的尾了,此时循环跳出 msg.next = p; prev.next = msg.这里msg其实是被放到了链表的尾部了。

3.看needWake的赋值操作,needWake = mBlocked && p.target == null && msg.isAsynchronous();这个条件判断是比较严格的,什么时候才会唤醒队列呢?队列是睡眠状态并且队列的第一条消息的target为空(Handler),待插入的msg为异步消息时才会唤醒队列。

tips:这个循环其实是有点绕的,需要理解的是每次遍历之前都更新了prev的值,且将头节点后移了一个p = p.next;然后开始遍历,当条件满足插入的位置时msg的前后节点都可以对应上。通俗的讲,这个prev其实就是msg的prev。需要理解的是p与prev在遍历的过程中是不断变化的。

msg消息插入示意图:

message插入.png

  • 关键方法-取消息Message next()
  Message next() {
    final long ptr = mPtr;
    if (ptr == 0) {
      return null;
    }
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (; ; ) {
      if (nextPollTimeoutMillis != 0) {
        Binder.flushPendingCommands();
      }
      //method 1
      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;
        //method 2
        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());
        }
        //method 3
        if (msg != null) {
          if (now < msg.when) {
            //method 4
            // 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 {
            //method 5
            // Got a message.
            mBlocked = false;
            if (prevMsg != null) {
              prevMsg.next = msg.next;
            } else {
              mMessages = msg.next;
            }
            msg.next = null;
            msg.markInUse();
            return msg;
          }
        } else {
          //method 6
          nextPollTimeoutMillis = -1;
        }
        if (mQuitting) {
          dispose();
          return null;
        }
        if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
          pendingIdleHandlerCount = mIdleHandlers.size();
        }
        if (pendingIdleHandlerCount <= 0) {
          mBlocked = true;
          continue;
        }
        if (mPendingIdleHandlers == null) {
          mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
      }
      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) {
        }
        if (!keep) {
          synchronized (this) {
            mIdleHandlers.remove(idler);
          }
        }
      }
      pendingIdleHandlerCount = 0;
      nextPollTimeoutMillis = 0;
    }
  }

Message的取操作代码很长,另外发现这个取操作是一个死循环?那么问题来了,这样不会造成CPU资源浪费嘛,也没有出现ANR的情况,这是为什么呢?在了解这些之前首先需要了解一个概念-pipe/epoll机制,这其实属于Linux管道方面的知识,通俗的讲,当有消息时就会唤醒操作,如果没有消息就会阻塞,并且释放了CPU资源。具体的描述看官方的解释。epoll_wait(),对于阻塞机制与唤醒规则,给出了如下解释:

Note that the timeout interval will be rounded up to the system clock granularity, and kernel scheduling delays mean that the blocking interval may overrun by a small amount.  Specifying a
timeout of -1 causes epoll_wait() to block indefinitely, while specifying a timeout equal to zero cause epoll_wait() to return immediately, even if no events are available.
//请注意,超时间隔将四舍五入到系统时钟粒度,内核调度延迟意味着阻塞间隔可能会超过少量。指定一个
//-1的超时会导致epoll_wait()无限期阻止,而指定等于零的超时会导致epoll_wait()立即返回,即使没有可用的事件。

注意几个关键的点:

1.当超时的时间为-1时,会一直阻塞(即next()方法中死循环会被一直阻塞的原因)。

2.等于0时,会立即返回结果。

3.大于0时,则达到超时时间时返回结果,但是这个结果是不精确的,由于系统调度的操作加上一些方法执行时间,这个时间存在四舍五入的可能(这个需要理解的点,后面的问题会解释)。

从标注的method 1-7逐步分析,method 1的方法nativePollOnce,前面已经解释了,是Linux中管道的一种唤醒机制,当唤醒超时时长为0时表示不阻塞直接返回结果;nextPollTimeoutMillis = 0默认就是0。方法method 2其实可以看出对同步屏障消息的定义,即msg不为空,而msg.target (handler)为空。如果遇到同步屏障,则进入do while循环当中获取队列中第一条异步消息。如果不存在异步消息,当走到method 3此时msg还是队列的头部。简单的说,如果是同步屏障则取队列中第一条异步消息,否则取队列头消息。这个很好理解,按照优先顺序消息的获取自然是从头部开始,但是如果有同步屏障的情况下,异步消息会优先执行。方法method 4对延迟信息与当前的时间间隔做了对比,判断是否已经到执行时间。如果没有到达执行时间则更新nextPollTimeoutMillis的值。反之到method 5获取消息并返回处理。但是这里需要注意的是,对是否是同步屏障消息做了判断prevMsg != null 这个判断是为了确定消息头head下次遍历的顺序。方法method 6如果没有获取到消息则设置nextPollTimeoutMillis = -1,前面提到的管道机制,如果为-1则会进入阻塞等待让出CPU资源(当然在假设idle handler没有被设置的前提下)。对整个总结如下:

1.MessageQueueMessage的获取是一个死循环,但是内部才有了Linuxpipe/epoll机制,此机制可根据超时时长控制阻塞与唤醒,当为-1时表示阻塞等待唤醒,当为0时则立即返回结果;当大于0时则到达指定时间间隔时唤醒。但是由于内核的调度耗时,这个时间的精度是不准确的,存在四舍五入的情况。

2.如果消息队列中存在同步屏障,那么优先获取第一条异步消息来执行。如果不存在异步消息,那么还是按照队列消息依次获取。

3.获取的msg会与当前SystemClock.uptimeMillis()时间戳作对比判断是立即执行还是延迟到指定时间执行。

4.当队列中消息为空是且没有设置IdleHandler的情况下重置参数nextPollTimeoutMillis为-1以阻塞让出CPU的资源并等待消息唤醒。

3.Looper的具体实现

Class used to run a message loop for a thread. Threads by default do not have a message loop associated with them.

用来线程开启消息循环的类,线程默认是没有与其关联的消息循环的。获取到的消息交由Handler处理。

Handler机制中,Looper相当于一个传送带,不断的从MessageQueue取消息分发给Handler处理。主要参数有:

@UnsupportedAppUsage
//sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
@UnsupportedAppUsage
final MessageQueue mQueue;
final Thread mThread;

@UnsupportedAppUsage
private static Looper sMainLooper;  // guarded by Looper.class

参数并不是很多,持有了一个消息队列MessageQueue,并且使用了TLS作为对象的保存容器,至于TLS的特性就不用多说了。保证了与线程的一一对应关系。

  • 开启Looper
public static void prepare() {
  prepare(true);
}
private static void prepare(boolean quitAllowed) {
  if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
  }
  sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(boolean quitAllowed) {
  mQueue = new MessageQueue(quitAllowed);
  mThread = Thread.currentThread();
}

当调用prepare()方法实例化一个Looper对象时,注意的细节,首先判断sThreadLocal内是否已经有包含有对象,如果有则直接抛出异常,没有则实例化一个存入到TLS当中,使用当前线程作为key。回到之前提到的问题一个线程有几个Looper?如何保证?,那么这里就可以解释了,一个线程只有一个Looper对像,内容使用TLS存储对象保证了唯一性。在Android,主线程的Looper其实已经为我们实例好了,那么如果在子线程中开启一个Loop循环应该如何操作呢?官方给出了模版:

class LooperThread extends Thread {
  public Handler mHandler;
  public void run() {
    Looper.prepare();
    mHandler = new Handler() {
      public void handleMessage(Message msg) {
        // process incoming messages here
      }
    };
    Looper.loop();
  }
}

也就是说,如果想要在子线程中使用Looper必须手动开启Loop循环Looper.prepare()->Looper.loop()

  • 循环取消息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.");
  }
  if (me.mInLoop) {
    //code
  }
  me.mInLoop = true;
  final MessageQueue queue = me.mQueue;
  for (;;) {
    Message msg = queue.next(); // might block
    //....
    try {
      msg.target.dispatchMessage(msg);
    } catch {}
  }
}

重要的方法loop()实现比较简单,也是一个死循环,之前已经分析了在MessageQueue之中的next()获取消息的整个过程,这里主要看看获取到Message对象之后的操作,代码很简单-msg.target.dispatchMessage(msg)。直接通过targer分发出去了。那么这个target是什么呢?看Message类中代码:

public final class Message implements Parcelable {
  public int what;
  public int arg1;
  public int arg2;
  public Object obj;
  /*package*/ Bundle data;
   /*package*/ Handler target;
  /*package*/ Message next;
  /*package*/ Runnable callback;
}

到这里已经知道了target其实就是内部持有的Handler对像,那么这个target是何时被赋值的呢?看构造Message的方法:

public static Message obtain() {
  synchronized (sPoolSync) {
     if (sPool != null) {
         Message m = sPool;
         Pool = m.next;
         m.next = null;
         m.flags = 0; // clear in-use flag
         sPoolSize--;
         return m;
      }
   }
   return new Message();
}

public static Message obtain(Handler h) {
  Message m = obtain();
  m.target = h;
  return m;
}

public static Message obtain(Handler h, int what) {
  Message m = obtain();
  m.target = h;
  m.what = what;
  return m;
}
//.......

Message内部使用了sPool,而获取Message时推荐的做法是通过obtain,这是使用了池化的思想,对象的不断创建是很耗性能的。通过复用池中的Message对象提升效率。其次通过obtain参数同样知道,这个target就是发送消息的Handler本身。到这里就明白了,谁发送消息,则由谁处理消息。当HandlerUI线程中被创建,即使在子线程中发送消息,而msg.target还是这个Handler本身,也就是还是由本身处理;则消息就从子线程转发到了UI线程当中。当然是可以做更新UI操作的。如果Handler在子线程中被创建,而从主线程中发送消息,那么自然的消息将会在子线程中被处理,也就是完成了线程的切换操作。但是之前提到的handler.dispatchMessage()用来发送消息结果会怎样呢?看一下测试用例:

/**
 * Created by Sai
 * on 2022/1/19 09:21.
 * Description:
 */
public class HandlerThread extends Handler {

    @Override
    public void handleMessage(@NonNull Message msg) {
        Log.d("HandlerThread", "Handler:" + Thread.currentThread().getId() + " & arg1=" + msg.arg1);
        super.handleMessage(msg);
    }
}

public class MainActivity extends AppCompatActivity {
  private final HandlerThread handlerTest = new HandlerThread();
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.d("HandlerThread", "Main:" + Thread.currentThread().getId());
    
    Message msg3 = handlerTest.obtainMessage();
    msg3.arg1 = 3;
    handlerTest.dispatchMessage(msg3);
    test();
  }
  private void test() {
   new Thread(() -> {
       Log.d("HandlerThread", "Thread:" + Thread.currentThread().getId());
       Message msg = handlerTest.obtainMessage();
       msg.arg1 = 1;
       handlerTest.dispatchMessage(msg);

       Message msg2 = handlerTest.obtainMessage();
       msg2.arg1 = 2;
       handlerTest.sendMessage(msg2);
       }).start();
    }
}

打印的Log信息:

com.handlerapp D/HandlerThread: Main:2
com.handlerapp D/HandlerThread: Handler:2 & arg1=3
com.handlerapp D/HandlerThread: Thread:422
com.handlerapp D/HandlerThread: Handler:422 & arg1=1
com.handlerapp D/HandlerThread: Handler:2 & arg1=2

其中消息1与2都是在子线程被发送,分别使用的是dispatchMessagesendMessage,而消息3在主线程发送使用的是dispatchMessage,查看打印的信息可以发现,handlerTest在主线程被创建,通过sendMessage达到了线程的切换的目的(内部走了enqueueMessage,存在入队列的过程)。然而使用dispatchMessage时直接走了消息的处理流程。

public void dispatchMessage(@NonNull Message msg) {
   if (msg.callback != null) {
       handleCallback(msg);
     } else {
        if (mCallback != null) {
           if (mCallback.handleMessage(msg)) {
             return;
         }
      }
    handleMessage(msg);
  }
}

现在就清晰了,dispatchMessage的调用线程就是其处理线程,而sendMessage消息的处理线程则是handler的创建线程。整个流程如下(图片来自网络):

JZNmgU.jpeg
四、解决问题
  • 一个线程有几个Handler?一个线程有几个Looper?如何保证?一个线程可以有几个MessageQueue?

一个线程可以多个Handler对象,Handler的实例化并没有对Thread作校验。而一个线程只能有一个Looper对象,原因是由于内部使用TLS存储,对当前线程作了校验确保只有一个对象。其次MessageQueue是在实例化Looper时所创建,所以与Looper是一一对应的。

private Looper(boolean quitAllowed) {
  mQueue = new MessageQueue(quitAllowed);
  mThread = Thread.currentThread();
}
  • 如何创建一个Message对象?

推荐使用的是obtain,获取一个Message对象,这样可以达到复用消息池(sPool)中的消息对象。

  • Handler内存泄漏的根本原因?内部类是不是都会导致内存泄漏?

Handler造成内存泄漏的根本原因是因为handler的生命周期是比Activity长的,当handler中有延迟的消息,并且没有到时(未被处理)。此时Activity被销毁,但是Handler是无法被回收的?也就是典型的生命周期短的持有了生命周期长的对象。像我们平时写的view的点击事件,也是内部类的形式但并不会造成内存的泄露,根本原因不就是view的生命周期比Activity短嘛。

  • 主线程中如何实例化一个Handler对象,子线程中呢?

主线程因为系统已经帮我们实现了mLooper对象,所以可以直接实例化出来,但是子线程中默认是没有Looper对象与其关联的。需要调用Looper.prepare() -> Looper.loop(),来开启消息循环。

  • Looper死循环为什么不会导致应用ANR,会消耗资源吗?

Looper死循环会阻塞在Message.next()中,根本原因是内部使用了Linux中pipe/epoll机制,当超时参数为0时可以立即返回结果,而-1时则直接阻塞直到被唤醒,但是阻塞时是会让出CPU的资源的。所以并不会造成资源的浪费,这里的ANR跟死循环是有本质区别的。而主线程本身就需要一直循环,退出那么App就退出了

  • 子线程中维护Looper,消息队列无消息时如何处理?有什么用?

应该安全的退出这个looper

public void quitSafely() {
  mQueue.quit(true);
}
  • 使用Handler的postDelay后消息队列的变化?

延迟消息的发送主要在消息队列中MessageQueue插入的判断

  • 多个Handler如何保证线程安全?

经过分析知道,无论实现多少个handler,在msg的传递过程中msg.target持有的就是这个handler本身,也就是谁发送谁处理。当然无论是enqueueMessage还是Message.next中都使用了synchronized同步锁机制。

  • 延迟消息的实现?

延迟消息,只需要在发送时带上delay的时间间隔,但是需要明确的是,由于内核的调度机制,pipe/epoll的唤醒的时间是不够精确的,这是系统所决定的。因此想要实现精确的延迟消息仅仅依赖与handler是不够的。

五、最后

Android Code Search

MessageQueue

Message

Looper

epoll_wait

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

推荐阅读更多精彩内容