学不会Handler?那是因为你还没有看过这篇文章

news2024/11/27 13:32:19

对Android开发者来suo,Handler机制无疑是最重要的知识之一,大家肯定也已经看过诸多有关Handler的教学文章了,为什么你会看到这篇文章?显然是你还没学会,或者忘记了,或者想深究一下。好消息是,这篇文章,绝对会与众不同,一定会让你对Handler机制有更深入的理解。不信?你就读读看

本文将立志成为全网最通俗易懂的Handler机制教学文章!!!

你将会看到与众不同的关于ThreadLocal的讲解!

你将会看到与众不同的Handler机制源码剖析!

流程图贼多的文章,喜欢看图的朋友一定不能错过!

一. 机制介绍

以终为始,还是要问一问为什么,即我们为什么要学习Handler消息机制,Handler消息机制有什么作用?

首先,你要知道,Handler消息机制和Handler不一样哦,Handler只是指Handler这个具体类,而Handler消息机制是包括Handler在内的一系列相关类组成的一个机制。

Handler消息机制有四大作用

  1. 是整个App运行的一个基础
  2. 进行线程间的通信
  3. 发送延时任务
  4. 进行性能优化

下面,分别介绍一下这四个作用

1. 为什么说是整个App运行的一个基础?

我们看下面这个代码

ActivityThread的main方法,是App的程序入口。在入口里面,调用了Looper.loop方法,开启了死循环,使得App得以正常运行,而Lopper又是Handler消息机制里面一个很重要的角色,所以说它是整个App运行的基础

2. 关于线程间通信

线程间通信,可谓是Handler的看家本领。它最核心的功能,就是线程间通信,比如子线程向主线程进行通信,以解决子线程无法更新UI的问题。或者主线程向子线程进行通信,以执行一些耗时任务。

3. 关于发送延时任务

Handler可以通过一系列xxxDelay方法,发送延时任务。

4. 关于进行性能优化

可以通过IdleHandler,或者通过重启消息循环以解除App崩溃等方式,进行App的性能优化

二. 角色介绍

Handler消息机制里面,有几个非常关键的角色,需要逐个捋清。下面,咱们挨个说说

1. Message

它是具体的消息类。看源码,对Message类的定义

总结一下,这个源码定义

(1)Message是可以被发送给Handler来进行处理的一个类

(2)这个类里面包含了一些描述信息(其实就是它的what属性),和一些数据对象(其实就是它的data属性)

(3)获取Message,建议使用Message.obtain()或者Handler.obtainMessage()方法来获取,以实现Message的复用效果

除了这些,还有一些值得注意的点

(1)target属性,存储Message对应的Handler

(2)when属性,确定Message的执行时间

(3)setAsynchronous方法,设置Message是否异步

总之,关于Message,可以这样描述

Message是一个消息类,可以被Handler放到消息队列,也可以被取出,交给Handler来处理。它的what、when、data、target、setAsynchronous等一些属性和方法比较常用,同时它还实现了缓存复用,可以通过obtain来复用Message

2. Handler

老样子,还是看源码的定义

这个源码其实告诉了我非常多的东西。建议仔细研读一遍。我试图做一个归纳

(1)Handler唯一绑定一个线程,同时也绑定其MessageQueue、Looper,属于多对一的关系,即一个线程可以对应多个Handler

(2)主要有两个作用,一是调度Message/Runnable,二是进行线程间通信

针对前者,列举了一些调度Message/Runnable的具体方法,还提了一下延时任务

针对后者,说到主线程会专门维护一个MessageQueue,子线程可以通过Handler和主线程通信

总之,关于Handler,可以这样描述

Handler有两个作用,一是调度Message/Runnable(包括分发和处理),二是进行线程间通信。它和线程、MessageQueue、Looper是多对一的关系

3. MessageQueue

从源码中,我们可以了解到

(1)它持有一个会被Looper分发的Message的列表

(2)Message由Handler添加,而非MessageQueue直接添加

(3)可以通过Looper.myQueue()来获取当前线程的MessageQueue

总之,关于MessageQueue,可以这样描述

MessgaeQueue是一个单链表构成的优先级队列,元素为Message,根据Message的when属性来确定优先级。由Handler来添加Message,由Looper来取出Message

4. Looper

源码依然告诉了我非常非常多的信息,让我试图归纳一下

(1)此类是用来开启,一个线程中,Message的循环的

(2)线程默认是没有消息循环的,需要手动创建,可以通过调用prepareloop,来开启循环

(3)与Looper交互最多的是Handler

(4)给了一个典型的实现了消息循环的线程案例

总之,关于Looper,可以这样描述

可以理解为一个消息循环器,从MessageQueue中取出消息,交给Handler执行。创建方式是prepare,开启循环的方式是调用loop方法

