06.Android之消息机制问题

46次阅读

共计 8114 个字符,预计需要花费 21 分钟才能阅读完成。

目录介绍

6.0.0.1 谈谈消息机制 Hander 作用?有哪些要素?流程是怎样的?
6.0.0.2 为什么一个线程只有一个 Looper、只有一个 MessageQueue,可以有多个 Handler?
6.0.0.3 可以在子线程直接 new 一个 Handler 吗?会出现什么问题,那该怎么做?
6.0.0.4 Looper.prepare() 能否调用两次或者多次,会出现什么情况?
6.0.0.5 为什么系统不建议在子线程访问 UI,不对 UI 控件的访问加上锁机制的原因?
6.0.0.6 如何获取当前线程的 Looper?是怎么实现的?(理解 ThreadLocal)
6.0.0.7 Looper.loop 是一个死循环,拿不到需要处理的 Message 就会阻塞,那在 UI 线程中为什么不会导致 ANR?
6.0.0.8 Handler.sendMessageDelayed() 怎么实现延迟的?结合 Looper.loop() 循环中,Message=messageQueue.next() 和 MessageQueue.enqueueMessage() 分析。
6.0.0.9 Message 可以如何创建?哪种效果更好,为什么?
6.0.1.3 使用 Hanlder 的 postDealy() 后消息队列会发生什么变化?
6.0.1.4 ThreadLocal 有什么作用?

好消息

博客笔记大汇总【15 年 10 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 500 篇 [近 100 万字],将会陆续发表到网上,转载请注明出处,谢谢!
链接地址:https://github.com/yangchong2…
如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!所有的笔记将会更新到 GitHub 上,同时保持更新,欢迎同行提出或者 push 不同的看法或者笔记!

6.0.0.1 谈谈消息机制 Hander 作用?有哪些要素?流程是怎样的?

作用:
跨线程通信。当子线程中进行耗时操作后需要更新 UI 时,通过 Handler 将有关 UI 的操作切换到主线程中执行。

四要素:

Message(消息):需要被传递的消息,其中包含了消息 ID,消息处理对象以及处理的数据等,由 MessageQueue 统一列队,最终由 Handler 处理。技术博客大总结

MessageQueue(消息队列):用来存放 Handler 发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待 Looper 的抽取。
Handler(处理者):负责 Message 的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
Looper(消息泵):通过 Looper.loop() 不断地从 MessageQueue 中抽取 Message,按分发机制将消息分发给目标处理者。

具体流程

Handler.sendMessage() 发送消息时,会通过 MessageQueue.enqueueMessage() 向 MessageQueue 中添加一条消息;
通过 Looper.loop() 开启循环后,不断轮询调用 MessageQueue.next();
调用目标 Handler.dispatchMessage() 去传递消息,目标 Handler 收到消息后调用 Handler.handlerMessage() 处理消息。

6.0.0.2 为什么一个线程只有一个 Looper、只有一个 MessageQueue,可以有多个 Handler?

注意:一个 Thread 只能有一个 Looper,可以有多个 Handler
Looper 有一个 MessageQueue,可以处理来自多个 Handler 的 Message;MessageQueue 有一组待处理的 Message,这些 Message 可来自不同的 Handler;Message 中记录了负责发送和处理消息的 Handler;Handler 中有 Looper 和 MessageQueue。

为什么一个线程只有一个 Looper?技术博客大总结

需使用 Looper 的 prepare 方法,Looper.prepare()。可以看下源代码,Android 中一个线程最多仅仅能有一个 Looper,若在已有 Looper 的线程中调用 Looper.prepare() 会抛出 RuntimeException(“Only one Looper may be created per thread”)。
所以一个线程只有一个 Looper,不知道这样解释是否合理!更多可以查看我的博客汇总:https://github.com/yangchong2…

public static void prepare() {
prepare(true);
}

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

