android 有哪些机制,06.Android之消息机制问题

本文详细介绍了Android中的消息机制,包括Handler的作用、要素及其工作流程。解释了为何一个线程只能有一个Looper和MessageQueue,但可以有多个Handler。还探讨了在子线程中使用Handler的正确方法,Looper.prepare()的调用限制,以及为何不建议在子线程中直接访问UI。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录介绍

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万字],将会陆续发表到网上,转载请注明出处,谢谢!

如果觉得好,可以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()处理消息。

1460000017854978

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.关于博客汇总链接

02.关于我的博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值