通过多次吃亏,我好像有点明白什么是handler了

handler相关的blog数不胜数。不好意思,没有记住,就是没有理解到,无法和已有的知识进行关联,嗯,知识薄弱,关联不上来。大家都知道,要想小孩子聪明,得多吃鱼,吃鱼油啥的,成年人想要聪明,一般是采用吃亏的方式。

handler相关的blog,真的看了很多遍,知识不进脑子,而且源码,也点了好几遍,感觉就没有感觉,就是那种单相思的感觉,我喜欢她她不喜欢我,然后我另寻新欢,但是架不住重新喜欢,周而复始。但是从今年开始,啥岗位都问handler,外包也问,问多了,结合面试官的提点,然后重新学习下,突然就感觉可以入门了的感觉。

正文

handler的使用

要想理解一个东西,最好的方式应该是通过理解他解决的痛点是啥,然后去思考他如何解决这个痛点。

主线程中创建一个handler

kotlin 复制代码
class MyHandler:Handler(Looper.getMainLooper()){
    override fun handleMessage(msg: Message) {
        LogUtils.e(msg.obj)
    }
}

创建对象:

kotlin 复制代码
val myHandler by lazy {
    MyHandler()
}

发送消息:

ini 复制代码
myHandler.sendMessage(Message.obtain().apply {
    obj=System.currentTimeMillis()
})

清空消息:

kotlin 复制代码
override fun onDestroy() {
    super.onDestroy()
    myHandler.removeCallbacksAndMessages(null)
}

子线程中创建一个handler

kotlin 复制代码
class LooperThread : Thread() {
    lateinit var mHandler: Handler
    override fun run() {
        Looper.prepare()
        mHandler = object : Handler(Looper.myLooper()!!) {
            override fun handleMessage(msg: Message) {
                LogUtils.e(msg.obj)
            }
        }
        Looper.loop()
        LogUtils.e("循环退出")
    }
}

那么我们需要运行子线程:

css 复制代码
            lateinit var localThread: LooperThread
            localThread=LooperThread().apply {
                start()
            }

发送消息:我们这里发送一个时间戳

ini 复制代码
localThread.mHandler.sendMessage(Message.obtain().apply {
    obj=System.currentTimeMillis()
})

关闭handler:

scss 复制代码
localThread.mHandler.looper.quit()

我们知道。Looper.loop() 可以理解成一个死循环,所以说,我们自定义线程中的handler 需要关闭looper的循环。那么我们在主线程中的handler 可以关闭吗?

答案是不行的,通过主线程的代码,我们可以看到,我们使用的是主线程looper,所以关闭掉,APP就直接死掉了。然后会抛一个异常: java.lang.IllegalStateException: Main thread not allowed to quit. 告诉你,主线程不能这么搞。

似乎,看不出来他解决了什么,感觉就是一个通信工具。

那么Android主线程是如何做的

主线程looper 的创建以及ThreadLocal

在ActivityThread类中的main 函数中首先调用了:

ini 复制代码
Looper.prepareMainLooper();

细究代码就可以发下,我们上面创建的Looper 对象的quitAllowed是true,而系统层的确实false.

  • 当quitAllowed为TRUE的时候,用于创建一个可以被退出的Looper。
  • 当quitAllowed 为false的时候,用于创建一个不可被退出去的Looper 对象。

而通过looper的构造函数可以看到:

ini 复制代码
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

looper 中创建了一个MessageQueue,而quitAllowed 则是给了MessageQueue,然后获取了下当前线程。在函数prepare中:

csharp 复制代码
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));
}

sThreadLocal则是ThreadLocal 的实例。

swift 复制代码
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

我们将当前的Looper 对象创建成功后,便放到ThreadLocal 中。

ThreadLocal 它为每个线程提供了一个独立的变量副本。这意味着每个线程都可以设置其自己的ThreadLocal变量,而不会影响其他线程的变量。

