本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
引言
本篇是ArkUI Engine 系列的第五篇,通过前四篇文章,相信读者能够掌握一个ArkUI控件最重要的绘制过程与事件绑定过程的原理了,控件的绘制是Engine中的主要流程。当然,Engine做的不只是UI的绘制工作,还有一个流畅度监控系统,即WatchDog机制。
通过学习本篇,你将了解到鸿蒙的WatchDog机制与ANR(应用无响应)判定相关的代码细节,方便我们进行后续的性能监控与优化。
WatchDog
无论是哪个UI系统,都有着系统流畅度监控的需求,鸿蒙也不例外,当我们遇到以下代码时,点击Text就会进入死循环,此时我们再次进行点击事件,就会出现我们熟知的ANR弹窗
1 | scss复制代码Column() { |
ANR弹窗如下:
ANR的检测,其实就通过WatchDog 机制完成的,下面我们来详细了解一下WatchDog机制
WatchDog 初始化
WatchDog机制中,有两个相关的类,一个是Watchers 结构体,另一个是WatchDog类,WatchDog中会有一个持有着value为Watchers的map
1 | arduino复制代码namespace OHOS::Ace { |
WatchDog在构造函数的时候,会创建启动一个AnrThread
1 | scss复制代码WatchDog::WatchDog() |
AnrThread定义也很简单,它用于一个事件循环的能力,即像Android的Looper一样不断进行事件的分发
1 | arduino复制代码namespace OHOS::Ace { |
事件分发的能力是由TaskRunnerAdapter类提供的,TaskRunnerAdapter抽象了事件分发的能力,它的事件可以是任何具备能力分发的类提供,比如(OHOS::AppExecFwk::EventRunner)
1 | arduino复制代码namespace { |
初始化的动作很简单,即启动一个具备事件循环机制的类,用于后面进行事件的循环分发,同时当前平台如果定义了这两个宏情况下OHOS_PLATFORM或者ANDROID_PLATFORM, 那么将会发起第一个事件,用于GC信号的注册。没错,Engine中需要通过信号触发GC,通过注册自定义信号SIGNAL_FOR_GC(60)来进行信号绑定
1 | scss复制代码 |
CheckGcSignal 通过sigtimedwait函数,用于当一定时间内等待信号来临,如果在时间内有收到信号,那么顺利执行AceEngine::Get().TriggerGarbageCollection();方法进行GC。(sigtimedwait 超时时result会小于0同时errno会被设置为EAGAIN,同时判断EINTR的目的是其他信号来临时也会打断sigtimedwait调用)
1 | scss复制代码void CheckGcSignal() |
至此,WatchDog事件循环机制已经完成初始化,可以接受后面的“埋炸弹”与“拆炸弹”动作了
ANR机制
WatchDog 通过暴露Register 方法,提供给Engine以外的模块进行注册,注册之后就可以使用WatchDog的监控
1 | ini复制代码void WatchDog::Register(int32_t instanceId, const RefPtr<TaskExecutor>& taskExecutor, bool useUIAsJSThread) |
在ArkTS环境中,WatchDog只会创建uiWatcher并赋值给结构体(Watchers的uiWatcher),它是一个ThreadWatcher对象
ThreadWatcher对象初始化的时候,将启动检查,通过AnrThread::PostTaskToTaskRunner启动了一个检查任务
1 | scss复制代码ThreadWatcher::ThreadWatcher(int32_t instanceId, TaskExecutor::TaskType type, bool useUIAsJSThread) |
Check方法是整个ANR机制中最核心的实现,下面我们来看一下代码
1 | ini复制代码 |
为了理解上面的代码,我们简单总结一下上面提到的三种状态,分别是NORMAL,WARNING,FREEZE
NORMAL
NORMAL状态是正常的状态,我们可以看到,当IsThreadStuck返回false时,state变量就会被设置为NORMAL状态,我们看一下IsThreadStuck方法
1 | ini复制代码bool ThreadWatcher::IsThreadStuck() |
这里面涉及了非常关键的两个变量loopTime_ ,与threadTag_ 。我们可以想一下,ANR如果发生时,必定是消息循环的某个消息执行时间过长才会导致的,那么如何判断消息执行时间呢?就靠这两个变量
1 | scss复制代码 |
loopTime_ :每次engine调用PostCheckTask的时候,就会自增
threadTag_: 每次任务被调度的时候,就会自增
正常情况下,loopTime_都约等于threadTag_,调用PostCheckTask的时候如果没有delay的话,理应任务也会被调度。但是如果处在异常情况,比如这个task是一个耗时执行,比如一个死循环被调度,那么这两个变量的差值会随着PostCheckTask的调用被不断增大,从而判定为线程卡顿。当然,这里还同时判断了当前任务与前一个任务的id,两者如果相同,那么就大大证明了这个task存在卡顿。
如果处于无卡顿状态,那么state变量就会被赋值为NORMAL状态。
WARNING
WARNING是一个中间状态,我们在上文IsThreadStuck函数可以看到,执行完IsThreadStuck后就会又调用PostCheckTask函数,再次向消息循环中抛出一个check函数执行。
如果IsThreadStuck返回了false,那么state就会被立即设置为WARNING状态,如果消息循环中的check函数再次被调度时还是IsThreadStuck返回了false,那么就立即升级为FREEZE状态
FREEZE
FREEZE 状态是ANR的充分状态,因为两次消息循环中IsThreadStuck都返回了false,那么此时就会调用DetonatedBomb进行“炸弹引爆”。
值得注意的是,我们还有一个else分支,即多次消息循环中,上一次状态为FREEZE,下一次状态仍然为FREEZE,那么当累计次数达到ANR_DIALOG_BLOCK_TIME(5)次时,将再次把canShowDialog_修改为true(canShowDialog_控制着是否弹出ANR弹窗,当上一次ANR弹窗弹出时会被设置为false,因此只要再超过5次时,就会再次把这个变量设置为true让ANR弹窗再次可弹。)。同样的,如果多次处于FREEZE状态,那么每一次都会调用DetonatedBomb函数“引爆炸弹”
1 | ini复制代码 } else if (state_ == State::WARNING) { |
“引爆炸弹”&“埋炸弹”&“拆炸弹”
我们上面说到的“引爆炸弹”,其实就是指DetonatedBomb函数,它用于触发ANR任务,如果满足条件的情况下。
当然,DetonatedBomb并不是调用了就会产生ANR弹窗,而是会判断inputTaskIds_中第一个任务与当前运行任务的时间差值是否大于ANR_INPUT_FREEZE_TIME(5000 即5s),如果大于这个阈值那么毫无疑问是一个ANR,否则就只是一个卡顿。如果canShowDialog_为true,那么就调用ShowDialog方法弹出ANR弹窗
1 | c复制代码void ThreadWatcher::DetonatedBomb() |
inputTaskIds_变量其实是一个队列
1 | arduino复制代码std::queue<uint64_t> inputTaskIds_; |
使用者可以通过BuriedBomb进行“埋炸弹”,用于关键的流程进行ANR判断
1 | arduino复制代码void ThreadWatcher::BuriedBomb(uint64_t bombId) |
当然,使用者也可以通过DefusingBomb方法进行“拆炸弹”
1 | scss复制代码void ThreadWatcher::DefusingBomb() |
本质都是对这个队列的元素进行增删操作,因为后续触发DetonatedBomb方法的时候,会先判断inputTaskIds_是否为空,如果为空的情况下,那么其实就算消息延迟也不算为ANR。
总结
通过本章,我们学习到Engine提供的WatchDog机制以及其ANR实现的原理,通过学习这些源码,我们将会对整个ArkUIEngine更加的熟悉,方便我们进行后续的监控或者优化。
本文转载自: 掘金