5. ThreadLocal

这里的源码,略显难懂,但我也试图归纳一下

(1)ThreadLocal是与线程相关的,可以提供线程局部变量

(2)举了一个例子,说明ThreadLocal是如何提供线程局部变量的

只看这些,基本很难懂。因地制宜,对于理解ThreadLocal,我们或许可以不看源码的定义,转而看他的使用。也就是它的get方法,和set方法。

我们只看get,因为看懂了get方法,也就能看懂set方法了

ThreadLocal的get方法

get方法就很清晰,比如我们调用ThreadLocal.get()方法,它会这样执行:

首先拿到调用这个方法的线程,然后从这个线程中取出ThreadLocalMap对象,然后将ThreadLocal作为key,找到它对应的value,从而作为返回值返回。

那么这里,问题来了,ThreadLocalMap是什么呢?先不看源码,只看名字,大体能猜到,这是一个Map集合,那么既然是Map集合,key和value又分别是什么呢?我们进去看一下

可以看到,key就是ThreadLocal本身!

所以,我总结一下。

在每次调用ThreadLocal的get方法时,会首先拿到当前执行的线程,这个线程里面有一个变量,是ThreadLocalMap类型的,key是ThreadLocal,value就是我们想要得到的值

画图来看,就是这样

剖析ThreadLocal原理

又有一个问题,同一个ThreadLocal,在不同的线程,调用get方法,得到的value值是不一样的吗?

答案,是的。因为这就是ThreadLocal机制的作用呀,它的作用就是保存线程本地值,在不同的线程,需要映射为不同的值。它的原理,就是因为不同的线程,有不同的ThreadLocalMap,即虽然key一样,但是Map不一样,所以value可以不同

类比我们生活中的例子,就像是我们同一个人(同一个ThreadLocal),在不同的环境(不同的Thread),承担着不同的角色(映射出不同的value)。比如我们在家这个环境中,承担着孩子的角色,为人父母的角色,在职场这个环境中,我们就承担着职工,同事的角色。真可谓是“艺术来源于生活,高于生活”呀

所以,我说,不同的线程之间Looper可以不同,是通过ThreadLocal来保证的,这句话,相信你也能很快理解了。就是因为不同的线程,ThreadLocalMap不同而已。

ThreadLocal总结

综上,关于ThreadLocal,可以这样描述

能够映射线程本地变量,映射的原理,就是不同的线程,ThreadLocalMap不同

总结

ok,以上就是针对Handler机制几大角色的非常详细又通俗易懂的解释。这些都只是一个开胃菜,更详细,更有含金量的,还在后面。

接下来,让我们来介绍一下,关于Handler机制的运行原理。打起精神来!各位!!!

三. Handler消息机制的运行原理

在此部分,我会先捋源码,然后给出一个非常详细的图加深理解,以此来讲清楚Handler消息机制的运行原理

一图以蔽之

这张图,可以说非常详细、生动地展现了Handler机制整体的运行原理。再结合上文,对各个角色的解释,相信你很容易就能看懂了。那么,对自己要求高的人,肯定还是不满于此,是不是还想扒开源码看看?OK,我来满足你的需求。

Handler消息机制源码

看源码,有个问题,即我们看的线索,怎么找,我们从哪一截开始探入,会比较好理解,不会迷乱在纷乱的源码世界中?这个,不同的人有不同的理解,我认为,可以从Handler放消息,即我们调用sendXXX、postXXX方法开始看起,从Looper交给Handler进行消息处理作为结束,正好按照时间顺序,同时也形成闭环。

所以,先从这里看起

Handler怎么放的消息

也就是图中红色区域

在前文,Handler源码的定义中,有介绍到Handler发送消息的几种方式,我再贴一下

一般来说呢,我们使用post、sendMessage这两种方法会多一些,看方法名我们也可以猜出来,其他几种方法,基本都是这两种方式的变形体,所以呢,我们只关注post方法,和sendMessage方法。

sendMessage方法的执行过程

由简到繁,我们先看sendMessage方法的执行过程

这里调用了sendMessageDelayed()方法,将msg传进去了,然后有一个delayMills为0,这个delayMills的意思就是延时任务的延时时间,前面也介绍了,Handler有一个功能是可以执行延时任务。那么延时的时间,就是sendMessageDelayeddelayMills参数。

那进去sendMessageDelayed方法

这个方法又调用了一个sendMessageAtTime

传入了一个uptimeMills参数,这个参数,是之前的delayMills,加上启动后的时间。所以前面的delayMills是一个相对时间,然后再加上SystemClock.uptimeMillis(),组成了一个绝对时间uptimeMills。(其实这在后面会被赋值为Message的when属性)

然后调用了Handler的enqueueMessage方法

最后,调用到了MessageQueue的enqueueMessage方法。自此,就把Message放入消息队列了。

