15分钟彻底掌握Handler

我们一起分析 Android Handler 的源码。

Handler 现在几乎是 Android 面试的必问知识点了,大多数 Android 工程师都在项目中使用过 Handler。主要场景是子线程完成耗时操作的过程中,通过 Handler 向主线程发送消息 Message,用来刷新 UI 界面。这节课我们来了解 Handler 的发送消息和处理消息的源码实现。

分析源码的时候最好是找到一个合适的切入点,Handler 源码的一个切入点就是它的默认构造器。

从 new Handler() 开始

511591752260_.png

在无参构造器里调用了重载的构造方法并分别传入 null 和 false。并且在构造方法中给两个全局变量赋值:mLooper 和 mQueue。

这两者都是通过 Looper 来获取,具体代码如下:


521591768081_.png

可以看出,myLooper 通过一个线程本地变量中的存根,然后 mQueue 是 Looper 中的一个全局变量,类型是 MessageQueue 类型。

接下来的分析重点就是这个 Looper 是什么?以及何时被初始化?

Looper 介绍

不知你有没有思考过一个问题,启动一个 Java 程序的入口函数是 main 方法,但是当 main 函数执行完毕之后此程序停止运行,也就是进程会自动终止。但是当我们打开一个 Activity 之后,只要我们不按下返回键 Activity 会一直显示在屏幕上,也就是 Activity 所在进程会一直处于运行状态。实际上 Looper 内部维护一个无限循环,保证 App 进程持续进行。

Looper初始化

Activity 启动过程时,ActivityThread 的 main 方法是一个新的 App 进程的入口,其具体实现如下:

531591768172_.png

解释说明:

图中 1 处就是初始化当前进程的 Looper 对象;
图中 2 处调用 Looper 的 loop 方法开启无限循环。
prepareMainLooper 方法如下:


541591768413_.png

这里我没有省略任何一行代码,因为此处的代码很精简但是每一行又都有意义。

图中 1 处在 prepareMainLooper 中调用 prepare 方法创建 Looper 对象,仔细查看发现其实就是 new 出一个 Looper。核心之处在于将 new 出的 Looper 设置到了线程本地变量 sThreadLocal 中。也就是说创建的 Looper 与当前线程发生了绑定。

Looper 的构造方法如下:


551591768596_.png

可以看出,在构造方法中初始化了消息队列 MessageQueue 对象。

prepare 方法执行完之后,会在图中 3 处调用 myLooper() 方法,从 sThreadLocal 中取出 Looper 对象并赋值给 sMainLooper 变量。


561591768607_.png

注意:

图中 2 处在创建 Looper 对象之前,会判断 sThreaLocal 中是否已经绑定过 Looper 对象,如果是则抛出异常。这行代码的目的是确保在一个线程中 Looper.prepare() 方法只能被调用 1 次。比如以下代码:


571591768618_.png

执行上述代码程序会秒崩,打印日志如下:


581591768629_.png

注意:

不是说调用 2 次 prepare 才会抛异常吗?为什么 MainActivity 中只调用了 1 遍就导致程序崩溃? 这是因为在 MainActivity 所在进程被创建时,Looper 的 prepare 方法已经在 main 方法中调用了 1 遍。这会直接导致一个非常重要的结果:

prepare 方法在一个线程中只能被调用 1 次;
Looper 的构造方法在一个线程中只能被调用 1 次;
最终导致 MessageQueue 在一个线程中只会被初始化 1 次。
也就是说 UI 线程中只会存在 1 个 MessageQueue 对象,后续我们通过 Handler 发送的消息都会被发送到这个 MessageQueue 中。

Looper 负责做什么事情

用一句话总结 Looper 做的事情就是:不断从 MessageQueue 中取出 Message,然后处理 Message 中指定的任务。

在 ActivityThread 的 main 方法中,除了调用 Looper.prepareMainLooper 初始化 Looper 对象之外,还调用了 Looper.loop 方法开启无限循环,Looper 的主要功能就是在这个循环中完成的。


591591768875_.png

很显然,loop 方法中执行了一个死循环,这也是一个 Android App 进程能够持续运行的原因。

图中 1 处不断地调用 MessageQueue 的 next 方法取出 Message。如果 message 不为 null 则调用图中 2 处进行后续处理。具体就是从 Message 中取出 target 对象,然后调用其 dispatchMessage 方法处理 Message 自身。那这个 target 是谁呢?查看 Message.java 源码可以看出 target 就是 Handler 对象,如下所示:


601591768883_.png

Handler 的 dispatchMessage 方法如下:


611591768897_.png

可以看出,在 dispatchMessage 方法中会调用一个空方法 handleMessage,而这个方法也正是我们创建 Handler 时需要覆盖的方法。那么 Handler 是何时将其设置为一个 Message 的 target 的呢?