ThreadLocal常用于实现线程局部变量。这种变量在每个线程中都有自己的副本,并且只对当前线程可见,而不会影响其他线程。这对于开发多线程应用程序非常有用,因为它可以避免多个线程之间共享变量时可能出现的问题,例如数据不一致性、竞争条件等。

ThreadLocal类的主要方法是get()和set()。get()方法用于获取当前线程的ThreadLocal变量值,而set()方法用于设置当前线程的ThreadLocal变量值。

结合上面这句话,我们就可以知道以下几个面试题的答案了:

  • 为什么主线程中不调用prepare,因为ActivityThread 已经调用了,再次调用会报错。
  • 为什么一个线程只能调用一次prepare,因为这个代码就是这么写的,创建一次后,再次创建会报错。
  • looper对象如何怎么存储的,ThreadLocal。

loop 从死循环到如何发送消息进行关联

其实,代码都在这里,无论他怎么问,都是不会脱离ThreadLocal,这才是核心。我们继续往后看,在ActivityThred 的main 函数中:

ini 复制代码
Looper.loop();

这是不是和我们自己在子线程中调用是一致的,但是,到这里,我们似乎没有看到handler 绑定looper 的过程,因为我们在上面的代码里面创建handler的时候,都传递了一个looper,那么是否说明,有一种方案,是可以不传递looper的,我们通过Handler的构造函数可以发现:

kotlin 复制代码
class MyHandler2:Handler(){
    override fun handleMessage(msg: Message) {
        LogUtils.e(msg.obj)
    }
}

这么写也可以接受到handler 消息。而在ActivityThread中:

ini 复制代码
final H mH = new H();

这个调调就是主线程中的handler。这个扯远了,我们还是来看Looper.loop()的实现,其他的先不管可以看到这个里面有一段这样的代码:

ini 复制代码
for (;;) {
    if (!loopOnce(me, ident, thresholdOverride)) {
        return;
    }
}

这个me,贼关键,就是looper 本身。这个就是我们常说的死循环的实现。在loopOnce函数中:

ini 复制代码
Message msg = me.mQueue.next(); // might block

所以。我们回顾下,我们创建looper 的时候,是不是自己创建了一个对象,这个对象就是mQueue,所以,这个死循环就一直尝试通过mQueue 进行消息获取。

所以,我们这里代码就走到了,mQueue.next() 的返回值是一message,所以我们来看下如何发送消息。

ini 复制代码
myHandler.sendMessage(Message.obtain().apply {
    obj=System.currentTimeMillis()
})
  • 调用handler.sendMessage()

  • sendMessage() 调用:sendMessageDelayed(msg, 0)

  • sendMessageDelayed(msg, 0) 调用:sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis) ,这个玩意贼关键,因为有些面试官要考这个玩意。在这里,我们将当前时间和延时时间相加,得到一个handler执行的预期时间。

  • enqueueMessage(queue, msg, uptimeMillis) 调用到了这里,可以看到,新增了一个入参,这个queue就是我们handler 构造函数传入的looper中的mqueue,但是我们记得handler 可以不传递looper的:

    ini 复制代码
    public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
    ​
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

通过handler的构造函数,可以看到,如果走不传递looper 的构造函数,那么他会通Looper.myLooper() 获取到一个,结合上面的知识点,我们looper 对象存储到ThreadLocal中,所以每个线程中获取到的对象都不一样。所以在子线程没有调用:Looper.prepare()那么就会报错。我们解决了queue怎么来的问题后。再来看handler 如何发送一条消息

  • 在函数:enqueueMessage中:

    less 复制代码
    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);
    }
    • 可以看到,他对msg的target进行了赋值,同时将mAsynchronous 传递给了msg,这个就尤为的重要的。mAsynchronous用于标记是否是异步任务。
    • 最后,将消息存储到了mQueue 中。

结合上面的循环通过mQueue 获取消息就知道,handler 其实是一个中间对象,负责发送消息和消费消息。循环通过looper 管理,mQueue 则才是消息的存储者和决定当前什么消息被消费的管理者。