一图以蔽之

让我来总结一下,调用sendMessage,需要传入一个Message对象,然后通过层层传递,最终调用到了MessageQueue的enqueueMessage方法,将Message加入消息队列。

post方法的执行过程

看完sendMessage,我们再看post方法。

一般来说呢,我们是这样调用post方法的

我们进去post方法,看一看

what???它居然又调用了sendMessageDelayed方法!我们需要看下这个getPostMessage做了啥

哦~,原来它把Runnable封装为了Message,并且将自身作为了Message的callback属性!

后续流程都是一样的了,就不继续跟了。

一图以蔽之

好,以上我们就把Handler将Message传递给MessageQueue的过程,捋清楚了。

MessageQueue怎么调度的消息

接下来,我们看MessageQueue是怎么把Message加进去的,怎么把Message取出来的,即图中的这一部分

MessageQueue的放Message的方法,是enqueueMessage,看名字就知道,它的意思是将Message放入队列,里面一定包含了一些策略。MessageQueue的取出Message方法,是next。下面,我们还是按照时间顺序,先enqueueMessage方法,然后再看next方法。

enqueueMessage方法的执行过程

enqueueMessage方法,有点大,截图截不开,我就直接贴代码了。别担心,我会在关键地方,留下注释

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        // 不允许发送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; // 这里将when赋值给了message的when属性
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // 把传入的message,作为新的头节点
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked; // 如果之前是阻塞状态,则唤醒
        } else {
            // 下面的英文注释说的非常详细,我可以再试图总结一下
            // 只有队列的头节点为同步屏障消息,并且当前message,是最早加进来的异步消息时,才有可能需要唤醒
            // 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;
                }
            }
            // 把传进来的message插入了队列中
            msg.next = p; 
            prev.next = msg;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

建议你仔细看几遍以上代码,然后,看下面的流程图

一图以蔽之

此方法执行完后,如果一切正常的话,就可以将Message入队了。

next方法的执行过程

下面,我们来看,从MessageQueue中取出Message的部分,也就是next方法的执行过程

一样的,我还是直接贴代码,不截图了,因为一屏截不下

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.
    // 如果应用程序试图在退出后重新启动looper(这是不支持的),就会发生这种情况。
    final long ptr = mPtr; // mPtr是native代码使用的
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0; // 阻塞时间
    for (;;) { // 注意,这里是一个死循环
        if (nextPollTimeoutMillis != 0) {
            // 将当前线程中挂起的Binder命令刷新到内核驱动程序。
            // 在执行可能会阻塞很长时间的操作之前调用此方法非常有用,
            // 以确保所有挂起的对象引用已被释放,以防止进程持有对象的时间超过所需时间。
            // 总之,这个方法在执行阻塞任务之前调用会好,又因为下面有可能会阻塞,所以在这里调用了这个方法。
            Binder.flushPendingCommands();
        }

        // 阻塞,阻塞时长为nextPollTimeoutMillis
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            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());
            }
            // 此时,msg要么为null,要么为队首消息,要么为第一个异步消息
            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 {
                // -1表示一直阻塞
                nextPollTimeoutMillis = -1;
            }

            // 当处理完所有该处理的message时,才去处理要不要quit
            if (mQuitting) {
                dispose();
                return null;
            }

            // idle handles 只能在MessageQueue为空,或者队首的Message的when不符合要求时,才会执行
            // 下面的英文其实说的更详细
            // 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();
            }
            // 如果第一次循环,处理了mIdleHandlers里面的任务,那么第二次循环时
            // pendingIdleHandlerCount为0,所以直接continue,而不会重新处理mIdleHandlers
            // 注意:这里的循环指的是某一次next方法执行时,里面for死循环中的一次循环。
            // 而又因为next方法会执行多次,所以第二次next方法执行时,又会重新启动一次for死循环,
            // 那么这时,还是会判断是否要处理mIdleHandlers里面的任务。
            if (pendingIdleHandlerCount <= 0) {
                // No idle Handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue; // 注意一下这个continue。
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // 运行IdleHandler里面的代码,只有第一次迭代的时候会执行到这里
        // 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);
                }
            }
        }

        // 注意这里,不管上面的mIdleHandlers是否还有元素,都重置。因为queueIdle都已经执行过一次了
        // Reset the idle Handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // 设置阻塞时长为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;
    }
}
一图以蔽之

出现了!!!你绝没见过的流程图!!! 这个就稍显复杂了,不过多看几遍代码,和我画的流程图做对照,相信你很快就能理解。

需要注意的点,是IdleHandler。在这里,可以看到,它执行的时机,是取不出合适的Message,在阻塞线程之前,执行。同时,执行完之后,会刷新阻塞时长。

所以在这里,它也有坑,

