本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
掌握Android Handler消息机制核心代码
追忆似水年华 · 471浏览 · 发布于2021-09-27 +关注

该文主要是分析Handler消息机制的关键源码,文章会从对handler有一些基本的认识开始介绍,内容详细,感兴趣的小伙伴可以参考下

阅读前需要对handler有一些基本的认识。这里先简要概述一下:

一、handler基本认识

1、基本组成

完整的消息处理机制包含四个要素:

  • Message(消息):信息的载体

  • MessageQueue(消息队列):用来存储消息的队列

  • Looper(消息循环):负责检查消息队列中是否有消息,并负责取出消息

  • Handler(发送和处理消息):把消息加入消息队列中,并负责分发和处理消息

2、基本使用方法

Handler的简单用法如下:

Handler handler = new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) {                    
        super.handleMessage(msg);
    }
};
Message message = new Message();
handler.sendMessage(message);


注意在非主线程中的要调用Looper.prepare() Looper.loop()方法 

3、工作流程

其工作流程如下图所示:

工作流程 从发送消息到接收消息的流程概括如下:

  • 发送消息

  • 消息进入消息队列

  • 从消息队列里取出消息

  • 消息的处理

下面就一折四个步骤分析一下相关源码:

二、发送消息

handle有两类发送消息的方法,它们在本质上并没有什么区别:

sendXxxx()

  • boolean sendMessage(Message msg)

  • boolean sendEmptyMessage(int what)

  • boolean sendEmptyMessageDelayed(int what, long delayMillis)

  • boolean sendEmptyMessageAtTime(int what, long uptimeMillis)

  • boolean sendMessageDelayed(Message msg, long delayMillis)

  • boolean sendMessageAtTime(Message msg, long uptimeMillis)

  • boolean sendMessageAtFrontOfQueue(Message msg)

postXxxx()

  • boolean post(Runnable r)

  • boolean postAtFrontOfQueue(Runnable r)

  • boolean postAtTime(Runnable r, long uptimeMillis)

  • boolean postAtTime(Runnable r, Object token, long uptimeMillis)

  • boolean postDelayed(Runnable r, long delayMillis)

  • boolean postDelayed(Runnable r, Object token, long delayMillis)

这里不分析具体的方法特性,它们最终都是通过调用sendMessageAtTime()或者sendMessageAtFrontOfQueue实现消息入队的操作,唯一的区别就是post系列方法在消息发送前调用了getPostMessage方法:

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

需要注意的是:sendMessageAtTime()再被其他sendXxx调用时,典型用法为: 

sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);


若调用者没有指定延迟时间,则消息的执行时间即为当前时间,也就是立即执行。Handler所暴露的方法都遵循这种操作,除非特别指定,msg消息执行时间就为:当前时间加上延迟时间,本质上是个时间戳。当然,你也可以任意指定时间,这个时间稍后的消息插入中会用到。 代码很简单,就是讲调用者传递过来的Runnable回调赋值给message(用处在消息处理中讲)。 sendMessageAtTime()sendMessageAtFrontOfQueue方法都会通过enqueueMessage方法实现消息的入栈: 

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

代码很简单,主要有以下操作: 

  • message持有发送它的Handler的引用(这也是处理消息时能找到对应handler的关键)

  • 设置消息是否为异步消息(异步消息无须排队,通过同步屏障,插队执行)

  • 调用MessageQueueenqueueMessage方法将消息加入队列

三、消息进入消息队列

1、入队前的准备工作

enqueueMessage方法是消息加入到MessageQueue的关键,下面分段来分析一下:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
     throw new IllegalArgumentException("Message must have a target.");
    }
    //...省略下文代码
}

这端代码很简单:判断messagetarget是否为空,为空则抛出异常。其中,target就是上文Handler.enqueueMessage里提到到Handler引用。 接下来下来开始判断和处理消息 

boolean enqueueMessage(Message msg, long when) {
    //...省略上文代码
    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;
        //...省略下文代码
    }
    //...省略下文代码
}


首先加一个同步锁,接下来所有的操作都在synchronized代码块里运行 然后两个if语句用来处理两个异常情况: 

  • 判断当前msg是否已经被使用,若被使用,则排除异常;

  • 判断消息队列(MessageQueue)是否正在关闭,如果是,则回收消息,返回入队失败(false)给调用者,并打印相关日志

若一切正常,通过markInUse标记消息正在使用(对应第一个if的异常),然后设置消息发送的时间(机器系统时间)。 接下来开始执行插入的相关操作

2、将消息加入队列

继续看enqueueMessage的代码实现

boolean enqueueMessage(Message msg, long when) {
    //...省略上文代码
    synchronized (this) {
            //...省略上文代码
            //步骤1
            Message p = mMessages;
            boolean needWake;
            //步骤2
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                //步骤3
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p;
                prev.next = msg;
            }
            if (needWake) {
                nativeWake(mPtr);
            }
    }
    //步骤4
    return true;
}

首先说明MessageQueue使用一个单向链表维持着消息队列的,遵循先进先出的软解。 分析上面这端代码: 

第一步:mMessages就是表头,首先取出链表头部。