如何存储消息

我们先来看queue.enqueueMessage(msg, uptimeMillis)。这个函数的实现:

ini 复制代码
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;
}
  • 首先判断了target 是否等于空,结合上面的信息,我们在enqueueMessage 设置了target,既然判断了,说明有地方允许msgtarget 为空。
  • 第二步,判断message 是否被使用,通常来说,不会出现这种,情况,因为我们使用message 是通过Message.obtain() 实现的,但是架不住自己创建一个message对象,多次通过handler 发送。Message.obtain() 就涉及到享元模式了(这个后面写)
  • 判断mQuitting,这个是我们取消子线程循环的时候调用mHandler.looper.quit() 的时候赋值为true。也就是说,被终止了,handler 也无法添加消息了。
  • 然后设置messae 被使用状态,也就是调用:msg.markInUse()
  • if (p == null || when == 0 || when < p.when) 这个条件,因为mMessages 默认是空,所以为空的时候是第一次,when=0,我们发送消息的时候,when 是给了时间戳的,说明有其他地方发送when=0的message, when < p.when 也是同样的情况,通常来说synchronized 结合我们时间戳,发送延时消息的时候,就会出现头一个节点消息是延时小时,后一个消息不是延时消息,就会将后一个消息放到前面。通过翻译代码注释:New head, wake up the event queue if blocked. 通过这个代码可以看到,他重置了链表的头节点,后续的message 都将添加到这个新的链表里面去。
  • 后续的代码就是将新添加到message 添加到链表的最后一个节点或者基于when < p.when,将新添加到message 添加到P的上一个节点。那么什么时候添加到小于上一个呢? 那就是发送延时消息的时候,比如B是延时消息5毫秒,先send B 消息,A则是马上发送的消息,send A 的时候,就会把A放到B的前面。

到这里,我们通过自定义handler 如何添加消息就已经基本搞清楚了。但是还有一个问题他用的是SystemClock.uptimeMillis():

SystemClock.uptimeMillis()是一个Java方法,它返回自系统启动以来经过的毫秒数。这并不包括睡眠时间或暂停时间。这个方法常被用于计算程序的运行时间或者在一段时间内执行某些操作。

在Android中,SystemClock.uptimeMillis()只有在系统重启后才会重新从0开始。该方法返回的是自系统启动以来经过的毫秒数,因此如果设备没有重启,那么uptimeMillis()将一直增加。如果设备重新启动,那么uptimeMillis()将重新从0开始计数。请注意,如果设备进入休眠状态或屏幕关闭等低功耗模式,uptimeMillis()仍会继续计数。这是因为uptimeMillis()是系统启动后的时间,不受设备是否在使用或处于休眠状态的影响。

所以说,这个玩意可以做埋点快速跳转的指纹的参数,这可能是手机一段时间不关键就感觉有点卡的其中一片雪花吧。

到这里,我们其实就可以知道,mQueue 其实是通过时间戳结合链表对消息队列进行排序的,而为什么可以发送延时消息,也是因为在插入数据的时候就已经处理了。

如何决定应该消费什么消息

OK,我们发送消息大概看完了,我们再次回到mQueue.next() 上来,结合上面的信息,我们looper.loop 死循环里面在一直调用这个这个函数。可以在函数中存在这么的代码块:

ini 复制代码
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());
}
​

这是什么呢? 还记得我们handler 的构造函数吗?构造函数中mAsynchronous 用于标记当前handler 发送的是否是异步消息。而在enqueueMessage函数中,对于message 对象设置中除了赋值了我们的handler 自己,还设置了mAsynchronous。所以从这里可以看出handler 存在两种消息:

  • 同步消息
  • 异步消息

我们知道 mMessages 是链表的头节点,那么,而traget自定义handler 的里面是百分百赋值的,那么msg != null && msg.target == null 判断和msg != null && !msg.isAsynchronous() 这两个条件什么时候触发呢?