一是有可能永远都不会执行,因为有可能一直都能取出合适的Message。

二是有可能delay正常的Message。比如队首的Message,需要等待1ms就可以执行,然后在阻塞1ms之前,发现有idleHandler可以执行,那么就去执行idleHandler,如果执行idleHandler的耗时为10ms,那么就delay了队首的Message的执行时机,delay了9ms。

尽管它有坑,但还是会在性能优化的场景中用到,用于在线程空闲时执行一些优化任务。

好,以上,关于MessageQueue的拿到Message,和交付Message,就全部介绍完了。

Looper怎么调度的消息

下面介绍Looper的拿到Message,和交付Message的过程,也就是这一部分

如何拿到消息

拿到Message,是在开启消息循环,即Looper.loop中实现的

可以看到这个方法的注释,谷歌官方特意提示,一定要调用!所以我们在开启子线程的消息循环的时候,一定要调用Looper.loop方法。里面有一个核心逻辑,是死循环,每一次循环,会调用loopOnce方法,让我们进入到这个loopOnce方法中,看一看

private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // 这里有可能阻塞
    if (msg == null) {
        // msg为null,只有两种情况,即MessageQueue正在退出或者已经退出
        return false;
    }
    ......
    msg.target.dispatchMessage(msg); // 把message交付给Handler
    ......
    msg.recycleUnchecked(); // 回收message,缓存起来,以便下次复用
    ......
    return true;
}

在每次loopOnce中,一切正常的话,就可以取出一个消息。

如何交付给Handler的

message交付给Handler是怎么交付的呢?Message的target属性就是Handler,所以在这里就是调用了Handler的dispatchMessage方法,进去看下

这里就非常明朗了,在这里,进行最终关于message的处理,然后形成了闭环。

一图以蔽之(Looper的loop)

细看Handler对Message的处理

其实,这里值得再强调下,我们看到,这里有三种方式来处理message,分别是

我们一个一个地看。

处理方式一:handleCallback方法

第一个,handleCallback,是Message的callback属性不为null时,调用。在前面的内容中有过介绍,Message的callback属性,就是调用Handler.post()时,传入的Runnable。可以看下图

所以,如果此Message,是通过Handler.post方式传进来的,那么就直接执行Runnable里面的run方法。

这是第一种处理message的方式。

处理方式二:mCallback的handleMessage方法

第二种,是看Handler的mCallback属性是否为null,如果不为null,则调用mCallbackhandleMessage方法。这里的mCallback和前面说的Message的callback属性不同。这里的mCallback是创建Handler的时候,通过构造方法得到的,

这个Callback和Runnable没有关系,而是一个自定义的接口,里面有一个方法,是handleMessage

所以第二种处理方式,是调用mCallbackhandleMessage方法。如果返回了true,则处理结束,如果返回了false,那么就进入到第三种处理方式,即handleMessage

处理方式三:子类实现handleMessage方法

这里默认实现为空,所以此方式,一般是调用的子Handler的handleMessage方法,来处理的Message。

一图以蔽之

为什么分了三种处理方式?

我个人认为,有两个原因。

(1)保证消息最终能够进行处理

Handler可以通过多种方式进行创建:比如可以直接new,或者new一个子类,同时呢,Handler的构造函数还有好几种。每一种创建方式,对应的消息处理方式,不一定是相同的。所以,分了三种,原因之一,是要把所有的消息处理方式全部覆盖到,尽可能防止有消息处理不到的情况。

(2)有些场景,可能需要对message多次处理,有第二种和第三种处理方式,可以满足此需求。

在代码上,关于第二种和第三种处理方式,分别是这样实现的

 // 第二种处理方式,赋值给Handler的mCallback属性
val Handler1 = Handler(object : Handler.Callback {
    override fun handleMessage(msg: Message): Boolean {
        return true/false
    }
})

// 第三种处理方式,派生子类,重写handleMessage方法
val Handler2 = object : Handler(){
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
    }
}

这两种处理方式有一种关系,就是如果第二种处理方式返回了false,那么第三种处理方式还要执行。即有可能两种处理都会执行,这在某些降级处理场景,可能会被用到。

但其实呢,谷歌提示我们尽量避免创建子类,进而避免调用子类的消息处理方法,而优先用前两种处理Message的方式。原因来自这个注释:

在Callback的类注释中,有“尽量避免不得不实现Handler的子类”这样的字眼,所以得出了上面那个结论。

所以,为什么分了三种处理方式,总结下,就是两个原因

  • 保证消息最终能够进行处理
  • 有些场景,可能需要对Message多次处理,有第二种和第三种处理方式,可以满足此需求。

以上,就把Handler机制的全貌,都捋了一遍了。

三. 加餐:Handler那些事儿

1. 为什么子线程不能访问UI,非要整一个Handler?

