换个姿势,带着问题看Handler

本文详细探讨了Android开发中关于主线程更新UI、网络操作的限制,解释了Handler、Looper、MessageQueue的工作原理,以及为何不能在主线程执行耗时操作引发ANR,还涉及了内存泄露和IdleHandler的使用。
摘要由CSDN通过智能技术生成

加上休眠

行吧,以后去面试别人问「子线程是不是一定不可以更新UI」别傻乎乎地点头说是了。

4.引生的另一个问题

说到「只能在主线程中更新UI」我又想到另一个问题「不能在主线程中进行网络操作

上述代码运行直接闪退,日志如下:

NetworkOnMainThreadException:网络请求在主线程进行异常。

em… 真的不能在主线程中做网络操作吗?

onCreate() 的 setContentView() 后插入下面两句代码:

运行下看看:

这…又打脸?先说下 StrictMode(严苟模式)

Android 2.3 引入,用于检测两大问题:ThreadPolicy(线程策略) 和 VmPolicy(VM策略)

相关方法如下

把严苟模式的网络检测关了,就可以 在主线程中执行网络操作了,不过一般是不建议这样做的:

在主线程中进行耗时操作,可能会导致程序无响应,即 ANR (Application Not Responding)。

至于常见的ANR时间,可以在对应的源码中找到:

// ActiveServices.java → Service服务
static final int SERVICE_TIMEOUT = 20*1000; // 前台
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // 后台

// ActivityManagerService.java → Broadcast广播、InputDispatching、ContentProvider
static final int BROADCAST_FG_TIMEOUT = 101000; // 前台
static final int BROADCAST_BG_TIMEOUT = 60
1000; // 后台
static final int KEY_DISPATCHING_TIMEOUT = 51000; // 关键调度
static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10
1000; // 内容提供者

时间统计区间:

  • 起点System_Server 进程调用 startProcessLocked() 后调用 AMS.attachApplicationLocked()
  • 终点Provider 进程的 **installProvider()**及 publishContentProviders() 调用到 AMS.publishContentProviders()

超过这个时间,系统就会杀掉 Provider 进程。

0x2、Handler怎么用

1.sendMessage() + handleMessage()

代码示例如下

黄色部分会有如下警告

Handler不是静态类可能引起**内存泄露**,原因以及正确写法等下再讲。

另外,建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:

从源码,可以看到obtain()的逻辑:加锁判断Message池是否为空

  • ① 不为空,取一枚Message对象,正在使用标记置为0,池容量-1,返回此对象;
  • ② 为空,新建一个Message对象返回;

此处复用Message,可避免**避免重复创建实例对象,达到节约内存的目的,而且不难看出Message实际上是无头结点的单链表**。

上述获取消息池的逻辑:

定位到下述代码,还可以知道:池的容量为50

然后问题来了,Message信息什么时候加到池中?

答:当Message 被Looper分发完后,会调用 recycleUnchecked()函数,回收没有在使用的Message对象。

标志设置为**FLAG_IN_USE**,表示正在使用,相关属性重置,加锁,判断消息池是否满,未满,单链表头插法 将消息插入到表头。

2.post(runnable)

代码示例如下

跟下post():

实际上调用了 sendMessageDelayed() 发送消息,只不过延迟秒数为0,那Runnable是怎么变成Message的呢?跟下getPostMessage()

噢,获取一个新的Message示例后,把 Runnable 变量的值赋值给 callback属性

3.附:其他两个种在子线程中更新UI的方法

activity.runOnUiThread()

view.post() 与 view.postDelay()

0x3、Handler底层原理解析

终于来到稍微有点技术含量的环节,在观摩源码了解原理前,先说下几个涉及到的类。

1.涉及到的几个类

2.前戏

在我们使用Handler前,Android系统已为我们做了一系列的工作,其中就包括了

创建「Looper」和「MessageQueue」对象

上图中有写:ActivityThreadmain函数是APP进程的入口,定位到 ActivityThread → main函数

定位到:Looper → prepareMainLooper函数

定位到:Looper → prepare函数

定位到:Looper → Looper构造函数

另外这里的 mQuitAllowed 变量,直译「退出允许」,具体作用是?跟下 MessageQueue

em…用来 防止开发者手动终止消息队列,停止Looper循环

3.消息队列的运行

前戏过后,创建了Looper与MessageQueue对象,接着调用Looper.loop()开启轮询。 定位到:Looper → loop函数

接着有几个问题,先是这个 myLooper() 函数:

这里的 ThreadLocal线程局部变量JDK提供的用于解决线程安全的工具类作用为每个线程提供一个独立的变量副本以解决并发访问的冲突问题本质

每个Thread内部都维护了一个ThreadLocalMap,这个map的key是ThreadLocal,