6.0.0.3 可以在子线程直接 new 一个 Handler 吗?会出现什么问题,那该怎么做?

不同于主线程直接 new 一个 Handler,由于子线程的 Looper 需要手动去创建,在创建 Handler 时需要多一些方法:

Handler 的工作是依赖于 Looper 的,而 Looper(与消息队列)又是属于某一个线程(ThreadLocal 是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。因此 Handler 就是间接跟线程是绑定在一起了。因此要使用 Handler 必须要保证 Handler 所创建的线程中有 Looper 对象并且启动循环。因为子线程中默认是没有 Looper 的,所以会报错。
正确的使用方法是:技术博客大总结

handler = null;
new Thread(new Runnable() {
private Looper mLooper;
@Override
public void run() {
// 必须调用 Looper 的 prepare 方法为当前线程创建一个 Looper 对象,然后启动循环
//prepare 方法中实质是给 ThreadLocal 对象创建了一个 Looper 对象
// 如果当前线程已经创建过 Looper 对象了,那么会报错
Looper.prepare();
handler = new Handler();
// 获取 Looper 对象
mLooper = Looper.myLooper();
// 启动消息循环
Looper.loop();
// 在适当的时候退出 Looper 的消息循环,防止内存泄漏
mLooper.quit();
}
}).start();

主线程中默认是创建了 Looper 并且启动了消息的循环的,因此不会报错:应用程序的入口是 ActivityThread 的 main 方法,在这个方法里面会创建 Looper,并且执行 Looper 的 loop 方法来启动消息的循环,使得应用程序一直运行。

6.0.0.4 Looper.prepare() 能否调用两次或者多次,会出现什么情况?

Looper.prepare() 方法源码分析
可以看到 Looper 中有一个 ThreadLocal 成员变量,熟悉 JDK 的同学应该知道,当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
public static void prepare() {
prepare(true);
}

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

思考:Looper.prepare() 能否调用两次或者多次

如果运行,则会报错,并提示 prepare 中的 Excetion 信息。由此可以得出在每个线程中 Looper.prepare() 能且只能调用一次
技术博客大总结

// 这里 Looper.prepare() 方法调用了两次
Looper.prepare();
Looper.prepare();
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
Log.i(TAG, “ 在子线程中定义 Handler,并接收到消息。。。”);
}
}
};
Looper.loop();

6.0.0.5 为什么系统不建议在子线程访问 UI,不对 UI 控件的访问加上锁机制的原因?

为什么系统不建议在子线程访问 UI
系统不建议在子线程访问 UI 的原因是,UI 控件非线程安全,在多线程中并发访问可能会导致 UI 控件处于不可预期的状态。

不对 UI 控件的访问加上锁机制的原因

上锁会让 UI 控件变得复杂和低效
上锁后会阻塞某些进程的执行技术博客大总结

6.0.0.7 Looper.loop 是一个死循环,拿不到需要处理的 Message 就会阻塞,那在 UI 线程中为什么不会导致 ANR?

问题描述
在处理消息的时候使用了 Looper.loop() 方法,并且在该方法中进入了一个死循环,同时 Looper.loop() 方法是在主线程中调用的,那么为什么没有造成阻塞呢?

ActivityThread 中 main 方法

ActivityThread 类的注释上可以知道这个类管理着我们平常所说的主线程 (UI 线程)
首先 ActivityThread 并不是一个 Thread,就只是一个 final 类而已。我们常说的主线程就是从这个类的 main 方法开始,main 方法很简短
public static final void main(String[] args) {

// 创建 Looper 和 MessageQueue
Looper.prepareMainLooper();

// 轮询器开始轮询
Looper.loop();

}

Looper.loop() 方法无限循环

看看 Looper.loop() 方法无限循环部分的代码
while (true) {
// 取出消息队列的消息,可能会阻塞
Message msg = queue.next(); // might block

// 解析消息,分发消息
msg.target.dispatchMessage(msg);

}