Handler 的 sendMessage 方法

Handler 有几个重载的 sendMessage 方法,但是基本都大同小异。我用最普通的 sendMessage 方法来分析,代码具体如下:

611591768897_.png

可以看出,经过几层调用之后,sendMessage 最终会调用 enqueueMessage 方法将 Message 插入到消息队列 MessageQueue 中。而这个消息队列就是我们刚才分析的在 ActivityThread 的 main 方法中通过 Looper 创建的 MessageQueue。

Handler 的 enqueueMessage 方法

621591768911_.png

可以看出:

在图中 1 处 enqueueMessage 方法中,将 Handler 自身设置为 Message的target 对象。因此后续 Message 会调用此 Handler 的 dispatchMessage 来处理;
图中 2 处会判断如果 Message 中的 target 没有被设置,则直接抛出异常;
图中 3 处会按照 Message 的时间 when 来有序得插入 MessageQueue 中,可以看出 MessageQueue 实际上是一个有序队列,只不过是按照 Message 的执行时间来排序。
至此 Handler 的发送消息和消息处理流程已经介绍完毕,接下来看几个面试中经常被问到的与 Handler 相关的题目。

Handler 的 post(Runnable) 与 sendMessage 有什么区别

看一下 post(Runnable) 的源码实现如下:


631591769178_.png

实际上 post(Runnable) 会将 Runnable 赋值到 Message 的 callback 变量中,那么这个 Runnable 是在什么地方被执行的呢?Looper 从 MessageQueue 中取出 Message 之后,会调用 dispatchMessage 方法进行处理,再看下其实现:


641591769188_.png

可以看出,dispatchMessage 分两种情况:

如果 Message 的 Callback 不为 null,一般为通过 post(Runnabl) 方式,会直接执行 Runnable 的 run 方法。因此这里的 Runnable 实际上就是一个回调接口,跟线程 Thread 没有任何关系。
如果 Message 的 Callback 为 null,这种一般为 sendMessage 的方式,则会调用 Handler 的 hanlerMessage 方法进行处理。
Looper.loop() 为什么不会阻塞主线程

刚才我们了解了,Looper 中的 loop 方法实际上是一个死循环。但是我们的 UI 线程却并没有被阻塞,反而还能够进行各种手势操作,这是为什么呢?在 MessageQueue 的 next 方法中,有如下一段代码:

651591769196_.png

nativePollOnce 方法是一个 native 方法,当调用此 native 方法时,主线程会释放 CPU 资源进入休眠状态,直到下条消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作,这里采用的 epoll 机制。关于 nativePollOnce 的详细分析可以参考:nativePollOnce函数分析。

Handler 的 sendMessageDelayed 或者 postDelayed 是如何实现的

之前我已经介绍过,在向 MessageQueue 队列中插入 Message 时,会根据 Message 的执行时间排序。而消息的延时处理的核心实现是在获取 Message 的阶段,接下来看下 MessageQueue 的 next 方法。


661591769209_.png

图中蓝框处表示从 MessageQueue 中取出一个 Message,但是当前的系统时间小于 Message.when,因此会计算一个 timeout,目的是实现在 timeout 时间段后再将 UI 线程唤醒,因此后续处理 Message 的代码只会在 timeout 时间之后才会被 CPU 执行。

注意:在上述代码中也能看出,如果当前系统时间大于或等于 Message.when,那么会返回 Message 给 Looper.loop()。但是这个逻辑只能保证在 when 之前消息不被处理,不能够保证一定在 when 时被处理。

总结

应用启动是从 ActivityThread 的 main 开始的,先是执行了 Looper.prepare(),该方法先是 new 了一个 Looper 对象,在私有的构造方法中又创建了 MessageQueue 作为此 Looper 对象的成员变量,Looper 对象通过 ThreadLocal 绑定 MainThread 中;
当我们创建 Handler 子类对象时,在构造方法中通过 ThreadLocal 获取绑定的 Looper 对象,并获取此 Looper 对象的成员变量 MessageQueue 作为该 Handler 对象的成员变量;
在子线程中调用上一步创建的 Handler 子类对象的 sendMesage(msg) 方法时,在该方法中将 msg 的 target 属性设置为自己本身,同时调用成员变量 MessageQueue 对象的 enqueueMessag() 方法将 msg 放入 MessageQueue 中;
主线程创建好之后,会执行 Looper.loop() 方法,该方法中获取与线程绑定的 Looper 对象,继而获取该 Looper 对象的成员变量 MessageQueue 对象,并开启一个会阻塞(不占用资源)的死循环,只要 MessageQueue 中有 msg,就会获取该 msg,并执行 msg.target.dispatchMessage(msg) 方法(msg.target 即上一步引用的 handler 对象),此方法中调用了我们第二步创建 handler 子类对象时覆写的 handleMessage() 方法。

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