因为在Android中,关于UI操作,是没有加锁的,所以不是线程安全的,所以只能是单线程访问才可以。因此Android只能单线程修改UI,而且这个单线程只能是主线程。

那么,为什么不加锁呢?

我能想到的一个很重要的原因,就是流畅性,UI修改一定要是瞬时就要变化的,如果有多线程互相抢着锁,那UI的访问及修改效率都会低很多,手机就会表现的很卡。

而且像UI这种,非常基础的能力,没必要做的这么复杂,如果访问一个UI控件,修改一个UI控件都要加锁的话,那就太复杂了,完全没必要。

总结

Android只能允许单线程修改UI,然后设计了Handler机制来实现子线程修改UI的需求。

2. MessageQueue的阻塞唤醒机制是如何实现的?

其实就是:epoll、pipe机制

什么是pipe?

在类Unix-like操作系统中,一切皆文件,包括管道pipe。管道可以像文件一样在文件系统中存在,并且可以使用文件描述符来引用它们。

管道的作用之一,就是可以实现,线程间的通信。一个线程与另外一个线程之间发生的读、写操作,都可以通过管道,来实现。

什么是epoll?

epoll 是 Linux 中的高效的 I/O 多路复用机制,它会监听一个或多个文件描述符的多个事件类型,其中之一是文件描述符的写入事件。

epoll如何实现阻塞、唤醒?

epoll能够实现阻塞、唤醒的原理,就是能够监听管道这个文件描述符的读操作和写操作,实现阻塞和唤醒。

比如,一个MessageQueue中,没有了消息,那么就应该被阻塞了,那么怎么保持一直阻塞的?什么时候会被唤醒?答案就是:epoll可以监听,pipe的文件描述符的写操作。

当有新的消息写入时,epoll就可以监听到,从而通知线程,实现唤醒。没有写入时,就能保持一直阻塞。

因此,可以这么理解:在没有数据可读时,MessageQueue主要使用了 native 层的 epoll 机制来监听文件描述符(通常是pipe)的写入事件,以实现持续的阻塞和唤醒

一个更好理解的例子

举个生活中的例子,当有人给我打过来电话时,我的手机会通过亮屏,响手机铃声或者震动等方式来通知我。

OK,这里面,包含了两个方面。一是可以打电话,有电话线这条“管道”。二是可以通知我,让我知道,有人打电话过来了。

在这里,电话线就像是pipe,通知就像是epoll。在没有电话打来时,且我当前没有正在接听电话时,属于“读阻塞”的状态,此时如果有写入操作,则会进行唤醒。这里的写入,就是有新的人给我打来了电话,然后epoll通过通知我,唤醒了我,然后我就可以接听电话,即对写入的这条消息进行处理。同时呢,手机不光是在收到电话的时候可以通知,在比如收到微信视频通话的通知时,来了一条短信时,都可以发起通知,这对应epoll的可以监听多个文件描述符的事件的特点。

在MessageQueue中,哪些地方进行了阻塞

next方法中,总共有两个地方,对mBlocked进行了赋值。

  • 一是消息正常取出时,将mBlocked赋值为false,表示不阻塞。
  • 二是当没有可取出的消息,且也没有可以处理的IdleHandler时,赋值为true,表示阻塞。

3. 同步屏障消息和异步消息?

同步屏障消息即Message的target属性为null

异步消息即Message的isAsynchronous返回true

两者之间是如何运转的?

在没有遇到同步屏障消息时,同步消息和异步消息都是正常在MessageQueue上面根据when属性排列,依次取出,执行。然而,当遇到同步屏障消息时,就会向后遍历,找到第一个异步消息,然后根据when是否满足要求,决定如何处理。

这里呢,这种机制,一般会用在优先级比较高的,需要尽快执行的任务,比如绘制UI(draw、invalidate、requestLayout等方法)

4. 消息循环如何退出

其实这块的内容非常多,完全可以单拎出一篇文章来。我们先从入口说起。

退出,是通过调用Looper的quit或者quitSafely来实现的。

两者都是调用了MessageQueue的quit方法,只是传递的参数不同。让我们进去MessageQueue的quit来瞧一瞧

safe参数,顾名思义,就是描述是否安全退出。在这个方法中,首先会判断,是否允许退出,如果不允许,则抛出异常。如果允许,则看当前是否已经在退出中,如果是的话则return,不是,则继续。这里就用到传过来的参数safe了。如果是true,则代表安全退出,否则是非安全退出。让我们看下这两个方法有什么区别?

非安全退出

先看非安全退出,也就是removeAllMessagesLocked方法,它是找到Message队列的头节点,然后依次向后遍历,把所有的message全部回收。非常简单粗暴

安全退出

如果是安全退出

