Android中线程间通信机制Handler疑问解答

Android中线程间通信机制Handler疑问解答

说起Handler的使用,几乎是开发者入门必备的开发技能。而且都会知道Handler配合一个Looper和MessageQueue来实现消息的创建、分发、处理。

每一个Handler会绑定到创建它的线程以及一个消息队列。

通过Handler,我们可以跟其他线程发消息实现线程切换,也可以给当前线程Handler发消息实现定时任务。

这里总结一下对Handler一些可能有的疑问。

消息循环时是否会阻塞主线程

当Looper开始循环后,会开启一个死循环,在这个循环中不停的通过next()方法从MessageQueue中读取消息,而MessageQueue的next()是一个可能会被阻塞的方法。

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 (;;) {
       //当读取下一条消息,该循环可能会被next()方法阻塞住,直到读取到消息。
       //如果消息为空,说明线程已退出,该循环也可以直接跳出了。
       Message msg = queue.next(); // might block
       if (msg == null) {
           // No message indicates that the message queue is quitting.
           return;
       }

        ...省略部分代码
    }
}

那么MessageQueue的next()是如何在哪阻塞的呢?
答案是 nativePollOnce(ptr, nextPollTimeoutMillis);
这是一个本地C++实现的方法,ptr是native层NativeMessageQueue的指针,nextPollTimeoutMillis是一次阻塞的超时时间。
如果nextPollTimeoutMillis为0,则无需阻塞直接返回。
如果nextPollTimeoutMillis为-1,表示需要无限阻塞,直到被唤醒。
如果nextPollTimeoutMillis为其他正整数,则会阻塞相应的时间,然后返回。

Message next() {
    final long ptr = mPtr;
    int nextPollTimeoutMillis = 0;
    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
           final long now = SystemClock.uptimeMillis();
           Message prevMsg = null;
           //寻找下一条合法的消息
           Message msg = mMessages;
           //如果msg.target为null,代表是异步消息,要一直找到下一条同步消息
           if (msg != null && msg.target == null) {
               do {
                   prevMsg = msg;
                   msg = msg.next;
               } while (msg != null && !msg.isAsynchronous());
           }
           //如果找到一条待处理的消息,有两种情况需要考虑
           //1. 此消息的执行时间还未到,需要阻塞一段时间
           //2. 此消息的执行时间已到,则返回
           if (msg != null) {
               if (now < msg.when) {
                   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;
           }
    }
}

注:消息在入队列时,会按照消息的执行时间点排序,所以查询到的第一条消息,如果还没有到执行的时间点没有到,那么后面的消息都不应该处理。

为什么要阻塞主线程

防止主线程退出

我们都知道一个线程是CPU最小可执行单元,当线程中代码执行完成后,该线程就会退出销毁。而Android中的主线程负责UI绘制、事件响应等,如果主线程退出了,应用就无法继续运行下去了。那么保持一个线程一直执行的方法就是无限循环。
所以当主线程没有消息需要处理时,也必须阻塞住,不能退出。

主线程阻塞机制

Linux epoll

本地方法nativePollOnce(ptr, nextPollTimeoutMillis)的核心实现就是epoll命令。
下面看epoll在Android中应用:

//system/core/libutils/Looper.cpp

//构造唤醒事件的fd
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); 
//创建新的epoll实例,并注册wake管道
mEpollFd = epoll_create(EPOLL_SIZE_HINT); 
//将唤醒事件(mWakeEventFd)添加到epoll实例(mEpollFd)
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);

//等待返回
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);


// 向管道mWakeEventFd写入字符,唤醒等待事件mWakeEventFd的epoll实例
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));

其中epoll_wait()是个阻塞方法,这个方法有三种情况才会返回。
1. eventCount > 0,等待的事件发生,被唤醒
2. eventCount == 0,等待超时,超时时间由timeoutMillis指定
3. eventCount < 0,等待过程中出现异常