这就涉及到一个知识点,我们自定义view和刷新view的时候,调用了invalidate和requestLayout,而这两个函数都调用了:

scss 复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

关键在于调用了 mHandler.getLooper().getQueue().postSyncBarrier(),而在postSyncBarrier函数的实现中:

ini 复制代码
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;

这里可以看到,这个msg 是没有设置target的。所以只要链表头是通过 postSyncBarrier() 发送的消息,那么就一定满足:msg != null && msg.target == null 这个条件。在postSyncBarrer 函数中剩下的代码:

ini 复制代码
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;
}

在if(when!=0) 判断中,主要是通过链表头查找链表链表中when 大于的这个节点,当大于了whlie 就退出循环了。需要把这个消息插入到这个节点的后面。如果大于了,说明是延迟消息正常处理即可。如果没有就讲这个消息设置为头节点。因为我们这个链表是通过when 进行排序的,小的在前面。

我们在来看下面的代码:

arduino 复制代码
 static final int FLAG_ASYNCHRONOUS = 1 << 1;
static int flags;
 public static boolean isAsynchronous() {
     return (flags & FLAG_ASYNCHRONOUS) != 0;
 }

isAsynchronous() 这个函数代码一定返回false,所以msg != null && !msg.isAsynchronous()这个条件也是有满足的条件的,只要通过postSyncBarrier 发送消息即可。而发送postSyncBarrier() 通过注释翻译过来就是设置一个同步屏障:

postSyncBarrier 方法的主要作用是在消息队列中设置一个同步屏障。当消息队列遇到这个屏障时,会暂停后续消息的处理,直到调用了 removeSyncBarrier 方法并传入相应的 token 来移除这个屏障。这个方法常常用于实现一些需要等待特定条件满足才能继续执行的消息处理逻辑。

具体来说,如果你需要在某些消息处理完之后,再处理其他消息,就可以使用 postSyncBarrier 和 removeSyncBarrier 来实现。例如,你可能需要先处理一些初始化消息,然后再处理用户输入的消息。在这种情况下,你可以在处理完初始化消息后,调用 postSyncBarrier 方法设置一个屏障,然后继续处理用户输入的消息。当所有的用户输入消息处理完后,再调用 removeSyncBarrier 方法移除屏障,这样后续的消息就可以继续处理了。

需要注意的是,removeSyncBarrier 必须与 postSyncBarrier 配对使用,否则会导致消息队列无法正常工作,程序可能会挂起。

所以上面代码:

ini 复制代码
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());

的退出条件是:找到一个异步消息或者队列为空了,如果链表头是一个同步屏障标志(没有target)这个do{}while 会去查找一个异步消息直到链表遍历到最后一个节点。当退出循环后:

ini 复制代码
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;
    }
} 

这个函数代码分为两种情况:

  • 当头节点是消息屏障消息的时候。结合最开始msg = msg.next 代码,我们可以一个事情,就是如果链表中没有找到异步消息,那么msg 就是空,如果找到了,msg 就是找到的第一个异步消息。剩下的就是逻辑判断了,如果当前时间不小于异步消息的时间when,那么就讲这个异步消息发送出去。当然还处理头节点相关的关系,如 prevMsg.next = msg.next;这种情况下,就会发现,如果不移除消息屏障,头节点永远不变,那么后续的同步消息就一直接收不到。即使是我们返回了异步消息,然后下次循环进来,还是通过消息屏障设置的头节点在找异步消息。
  • 还有一种情况是头节点不是消息屏障消息,那么也是将头节点发送出去,并且将next节点设置成头节点。

。上面代码没有贴完。我们在看完整的代码:

ini 复制代码
Message next() {
    // Return here if the message loop has already quit and been disposed.
    // This can happen if the application tries to restart a looper after quit
    // which is not supported.
    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();
        }
​
        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;
    }
}