第二步:一个判断语句,满足三种条件则直接将msg作为表头:

  1. 若表头为空,说明队列内没有任何消息,msg直接作为链表头部;

  2. when == 0 说明消息要立即执行(例如 sendMessageAtFrontOfQueue方法,但一般的发送的消息除非特别指定都是发送时的时间加上延迟时间),msg插入作为链表头部;

  3. when < p.when,说明要插入的消息执行时间早于表头,msg插入作为链表头部。

第三步:通过循环不断的比对队列中消息的执行时间和插入消息的执行时间,遵循时间戳小的在前原则,将消息插入和合适的位置。

第四步:返回给调用者消息插入完成。

需要注意代码中的needWakenativeWake,它们是用来唤醒当前线程的。因为在消息取出端,当前线程会根据消息队列的状态进入阻塞状态,在插入时也要根据情况判断是否需要唤醒。

接下来就是从消息队列中取出消息了

四、从消息队列里取出消息

依旧是先看看准备准备工作

1、准备工作

在非主线程中使用Handler,必须要做两件事

  • Looper.prepare() :创建一个Loop

  • Looper.loop() :开启循环

我们先不管它的创建,直接分段看啊循环开始的代码:首先是一些检查和判断工作,具体细节在代码中已注释

public static void loop() {
   //获取loop对象
        final Looper me = myLooper();
        if (me == null) {
         //若loop为空,则抛出异常终止操作
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
         //loop循环重复开启
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }
        //标记当前loop已经开启
        me.mInLoop = true;
        //获取消息队列
        final MessageQueue queue = me.mQueue;
        //确保权限检查基于本地进程,
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);
        boolean slowDeliveryDetected = false;
        //...省略下文代码
}

2、loop中的操作

接下来就是循环的正式开启,精简关键代码:

public static void loop() {
   //...省略上文代码
   for (;;) {
    //步骤一
            Message msg = queue.next();
            if (msg == null) {
                //步骤二
                return;
            }
           //...省略非核心代码
            try {
             //步骤三
                msg.target.dispatchMessage(msg);
                //...
            } catch (Exception exception) {
                //...省略非核心代码
            } finally {
                //...省略非核心代码
            }
            //步骤四
            msg.recycleUnchecked();
        }
}


分步骤分析上述代码: 

步骤一:从消息队列MessageQueue中取出消息(queue.next()可能会造成阻塞,下文会讲到)
步骤二:如果消息为null,则结束循环(消息队列中没有消息并不会返回null,而是在队列关闭才会返回null,下文会讲到)
步骤三:拿到消息后开始消息的分发
步骤四:回收已经分发了的消息,然后开始新一轮的循环取数据

2.1 MessageQueue的next方法

我们先只看第一步消息的取出,其他的在稍后小节再看,queue.next()代码较多,依旧分段来看

Message next() {
 //步骤一
 final long ptr = mPtr;
    if (ptr == 0) {
  return null;
 }
 //步骤二
 int pendingIdleHandlerCount = -1;
 //步骤三
 int nextPollTimeoutMillis = 0;
 //...省略下文代码
}

第一步:如果消息循环已经退出并且已经disposed之后,直接返回null,对应上文中Loop通过queue.next()取消息拿到null后退出循环 

第二部:初始化IdleHandler计数器
第三部:初始化native需要用的判断条件,初始值为0,大于0表示还有消息等待处理(延时消息未到执行时间),-1则表示没有消息了。

继续分析代码:

Message next() {
 //...省略上文代码
 for(;;){
  if (nextPollTimeoutMillis != 0) {
   Binder.flushPendingCommands();
  }
  nativePollOnce(ptr, nextPollTimeoutMillis);
  //...省略下文代码 
 }
}


这一段比较简单: 

开启一个无限循环
nextPollTimeoutMillis != 0表示消息队列里没有消息或者所有消息都没到执行时间,调用nativeBinder.flushPendingCommands()方法,在进入阻塞之前跟内核线程发送消息,以便内核合理调度分配资源
再次调用native方法,根据nextPollTimeoutMillis判断,当为-1时,阻塞当前线程(在新消息入队时会重新进入可运行状态),当大于0时,说明有延时消息,nextPollTimeoutMillis会作为一个阻塞时间,也就是消息在多就后要执行。

继续看代码:

Message next() {
 //...省略上文代码
 for(;;){
  //...省略上文代码
       //开启同步锁
  synchronized (this) {
   final long now = SystemClock.uptimeMillis();    
                //步骤一
                Message prevMsg = null;
                Message msg = mMessages;
                //步骤二
                if (msg != null && msg.target == null) {
                    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.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    //步骤六
                    nextPollTimeoutMillis = -1;
                }
  }
  //...省略下文IdleHandler相关代码 
 }
}

android

相关推荐

android下vulkan与opengles纹理互通

talkchan · 1177浏览 · 2020-11-23 10:37:39
Android 使用RecyclerView实现轮播图

奔跑的男人 · 2175浏览 · 2019-05-09 17:11:13
微软发布新命令行工具 Windows Terminal

吴振华 · 869浏览 · 2019-05-09 17:15:04
Facebook 停止屏蔽部分区块链广告

· 754浏览 · 2019-05-09 17:20:08
加载中

0评论

评论
分类专栏
小鸟云服务器
扫码进入手机网页