epoll是一种高效的I/O多路复用机制。相对于select,epoll不受最大监视文件描述符数量的限制,相对于poll,epoll不是采用轮训方式去获取就绪的文件描述符。
epoll通过事先epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

主线程阻塞为什么不会ANR

主线程阻塞时,并没有任务(页面绘制、触摸输入、键盘输入)执行。因为这些任务处理时也会给主线程发送消息并唤醒主线程,所以不会ANR。

一个纯净的应用进程至少包含几个线程

线程名称 备注
com.qfpay.xxx 主线程
Heap thread poo 异步的堆线程
Signal Catcher 捕捉Kernel信息,比如SIGNAL_QUIT
JDWP 调试线程
ReferenceQueueD 引用队列守护线程
FinalizerDaemon 析构守护线程
FinalizerWatchd 析构监控的守护线程
HeapTrimmerDaem 堆正路的守护线程
GCDaemon 垃圾回收守护线程
Binder_1 Binder通信线程
RenderThread UI绘制线程
GL updater
hwuiTask1

ThreadLocal的作用

存储Looper实例,一个线程只能和一个Looper关联,一个ThreadLocal只能存储一个变量,使用ThreadLocal可以防止多次创建。
ThreadLocal的特性就是在同一个线程,多个类或方法间使用同一个变量副本。而在不同的线程间使用不同的变量副本。

//android.os.Looper

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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

ThreadLocal实现机制

简单来说,就是在线程实现类Thread中声明了一个集合类型的全局变量。每当对ThreadLocal进行存取时,均通过Thread.currentThread()拿到当前线程实例,从而实现线程作用域内的变量。

//java.lang.Thread
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;

    private void exit() {
        threadLocals = null;
    }
}

//java.lang.ThreadLocal
public class ThreadLocal<T> {

  public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

ThreadLocal变量集合类型

上面说到的集合类型在ThreadLocal.ThreadLocalMap这个包级私有的静态类中,核心实现是一个数组。

//java.lang.ThreadLocal


    static class ThreadLocalMap {
        private Entry[] table;


        static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;

                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    }

  1. table初始大小16,扩容的临界值为原容量的2/3,每次扩容后的容量为原容量的2倍。
  2. 每一项Entry的键为ThreadLocal对象,并声明为弱引用。值为要存储的变量,强引用。
  3. 为了避免长期持有value对象而引起内存泄漏,当进行set操作时,会把key对应value置为null,并把entry从数组中清理掉。

有几种处理消息的方式

当从消息队列取出消息后,会交给消息的目标Handler处理这个消息,Handler中处理消息的方法为dispatchMessage(Message)

//android.os.Handler

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
  1. 优先执行Message本身的runnable
  2. 然后由Handler实例的成员变量Callback处理,这个变量在Handler的构造函数中被初始化。例如AsyncLayoutInflater使用了这种方式
  3. 最后由handlerMessage(Message)方法处理,此方法可以由子类复写,是最常用的方式

可不可以在主线程休眠时做额外工作

线程空闲是指暂时没有消息需要处理,即将被epoll.waite阻塞,有两种情况:
1. 消息队列空闲
2. 消息队列头部消息没有到达执行时间

IdleHandler,当线程没有需要处理的消息,将要被阻塞的时候,会遍历已有的IdleHandler,转而处理IdleHandler类型的消息。

//android.os.MessageQueue

public static interface IdleHandler {
    //线程空闲时回调此方法。返回true此回调一直存在,返回false会自动删除。
    boolean queueIdle();
}

public void addIdleHandler(@NonNull IdleHandler handler) {
   if (handler == null) {
       throw new NullPointerException("Can't add a null IdleHandler");
   }
   synchronized (this) {
       mIdleHandlers.add(handler);
   }
}

添加IdleHandler方式如下:

Looper.myLooper().getQueue().addIdleHandler(new IdleHandler() {...});

消息队列调用时机如下:

//android.os.MessageQueue

public final class MessageQueue {