在这里,它会判断队首的messagewhen属性,和now的大小。如果now小于when,则说明队首的消息不满足被取出的条件,则调用removeAllMessagesLocked方法,将所有的消息全部回收。否则就向后遍历到第一个不符合取出要求的message,将它以及它后面的message全部回收。 也就是说,when满足要求的Message,还是会被Looper正常取出然后交给Handler进行消费。

回收Message后,做了什么?

然后,再回到MessageQueue的quit方法来,我们看到,在回收完之后,还调用了一个nativeWake方法

这里的目的其实是唤醒所有阻塞的线程,以免引起不必要的等待,因为消息循环,都已经退出了,所有该回收的消息都回收了,这时候就应该考虑释放线程的一些资源了,此时有可能有一些阻塞的线程,所以要把阻塞的线程全部唤醒,让他们知道,“消息循环已经退出了,你别再等了”。防止这些线程,无限期地阻塞下去。

一图以蔽之

退出之后,如果再继续放入消息,会发生什么呢?

我们知道,Handler的放入消息操作,最终会调用到MessageQueue的enqueueMessage方法

在方法里面,会判断,mQuitting是否为true,如果为true,则直接回收这个消息,然后返回false

所以这时候再放入消息,是不会被取出的,而是放一个回收一个。

退出之后,Looper的loop方法会发生什么呢?

我们都知道Looper的loop是进行消息循环的方法。当退出的时候,MessageQueue的next方法会返回null

此时,loopOnce方法会返回false

然后,在loop里面,当发现loopOnce返回false时,直接return

从而退出消息循环

5. HandlerThread是什么

看类的定义

我们可以发现,HandlerThread是一个线程,并且此线程具有消息循环的能力。也就是说,它是一个封装了Handler机制的线程

run方法里,有一个注意点,就是,给mLooper赋值后,调用了notifyAll方法进行唤醒。那么,是在哪里进行的阻塞呢?

啊哈!就是这里,当调用getLooper方法,获取Looper的时候,如果发现mLooper为null,则阻塞,直到它被成功赋值,才会唤醒阻塞在这的线程。

6. IntentService是什么

直接从类注释中找答案,最简单也最准确!

下面看下这个类的具体内容

@Deprecated
public abstract class IntentService extends Service {
    private volatile Looper mServiceLooper;
    @UnsupportedAppUsage
    private volatile ServiceHandler mServiceHandler;
    private String mName;
    private boolean mRedelivery;

    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

    public IntentService(String name) {
        super();
        mName = name;
    }

    public void setIntentRedelivery(boolean enabled) {
        mRedelivery = enabled;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();

        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        onStart(intent, startId);
        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        mServiceLooper.quit();
    }

     */
    @Override
    @Nullable
    public IBinder onBind(Intent intent) {
        return null;
    }

    @WorkerThread
    protected abstract void onHandleIntent(@Nullable Intent intent);
}

我们通过类注释,和类内容,可以总结出

它是个Service,并且内部维护了一个HandlerThread。它能够在子线程处理任务,并且只有一个子线程,同时只能处理一个子任务,并且当所有任务处理完时,自动关闭。

所以,它是一个可以在子线程执行耗时任务,同时只能执行一个任务,并且在所有任务执行完后,可以自动停止的Service

7. Handler内存泄漏

什么是内存泄漏?

内存泄漏的根本原因,是这样的:一个长生命周期的对象,持有着一个短生命周期的对象的引用,同时此长生命周期的对象又被其他对象引用着,向上能够追溯到GC Root,从而导致这个短生命周期的对象无法在合适时机回收

Handler发生内存泄漏的原因?

那么在Handler这里,很常见的一种内存泄漏的场景,是这样的:

Handler(长生命周期对象)持有了Activity(短生命周期的对象)的引用,同时Handler向上能够被主线程(GC Root)引用着,导致Activity无法在合适时机进行回收。

那么这个引用链,是这样的:

主线程-ThreadLocal-Looper-MessageQueue-Message-Handler-Activity

所以导致有可能发生内存泄漏。

如何解决Handler的内存泄漏问题?

解决方案有三个:

(1)不要用非静态内部类来使用Handler,不然Handler就会持有Activity的引用。

可以用静态内部类或者独立的外部类的方式,创建Handler,这样创建的Handler就不会自带Activity的引用了

(2)如果非要持有Activity引用,那么可以用弱引用包裹Activity

因为弱引用在垃圾回收时是一定会被回收的,所以可以解决内存泄漏的问题

(3)在短生命周期对象(比如Activity)结束时,释放Handler。

这种方式虽然可以解决,但是不太推荐,因为难以维护,容易忘记。如下:

@Override
protected void onDestroy() {
  if(mHanlder != null){
      mHandler.removeCallbacksAndMessages(null)
  }
  super.onDestroy();
}

