Android Handler 机制(二):Handler 机制深入探究问题梳理

本文目录:
一、‘非UI线程更新UI’问题探究
二、Handler发送消息的delay设置是否可靠?
三、Handler机制下消息队列MessageQueue的优化
四、主线程的Looper为什么不会导致ANR

一、‘非UI线程更新UI’问题探究

Android开发的时候非UI线程不能更新UI,这个是大家都知道的开发常识。但是当问到为什么?可能我们就会有些含糊了。

本文我们就针对这个问题进行探讨并进行一定的思维发散,来加深我们对Android界面刷新机制的理解。

1. UI线程的工作机制

主线程的工作机制可以概况为 生产者 - 消费者 - 队列 模型。

2. 为什么UI线程不设计成线程安全的

总所周知,如果设计成线程安全的,那性能肯定是大打折扣的,而UI更新的要求有如下特性:

UI是具有可变性的,甚至是高频可变。
UI对响应时间很敏感,这就要求UI操作必须要高效。
UI组件必须批量绘制来保证效率。
所以为了保证渲染性能,UI线程不能设计成线程安全的。Android设计了Handler机制来更新UI是避免多个子线程更新UI导致的UI错乱的问题,也避免了通过加锁机制设计成线程安全的,因为那样会导致性能下降的很厉害。

3. 子线程能创建Handler吗?

能。但是需要先调用Looper.prepare()方法,否则会抛出运行时异常[Can’t create handler inside thread that has not call Looper.prepared()]。

4. 子线程的Looper和主线程的Looper有什么区别

子线程的Looper可以退出的,主线程的Looper时不能退出的。

5. 非UI线程一定不能更新UI吗?

答:不一定。

说明:我们知道在Android提供的SurfaceView、GLSurfaceView里面是都能在非UI线程更新UI的。

并且在一些特定的场景下,子线程更新View也是能更新成功的。

例如,下面的代码在子线程中更新界面是可以成功的:

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
 
public class TestActivity extends Activity {

    Button btn = null;

    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       btn = (Button) findViewById(R.id.Button01);
       new TestThread(btn).start();
    }
 

    class TestThread extends Thread {

       Button btn = null;

       public TestThread(Button btn) {
           this.btn = btn;
       }

       @Override
       public void run() {
           btn.setText("TestThread.run");
       }
    }
}

当我们深入分析其原理的时候,就可以知道,能否更新成功的关键点在于是否会触发checkThead()导致更新失败,抛出异常:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");
    }
}

而在ViewRootImpl中,会有这些方法调用到checkThread()方法:
在这里插入图片描述

经过分析,最终可以得到,在子线程中给TextView setText 不会抛出异常的两个场景:

1:TextView 还没来得及加入到ViewTree中

2:TextView已经被加入了ViewTree,但是被设置了固定宽高,且开启了硬件加速

子线程操作View 确实不一定导致Crash,那是因为刚好满足一定的条件并没有触发checkThread机制,但这并不代表我们在开发过程中可以这么写,其实我们还是应该遵循google的建议,更新UI始终在UI线程里去做。

推荐资料:

《我感觉我学了一个假的Android…》

《Android 子线程更新TextView的text 不抛出异常原因 分析总结》

二、Handler发送消息的delay设置是否可靠?

答案是:不可靠。

原因:当Handler所属的线程(UI线程)要处理的内容非常多,当Looper出现事件积压的时候会使得delay不可靠。如ANR的出现就是一个最极端的代表例子。

为了理解为何在事件挤压的时候,handler会出现delay的不可靠,这里我们就加入一些核心逻辑的分析,来帮助我们进行此问题的探究。

三、Handler机制下消息队列MessageQueue的优化

一般情况下,可以考虑从以下几个方面进行优化

对消息队列中重复消息的过滤,用于控制一些操作的频率,既保证用户体验又保证性能(使用Handler Api)。
对消息队列中的互斥消息进行取消,用于类似开关之类的操作(使用Handler Api)。
消息池复用,减少创建Message实例的开销(Handler机制自带消息池)。
IdleHandler的合理使用,利用空闲的时机进行一些业务逻辑的处理(视业务而定)。

四、主线程的Looper为什么不会导致ANR

1. ANR的产生条件

输入超时:5秒(最常见)
服务超时:前台服务20s、后台服务200s。
广播队列超时:前台广播10秒、后台广播60秒。
ContentProvider超时:10秒
其实最终就是最终由系统的Api发送的Message,告知相关组件发生了ANR。

这里我们使用Service举例,当Service超时的时候,发送的Message.what就是ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG。

ANR也算是Android系统提醒开发者程序编写有问题的一个机制。

补充: WatchDog、BlockCanary、AndroidGodEye 都是比较不错的卡顿异常检测工具。

BlockCanary :https://github.com/markzhai/AndroidPerformanceMonitor(将近4年未更新维护,不推荐使用,但可以通过阅读其代码,来加深理解)

ANR-WatchDog:https://github.com/SalomonBrys/ANR-WatchDog

AndroidGodEye:https://github.com/Kyson/AndroidGodEye

2.Looper不会导致应用ANR的本质原因是什么?

当我们熟悉了Looper的工作机制,我们就会知道主线程执行的操作就是执行Loop.loop()不断的处理消息。Looper是进程上的一个概念,ANR是程序执行到某一个环节对开发者占用主线程耗时的一个监控机制,是应用没有在规定时间内完成AMS指定的任务导致的。ANR产生的根本原因是不是因为主线程Looper循环,而是因为主线程中有耗时任务。

3.Looper为什么不会导致CPU占用率高?(延伸)

这是因为使用了Linux底层的pipe管道机制和epoll机制,在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce() 方法里,在没有消息时阻塞线程并进入休眠释放cpu资源,有消息时唤醒线线程,这样不会导致CPU占用率高。

总结:Android的应用层通过Message.java实现队列,利用管道和epoll机制实现线程状态的管理,配合起来实现了Android主线程的消息队列模型。

拓展学习:

Linux 下 Epoll 机制概述:https://www.cnblogs.com/renhui/articles/12868221.html

Android中的Looper与epoll:https://www.jianshu.com/p/7bc2b86c4d89

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值