    Message next() {
        int pendingIdleHandlerCount = -1;
        for (;;) {

            //进入阻塞状体时,要获取IdleHandler的数量
            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);
        }

        //开始执行IdleHandler
        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);
                }
                //如果queueIdle()返回false,则将对应的IdleHandler从集合中移除
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            //IdleHandler重置为0,下次重新获取
            pendingIdleHandlerCount = 0;

            //如执行了IdleHandler,则有可能在执行期间有需要处理的消息,
            //所以需要把阻塞超时时间设置为0,表明需要立即读取消息
            nextPollTimeoutMillis = 0;
    }

}    

消息缓存池实现及大小

Android中Message类被设计成链表结构,MessageQueue中持有即将被处理的消息mMessages,当此条消息被处理后,指向下一条需要处理消息。

为了防止消息创建带来的开销,Message中持有一个消息队列mPool,mPool是静态的,由所有线程共用。

//android.os.Message

public final class Message implements Parcelable {
    Message next;
    private static Message sPool;
    //缓存池最大缓存50个消息
    private static final int MAX_POOL_SIZE = 50;

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

    //消息回收
    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

}

当调用obtain()方法时,会从消息缓存池中去除一个消息出来。当回收一个消息时,会放到消息缓存池的头部。

同步/异步消息

Message类有一个flag字段,其中可以取值FLAG_ASYNCHRONOUS,表明这个消息时异步消息,否则是同步消息。

同步消息:在消息队列中等待依次被执行
异步消息:在消息队列中,优先被执行,无论是不是在消息队列的头部

通过Message的setAsynchronous(boolean)方法可以将一个消息设置为异步消息。

那么在消息循环时是怎么优先处理异步消息的呢?
这里要解释一个异步栅栏(SyncBarrier)的东西,异步栅栏是一个特殊的Message,它的target Handler为空,也就是这个消息没有人处理。
当消息队列中存在这样一个异步栅栏的时候,消息循环会只从消息队列取出异步消息,而不是顺序执行。看起来就像是把同步消息拦在了外面。直到这个异步栅栏被移除后,消息队列则又恢复成依次处理消息的模式。

下面是向消息队列中添加和移除一个异步栅栏的步骤:

//android.os.MessageQueue

public final class MessageQueue {

       //添加异步栅栏
       private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

    //移除异步栅栏
     public void removeSyncBarrier(int token) {
        // Remove a sync barrier token from the queue.
        // If the queue is no longer stalled by a barrier then wake it.
        synchronized (this) {
            Message prev = null;
            Message p = mMessages;
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
            if (p == null) {
                throw new IllegalStateException("The specified message queue synchronization "
                        + " barrier token has not been posted or has already been removed.");
            }
            final boolean needWake;
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            p.recycleUnchecked();

            // If the loop is quitting then it is already awake.
            // We can assume mPtr != 0 when mQuitting is false.
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }
}

下面是消息循环时处理异步消息的过程:

//android.os.MessageQueue

public final class MessageQueue {
    Message next() {
        for (;;) {
            synchronized (this) {
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    //如果msg.target为空,表明是一个异步栅栏类型的消息
                    //那么就会一直在消息队列中寻找异步消息,然后去处理
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }

                if (msg != null) {
                    if (now < msg.when) {
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        mBlocked = false;
                        if (prevMsg != null) {
                            //如果prevMsg不为空,说明当前消息时异步消息,需要从消息队列中抽取出来执行
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                }
            }
        }
    }
}

需要注意的是添加/移除异步栅栏的api是隐藏的,也就是只有系统才有权调用,开发者用不了,除了反射。
系统在渲染页面的时候会用到这个特性。

//android.view.ViewRootImpl

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //添加异步栅栏
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

    void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除异步栅栏
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
展开阅读全文

没有更多推荐了,返回首页