可以看到,这个外层包裹了一层 for (;;) {} 这种死循环,退出的时候,也就只有msg 不为空的时候才退出函数。当然上面的ptr == 0和mQuitting也是支持返回null 退出函数的。mQuitting是需要我们调用quit()才会置为true,到这里,这个函数中获取message的过程大致都整完了,当然还剩下一些:

  • mPtr 怎么赋值,作用。当执行dispose(); mPtr 就会赋值为0。所以函数前面返回null 的逻辑也找到了,当然还有一些其他的关于mPtr 的细节。
  • pendingIdleHandlerCount 的作用。
  • Binder.flushPendingCommands();
  • nativePollOnce(ptr, nextPollTimeoutMillis);
  • 后面剩下的就是mIdleHandlers 相关的处理了,这个感觉的单独写,目前只需要记住 在next 里面执行了 keep = idler.queueIdle(); 并且移除了他即可。执行条件是什么呢?我们上面没有找到msg,并且头节点为Null或当前时间小于头节点的时候,然后就开始执行mIdleHandlers。就是说,循环里面不处理消息的时候,就是空闲状态的时候,就处理这个。

如何消费一个消息

上面我们看到了,如何获取到一个msg,然后有消息屏障,异步消息,同步消息,知道了头节点属于消息屏障的时候,会执行找到一个最近的异步消息,如果没有找到就一直循环找,直到消息屏障被移除,当没有消息的时候,会执行idlerHandler。还是回到loopOnce 函数,我们这里主要是获取一个msg 然后执行。我们知道next 函数可以返回空的,那是在调用quit() 函数后,所以:

java 复制代码
Message msg = me.mQueue.next(); // might block
if (msg == null) {
    // No message indicates that the message queue is quitting.
    return false;
}

这里的判断结合looper.loop里面的:

ini 复制代码
for (;;) {
    if (!loopOnce(me, ident, thresholdOverride)) {
        return;
    }
}

就实现了死循环的退出(这玩意,好像也有人问)。继续函loopOnce函数里面的:

java 复制代码
final Printer logging = me.mLogging;
if (logging != null) {
    logging.println(">>>>> Dispatching to " + msg.target + " "
            + msg.callback + ": " + msg.what);
}

通过Looper里面获取到了一个logging,简单的检测其实就可以通过这么做,网络上很多类型的blog就是添加logging 实现的。而执行的地方就是在loopOnce函数里面。在这个函数里面有这么关键点一句话:

ini 复制代码
try {
    msg.target.dispatchMessage(msg);
    if (observer != null) {
        observer.messageDispatched(token, msg);
    }
    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}

我们知道有消息屏障,但是真正返回的消息一定是有target的,所以在这里,就调用了dispatchMessage:

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

所以说,无论是callback,还是重写handleMessage 也是在这里调用的。

message的享元模式

我们使用message 的时候,往往都是通过obtain() 返回一个消息:

ini 复制代码
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();
}

我们知道Message 其实也是一个链表,这里的逻辑也很简单,就是判断链表头存不住,如果不存在就直接new,如果存在就返回链表头,将链表下一个设置为新的链表头,链表长度-1,然后设置flags 之类的。

那么什么时候添加进去呢?一定是消费后,我们从上面的代码可以看到,添加到消息的时候对消息是否被消费的flags 进行了判断。在loopOnce 里面最后几行有这么一句代码:

ini 复制代码
msg.recycleUnchecked();

recycleUnchecked的实现:

ini 复制代码
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 = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;
​
    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

可以看到在synchronized里面,就是将回收的添加到链表的头部。同时判断了下长度。当然了message 还有一些其他的操作,比如不同状态下的flags等。

如何移除消息屏障

我们知道添加消息屏障是调用的是postSyncBarrier(),那么移除消息屏障呢?就是removeSyncBarrier(token),

java 复制代码
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);
        }
    }
}

还记得 我们发送同步消息屏障创建msg 的代码:

ini 复制代码
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;