8. 为什么死循环不会导致ANR?

来了来了,非常经典的问题!!!

为什么消息循环是死循环?

死循环是为了能够保证线程能持续运行下去,而不是执行完就退出了。

为什么这个死循环没有导致ANR?

这里的死循环并不满足ANR的条件。Android的ANR机制,它会检测主线程是否在一定时间内无法响应用户输入。而在消息循环这里,用户在屏幕上操作后,都会通过立即发送一个消息或者其他方式,响应用户的输入。所以这里并不满足Android认定的符合ANR的标准。所以不会ANR。

所以,这是两个维度,死循环只是为了能够让程序一直运行下去。和ANR没有关系

所以真正触发ANR的并非Looper的死循环,而是某一次消息执行时阻塞了很久,即run方法或者handleMessage方法停滞了很久,比如在run方法或者handleMessage方法里面有一个死循环,导致主线程无法在一定时间内响应用户输入。

死循环,不会很耗费资源吗?

在这里也不会很消耗资源,因为在空闲时间会进行阻塞,在需要的时候会进行唤醒。就算有阻塞,用户做了什么操作,也是可以立刻向线程的MessageQueue发送消息,并唤醒线程的。

总结

以上,我就把关于Handler机制的核心知识全部介绍完了,此文花费了我大量精力,如果能对你有所帮助,那是我的荣幸!同时,如果有什么问题,也欢迎在评论区讨论。

如果你还没有掌握Framework,现在想要在最短的时间里吃透它,可以参考一下《Android Framework核心知识点》,里面内容包含了:Init、Zygote、SystemServer、Binder、Handler、AMS、PMS、Launcher……等知识点记录。

《Framework 核心知识点汇总手册》:https://qr18.cn/AQpN4J

Handler 机制实现原理部分:
1.宏观理论分析与Message源码分析
2.MessageQueue的源码分析
3.Looper的源码分析
4.handler的源码分析
5.总结

Binder 原理:
1.学习Binder前必须要了解的知识点
2.ServiceManager中的Binder机制
3.系统服务的注册过程
4.ServiceManager的启动过程
5.系统服务的获取过程
6.Java Binder的初始化
7.Java Binder中系统服务的注册过程

Zygote :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构

WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1177353.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

相对而言,嵌入式开发和纯软件开发哪个更有优势?

相对而言&#xff0c;嵌入式开发和纯软件开发哪个更有优势&#xff1f; 你关心什么方面的优势&#xff1f; 1. 钱途&#xff1f; 纯软件胜&#xff0c;纯软件天花板高很多。嵌入式开发只能说还行。不过天花板这东西是对牛人(至少前30%的人)而言的&#xff0c;混饭吃的话差别就…

JTS: 16 Orientation 方向