value是set的那个值。get的时候,线程都是从自己的变量中取值,所以不存在线程安全问题。

主线程和子线程的Looper对象实例相互隔离的!!!

另外,线程为key也保证了每个线程只有一个Looper,而创建Looper对象时又会创建MessageQueue对象,所以间接保证每个线程最多只能有一个MessageQueue。

知道这个以后,有个问题就解惑了:

为什么子线程中不能直接 new Handler(),而主线程可以?

答:主线程与子线程不共享同一个Looper实例,主线程的Looper在启动时就通过 prepareMainLooper() 完成了初始化,而子线程还需要调用 Looper.prepare() 和 Looper.loop()开启轮询,否则会报错,不信,可以试试:

直接就奔溃了~

加上试试?

可以,没有报错,程序正常运行。

对了,既然说Handler用于子线程和主线程通信,试试在主线程中给子线程的Handler发送信息,修改一波代码:

运行,直接报错:

原因:多线程并发的问题,当主线程执行到sendEnptyMessage时,子线程的Handler还没有创建。 一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:

运行结果如下:

可以,不过其实Android已经给我们封装好了一个轻量级的异步类 HandlerThread

4.HandlerThread

HandlerThread = 继承Thread + 封装Looper

使用方法很简单,改造下我们上面的代码:

用法挺简单的,源码其实也很简单,跟一跟:

剩下一个quit()和quitSafely()停止线程,就不用说了,所以HandlerThread的核心原理就是:

  • 继承Thread,getLooper()加锁死循环wait()堵塞线程;
  • run()加锁等待Looper对象创建成功,notifyAll()唤醒线程
  • 唤醒后,getLooper返回由run()中生成的Looper对象

是吧,HandlerThread的实现原理竟简单如斯,另外,顺带提个醒!!!

Java中所有类的父类是 Object 类,里面提供了wait、notify、notifyAll三个方法; Kotlin 中所有类的父类是 Any 类,里面可没有上述三个方法!!! 所以你不能在kotlin类中直接调用,但你可以创建一个java.lang.Object的实例作为lock, 去调用相关的方法。

代码示例如下

private val lock = java.lang.Object()

fun produce() = synchronized(lock) {
while(items>=maxItems) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items++
println(“Produced, count is i t e m s : items: items:{Thread.currentThread()}”)
lock.notifyAll()
}

fun consume() = synchronized(lock) {
while(items<=0) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items–
println(“Consumed, count is i t e m s : items: items:{Thread.currentThread()}”)
lock.notifyAll()
}

5.当我们用Handler发送一个消息发生了什么?

扯得有点远了,拉回来,刚讲到 ActivityThreadmain函数中调用 Looper.prepareMainLooper 完成主线程 Looper初始化,然后调用 Looper.loop() 开启消息循环 等待接收消息

嗯,接着说下 发送消息,上面说了,Handler可以通过sendMessage()和 post() 发送消息,上面也说了,源码中,这两个最后调用的其实都是 sendMessageDelayed()完成的:

第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟 sendMessageAtTime()

获取当前线程Looper中的MessageQueue队列,判空,空打印异常,否则返回 enqueueMessage(),跟:

这里的 mAsynchronous异步消息的标志,如果Handler构造方法不传入这个参数,默认false: 这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage

如果你了解数据结构中的单链表的话,这些都很简单。 不了解的可以移步至【面试】数据结构与算法(二) 学习一波~

6.Looper是怎么拣队列的消息的?

MessageQueue里有Message了,接着就该由Looper分拣了,定位到:Looper → loop函数

// Looper.loop()
final Looper me = myLooper(); // 获得当前线程的Looper实例
final MessageQueue queue = me.mQueue; // 获取消息队列
for (;😉 { // 死循环
Message msg = queue.next(); // 取出队列中的消息
msg.target.dispatchMessage(msg); // 将消息分发给Handler
}

queue.next() 从队列拿出消息,定位到:MessageQueue -> next函数

这里的关键其实就是:nextPollTimeoutMillis,决定了堵塞与否,以及堵塞的时间,三种情况:

等于0时,不堵塞,立即返回,Looper第一次处理消息,有一个消息处理完 ; 大于0时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行); 等于-1时,无消息时,会一直堵塞;

此处没有用java中的wait/notify堵塞,而是通过Linux的**epoll机制**来堵塞,原因是需要处理 native侧 的事件。

没有消息时堵塞并进入休眠释放CPU资源,有消息时再唤醒线程。

对epoll机制感兴趣的可移步至下述网站查阅:

Linux IO模式及 select、poll、epoll详解

7.分发给Handler的消息是怎么处理的?