为什么这个死循环不会造成 ANR 异常呢?
因为 Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说 Activity 的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。技术博客大总结

处理消息 handleMessage 方法

如下所示

可以看见 Activity 的生命周期都是依靠主线程的 Looper.loop,当收到不同 Message 时则采用相应措施。
如果某个消息处理时间过长,比如你在 onCreate(),onResume() 里面处理耗时操作,那么下一次的消息比如用户的点击事件不能处理了,整个循环就会产生卡顿,时间一长就成了 ANR。

public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, “>>> handling: ” + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “activityStart”);
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
break;
case RELAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “activityRestart”);
ActivityClientRecord r = (ActivityClientRecord) msg.obj;
handleRelaunchActivity(r);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
break;
case PAUSE_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “activityPause”);
handlePauseActivity((IBinder) msg.obj, false, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 2) != 0);
maybeSnapshot();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case PAUSE_ACTIVITY_FINISHING:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “activityPause”);
handlePauseActivity((IBinder) msg.obj, true, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 1) != 0);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
………..
}
}

loop 的循环消耗性能吗?

主线程 Looper 从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此 loop 的循环并不会对 CPU 性能有过多的消耗。
简单的来说:ActivityThread 的 main 方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。

6.0.0.9 Message 可以如何创建?哪种效果更好,为什么?runOnUiThread 如何实现子线程更新 UI?

创建 Message 对象的几种方式:技术博客大总结

Message msg = new Message();
Message msg = Message.obtain();
Message msg = handler1.obtainMessage();

后两种方法都是从整个 Messge 池中返回一个新的 Message 实例,能有效避免重复 Message 创建对象,因此更鼓励这种方式创建 Message

runOnUiThread 如何实现子线程更新 UI

看看源码,如下所示
如果 msg.callback 为空的话,会直接调用我们的 mCallback.handleMessage(msg),即 handler 的 handlerMessage 方法。由于 Handler 对象是在主线程中创建的,所以 handler 的 handlerMessage 方法的执行也会在主线程中。
在 runOnUiThread 程序首先会判断当前线程是否是 UI 线程,如果是就直接运行,如果不是则 post,这时其实质还是使用的 Handler 机制来处理线程与 UI 通讯。

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

@Override
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}

6.0.1.3 使用 Hanlder 的 postDealy() 后消息队列会发生什么变化?
post delay 的 Message 并不是先等待一定时间再放入到 MessageQueue 中,而是直接进入并阻塞当前线程,然后将其 delay 的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。此时,如果队头的 Message 正是被 delay 的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该 Message,否则唤醒后直接执行。
6.0.1.4 ThreadLocal 有什么作用?

线程本地存储的功能

ThreadLocal 类可实现线程本地存储的功能,把共享数据的可见范围限制在同一个线程之内,无须同步就能保证线程之间不出现数据争用的问题,这里可理解为 ThreadLocal 帮助 Handler 找到本线程的 Looper。
技术博客大总结

怎么存储呢?底层数据结构是啥?
每个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,它存储了一组以 ThreadLocal.threadLocalHashCode 为 key、以本地线程变量为 value 的键值对,而 ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,也就包含了一个独一无二的 threadLocalHashCode 值,通过这个值就可以在线程键值值对中找回对应的本地线程变量。

关于其他内容介绍
01. 关于博客汇总链接

1. 技术博客汇总

2. 开源项目汇总

3. 生活博客汇总

4. 喜马拉雅音频汇总

5. 其他汇总

02. 关于我的博客

我的个人站点:www.yczbj.org,www.ycbjie.cn
github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/…

简书:http://www.jianshu.com/u/b7b2…

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo…

开源中国:https://my.oschina.net/zbj161…

泡在网上的日子:http://www.jcodecraeer.com/me…

邮箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
segmentfault 头条:https://segmentfault.com/u/xi…

掘金:https://juejin.im/user/593943…

正文完
 0