这里写目录标题 版本代码 版本 org.locationtech.jts:jts-core:1.19.0 链接: github 代码 public static void main(String[] args) {OrientationUse orientationUse new OrientationUse();orientationUse.test02();}public void test02() {A new Coordinate(2, 1);B new …

高校为什么需要大数据挖掘平台?

目前数据挖掘已经成为各种应用领域的重要技术&#xff0c;大学数据挖掘课程的开放已经出现。数据挖掘课程整合了多门学科知识。该课程包括各种理论知识&#xff0c;也离不开相关的实用技术。整个教学过程是培养和提高学生全面创新和解决问题的能力。过去&#xff0c;教学过程理…

IDEA插件分享,支持接口调试!

平时我们在写完接口需要填入postman、Apipost等工具进行接口调试&#xff0c;今天给大家推荐一款IDEA插件Apipost-helper&#xff0c;写完代码直接可以进行调试&#xff0c;而且支持生成接口文档&#xff0c;JAVA工程师必用&#xff01; 可以点击下方链接或在插件商店中搜索安…

netty基本用法, 拆包、粘包等常见解决方案,看本文即可,不做原理说明,只进行实战操作

netty的基本用法 完整的介绍了netty最基本的发送byte、string、byteBuf发送接收数据&#xff0c;以及拆包、粘包演示&#xff0c;拆包、粘包的解决方案。看netty的用法只看本篇足以&#xff0c;本篇只说用法&#xff0c;不讲底层原理。 详细demo的git地址 示例的通用代码 客…

KT148A语音芯片的下载板子导入F1A声音下载操作多次只成功一次都没有声音

一、问题简介 为什么我使用KT148A语音芯片的下载板子&#xff0c;导入声音下载&#xff0c;操作好多次&#xff0c;只成功了一次&#xff0c;后面始终都没有声音 芯片分为两个版本&#xff0c;分别是按键版本和一线串口版本。看一下样品卡的校验码&#xff1a; 详细描述 1、如…

数学建模比赛中常用的建模提示词(数模prompt)

以下为数学建模比赛中常用的建模提示词&#xff0c;希望对你有所帮助&#xff01; 帮我总结一下数学建模有哪些预测类算法&#xff1f; 灰色预测模型级比检验是什么意思? 描述一下BP神经网络算法的建模步骤 对于分类变量与分类变量相关性分析用什么算法 前10年的数据分别是1&a…

京东店铺所有商品数据接口(JD.item_search_shop)

京东店铺所有商品数据接口是一种允许开发者在其应用程序中调用京东店铺所有商品数据的API接口。利用这一接口&#xff0c;开发者可以获取京东店铺的所有商品信息&#xff0c;包括商品标题、SKU信息、价格、优惠价、收藏数、销量、SKU图、标题、详情页图片等。 通过京东店铺所有…

C风格数组和std::array有什么区别

2023年11月6日&#xff0c;周一下午 C风格数组在 C/C 中没有值语义。当将内置数组作为函数参数或返回值时&#xff0c;实际传递的是指向数组首元素的指针&#xff0c;而不是整个数组的副本。这意味着对函数参数中的数组或返回值返回的数组进行修改会影响原始数组&#xff0c;因…

第七章:计算failure概率

文章目录 第七章Random testingSerial BlocksParallel Blocks如何创建 reliability 的 Block digramsmarkov model如何根据系统来构建马尔科夫计算模型software reliability growthbasic execution time model观察 failure操作概要(operational profiles)Time理解 reliablity …

开源不止,创新澎湃 | 2023开源产业生态大会六大专题抢“鲜”看!马上报名,锁定席位!

2023开源产业生态大会 作为上海市经济和信息化委员会、上海市科学技术协会指导的年度重点活动&#xff0c;2023开源产业生态大会即将于12月19日在上海开幕。大会将展示最前沿的开源创新项目&#xff0c;从金融科技、人工智能到智能汽车、智能制造、工业软件等一系列颠覆性应用…

Springboot自动配置那些事

Spring Boot中默认会扫描的启动类对应的子包下面的类&#xff0c;但是项目引入的其他包下面的类要加入到IOC中必须要有所说明&#xff0c;以下说到的自动配置就是干这个活的&#xff0c;springboot就会把配置中的类加载到ioc容器中。 &#xff08;1&#xff09;自动配置注册文…

Gradle version对应的 Gradle Plugin version

Gradle version对应的 Gradle Plugin version

如何成为前1%的程序员

目录 大量同质化的知识&#xff0c;会降低这些知识的含金量。 1、拥抱调试 2、质量胜于数量 3、读取代码 4、贡献 5、工具 如果你想成为前1%的程序员&#xff0c;你必须遵循1%的程序员做什么&#xff0c;了解其他99%的人不做什么。在现代&#xff0c;我们有各种学习平台…

伦敦金周末可以交易吗,黄金休市时间是那些?

伦敦金是国际性投资产品&#xff0c;主要交易中心有亚洲、欧洲和美洲&#xff0c;在时差的作用下&#xff0c;三大市场相互连接&#xff0c;形成了全天24小时几乎不间断的交易时间&#xff0c;也为炒金者们提供了充分的操作机会。即便如此&#xff0c;在一些特定的时间段内&…

C语言打印1/1+1/2+1/3.....+1/50结果

while语句&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int i 1;double sum 0.0;while(i < 50){sum 1.0/i;i;}printf("sum %lf\n",sum);return 0; } for语句&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1#includ…

JTS: 15 Angle 角度计算

这里写目录标题 版本代码 版本 org.locationtech.jts:jts-core:1.19.0 链接: github 代码 package pers.stu.algorithm;import org.locationtech.jts.algorithm.Angle; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; imp…

深入浅出继承

目录 一、继承的概念 二、继承的定义 2.1 继承格式 2.2 继承方式与访问限定符 2.3 继承方式和访问限定符 2.4 默认继承方式 三、基类与派生类对象赋值转换 四、继承中的作用域 六、派生类默认成员函数 七、继承与友元 八、继承与静态成员 一、继承的概念 继承&…

【c++】c++类的大小的计算和this指针

文章目录 1.类的大小如何计算&#xff1f;2.类内部的this指针3.this指针的特性 本文为作者关于c类学习过程中的小小总结 1.类的大小如何计算&#xff1f; c的类由成员变量和成员函数等组成&#xff0c;不同于c中的结构体只有成员变量&#xff0c;但类大小的计算方法和结构体的…

Redis中的Set类型

目录 set的相关命令 sadd smembers sismember scard spop smove srem 操作集合间的命令 sinter sinterstore sunion sunionstore sdiff sdiffstore 内部编码 set类型的应用场景 redis中的集合类型是保存多个字符串类型的元素的. 作为集合,有两个关键的特性:1…