通过MessageQueuequeue.next()拣出消息后,调用msg.target.dispatchMessage(msg) 把消息分发给对应的Handler,跟到:Handler -> dispatchMessage

到此,关于Handler的基本原理也说的七七八八了~

8.IdleHandler是什么?

评论区有小伙子说:把idleHandler加上就完整了,那就安排下吧~

MessageQueue 类中有一个 static 的接口 IdleHanlder

翻译下注释:当线程将要进入堵塞,以等待更多消息时,会回调这个接口; 简单点说:当MessageQueue中无可处理的Message时回调; 作用:UI线程处理完所有View事务后,回调一些额外的操作,且不会堵塞主进程;

接口中只有一个 queueIdle() 函数,线程进入堵塞时执行的额外操作可以写这里, 返回值是true的话,执行完此方法后还会保留这个IdleHandler,否则删除。

使用方法也很简单,代码示例如下:

输出结果如下

看下源码,了解下具体的原理:MessageQueue,定义了一个IdleHandler的列表和数组

定义了添加和删除IdleHandler的函数:

next() 函数中用到了 mIdleHandlers 列表:

原理就这样,一般使用场景:绘制完成回调,例子可参见: 《你知道 android 的 MessageQueue.IdleHandler 吗?》 也可以在一些开源项目上看到IdleHandler的应用: useof.org/java-open-s…

0x4、一些其他问题

1.Looper在主线程中死循环,为啥不会ANR?

答:上面说了,Looper通过**queue.next()**获取消息队列消息,当队列为空,会堵塞,

此时主线程也堵塞在这里,好处是:main函数无法退出,APP不会一启动就结束!

你可能会问:主线程都堵住了,怎么响应用户操作和回调Activity生命周期相关的方法?

答:application启动时,可不止一个main线程,还有其他两个Binder线程ApplicationThreadActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。

  • 当系统受到因用户操作产生的通知时,会通过 Binder 方式跨进程通知 ApplicationThread;
  • 它通过Handler机制,往 ActivityThreadMessageQueue 中插入消息,唤醒了主线程;
  • queue.next() 能拿到消息了,然后 dispatchMessage 完成事件分发;

Tips:ActivityThread 中的内部类H中有具体实现

死循环不会ANR,但是 dispatchMessage 中又可能会ANR哦!如果你在此执行一些耗时操作,导致这个消息一直没处理完,后面又接收到了很多消息,堆积太多,就会引起ANR异常!!!

2.Handler泄露的原因及正确写法

上面说了,如果直接在Activity中初始化一个Handler对象,会报如下错误:

原因是

在Java中,非静态内部类会持有一个外部类的隐式引用,可能会造成外部类无法被GC; 比如这里的Handler,就是非静态内部类,它会持有Activity的引用从而导致Activity无法正常释放。

而单单使用静态内部类,Handler就不能调用Activity里的非静态方法了,所以加上「弱引用」持有外部Activity。

代码示例如下

private static class MyHandler extends Handler {
//创建一个弱引用持有外部类的对象
private final WeakReference content;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结尾

最后,针对上面谈的内容,给大家推荐一个Android资料,应该对大家有用。

首先是一个知识清单:(对于现在的Android及移动互联网来说,我们需要掌握的技术)

泛型原理丶反射原理丶Java虚拟机原理丶线程池原理丶
注解原理丶注解原理丶序列化
Activity知识体系(Activity的生命周期丶Activity的任务栈丶Activity的启动模式丶View源码丶Fragment内核相关丶service原理等)
代码框架结构优化(数据结构丶排序算法丶设计模式)
APP性能优化(用户体验优化丶适配丶代码调优)
热修复丶热升级丶Hook技术丶IOC架构设计
NDK(c编程丶C++丶JNI丶LINUX)
如何提高开发效率?
MVC丶MVP丶MVVM
微信小程序
Hybrid
Flutter

接下来是资料清单:(敲黑板!!!


1.数据结构和算法

2.设计模式

3.全套体系化高级架构视频;七大主流技术模块,视频+源码+笔记

4.面试专题资料包(怎么能少了一份全面的面试题总结呢~)

不论遇到什么困难,都不应该成为我们放弃的理由!共勉~

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

24210460)]

2.设计模式

[外链图片转存中…(img-RtqMEiEf-1713624210461)]

3.全套体系化高级架构视频;七大主流技术模块,视频+源码+笔记

[外链图片转存中…(img-pbivDidb-1713624210462)]

4.面试专题资料包(怎么能少了一份全面的面试题总结呢~)

[外链图片转存中…(img-tHwilanG-1713624210463)]

不论遇到什么困难,都不应该成为我们放弃的理由!共勉~

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

[外链图片转存中…(img-eNyeMExJ-1713624210463)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值