他为msg 设置了一个arg1=token,同时target=Null, while (p != null && (p.target != null || p.arg1 != token)) 这个循环的退出条件也狠简单,链表循环完了或者找到一个同步消息屏障。再结合下面的代码:

csharp 复制代码
if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }

如何没有找到同步消息屏障,那么就直接抛异常了。剩下的逻辑就简单了:

  • prev 不为空的情况下,说明同步消息屏障没有在链表头部,就直接跳过这个屏障节点:prev.next = p.next
  • 当prev为空的时候,说明消息屏障是头节点, mMessages = p.next,同样跳过屏障节点即可。

MessageQueue native 相关函数

  • nativeInit 用于初始化消息队列,他创建了一个新的消息队列,并准备用于接收和发送消息。构造函数中的:mPtr = nativeInit();
  • nativeDetroy,该函数用于销毁消息队列,在不需要消息队列的时候调用,用于释放相关资源。
  • nativePollOnce 此函数用于从消息队列中轮询消息,他等待直到有消息可用,或指定的超时时间以已过。我们next for 循环里面就有这行代码。
  • nativelsPolling ,此函数用于检测当前线程是否正在轮序消息,如果是就是true
  • nativeSetFileDescrptorEvents 用于设置文件描述符事件,是一种通信机制,当文件描述符状态发送变化时,例如,数据可以读取或者写入,操作系统会通知应用程序。
  • nativeWake,此函数用于唤醒正在轮序的消息线程,如果一个线程正在轮序消息,并处于休眠状态,则调用此函数立即唤醒该线程,使其继续轮询消息。

好像没有看到轮序暂停的相关native 函数,没有看到aosp 中的实现代码,这一块就是迷糊的。

pendingIdleHandlerCount

pendingIdleHandlerCount 是一个在Android的 Looper 类中使用的内部变量。它用于存储当前在事件队列中挂起的 Handler 实例的数量。

当一个 Handler 实例调用 Looper.post()Looper.postAtFrontOfQueue() 方法将消息添加到事件队列时,如果该消息不能立即执行(例如,如果事件队列为空或者正在进行其他处理),那么这个 Handler 实例就会挂起,直到事件队列变得可用。在这个过程中,pendingIdleHandlerCount 就会增加。

当一个 Handler 实例从事件队列中取出一个消息并开始处理它时,pendingIdleHandlerCount 就会减少。当 pendingIdleHandlerCount 变为0时,表示没有挂起的 Handler 实例,事件循环可以进入空闲状态。

请注意,这是一个内部变量,通常不会在你的应用代码中直接使用。它主要用于 Android 系统内部的消息传递和事件处理机制。

Binder.flushPendingCommands();

Binder.flushPendingCommands()是Android的API方法,它用于强制执行所有挂起的命令。这可以确保所有的方法调用都会立即执行,即使这些方法原本可能会延迟执行。

在Android中,一些操作(例如,I/O操作,网络操作等)可能会被安排为异步执行,即这些操作不会立即执行,而是会在未来的某个时间点执行。在这种情况下,Binder.flushPendingCommands()可以用来强制立即执行这些操作。

然而,需要注意的是,过度使用或不当使用此方法可能会导致性能问题或意外的行为。例如,如果在主线程(UI线程)中频繁地调用此方法,那么可能会打乱应用的正常事件循环,导致界面卡顿或闪烁。

在大多数情况下,你应该让Android系统自行管理异步操作。只有在极少数需要立即响应用户输入或避免潜在的延迟问题的情况下,才应该使用此方法。

结束

到这里,我们基本上是将handler 如何发送消息,如何分发消息给整明白了,当然有图的话更容易理解,这个后期再补吧,主要是大佬们写的基本上都带图,感觉抄袭一个没有必要,要自己画才有意思。

当然了,还有很多细节上的东西,这只能是一次简单的汇总吧。

相关推荐
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王14 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡14 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库15 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道16 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe16 小时前
Android Hook - 动态加载so库
android
居居飒17 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He20 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗20 小时前
Android笔试面试题AI答之Android基础(1)
android