ArkUI Engine - 深入ANR机制 引言 Watc

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

引言

本篇是ArkUI Engine 系列的第五篇,通过前四篇文章,相信读者能够掌握一个ArkUI控件最重要的绘制过程与事件绑定过程的原理了,控件的绘制是Engine中的主要流程。当然,Engine做的不只是UI的绘制工作,还有一个流畅度监控系统,即WatchDog机制。

通过学习本篇,你将了解到鸿蒙的WatchDog机制与ANR(应用无响应)判定相关的代码细节,方便我们进行后续的性能监控与优化。

WatchDog

无论是哪个UI系统,都有着系统流畅度监控的需求,鸿蒙也不例外,当我们遇到以下代码时,点击Text就会进入死循环,此时我们再次进行点击事件,就会出现我们熟知的ANR弹窗

1
2
3
4
5
6
7
8
scss复制代码Column() {
Text(this.father.name)
.width("200vp")
.onClick(() => {
while (true){

}
})

ANR弹窗如下:

ANR的检测,其实就通过WatchDog 机制完成的,下面我们来详细了解一下WatchDog机制

WatchDog 初始化

WatchDog机制中,有两个相关的类,一个是Watchers 结构体,另一个是WatchDog类,WatchDog中会有一个持有着value为Watchers的map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
arduino复制代码namespace OHOS::Ace {
class ThreadWatcher;

struct Watchers {
RefPtr<ThreadWatcher> jsWatcher;
RefPtr<ThreadWatcher> uiWatcher;
};

class WatchDog final : public Referenced {
public:
WatchDog();
~WatchDog() override;

void Register(int32_t instanceId, const RefPtr<TaskExecutor>& taskExecutor, bool useUIAsJSThread);
void Unregister(int32_t instanceId);
void BuriedBomb(int32_t instanceId, uint64_t bombId);
void DefusingBomb(int32_t instanceId);

private:
std::unordered_map<int32_t, Watchers> watchMap_;

ACE_DISALLOW_COPY_AND_MOVE(WatchDog);
};

} // namespace OHOS::Ace

WatchDog在构造函数的时候,会创建启动一个AnrThread

1
2
3
4
5
6
7
scss复制代码WatchDog::WatchDog()
{
AnrThread::Start();
#if defined(OHOS_PLATFORM) || defined(ANDROID_PLATFORM)
AnrThread::PostTaskToTaskRunner(InitializeGcTrigger, GC_CHECK_PERIOD);
#endif
}

AnrThread定义也很简单,它用于一个事件循环的能力,即像Android的Looper一样不断进行事件的分发

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码namespace OHOS::Ace {
class AnrThread {

public:
static void Start();
static void Stop();
using Task = std::function<void()>;
static bool PostTaskToTaskRunner(Task&& task, uint32_t delayTime);
};
} // namespace OHOS::Ace
#endif

事件分发的能力是由TaskRunnerAdapter类提供的,TaskRunnerAdapter抽象了事件分发的能力,它的事件可以是任何具备能力分发的类提供,比如(OHOS::AppExecFwk::EventRunner)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
arduino复制代码namespace {
需要一个TaskRunnerAdapter,用于事件的分发
RefPtr<TaskRunnerAdapter> g_anrThread;
} // namespace

void AnrThread::Start()
{
if (!g_anrThread) {
g_anrThread = TaskRunnerAdapterFactory::Create(false, "anr");
}
}

void AnrThread::Stop()
{
g_anrThread.Reset();
}

bool AnrThread::PostTaskToTaskRunner(Task&& task, uint32_t delayTime)
{
if (!g_anrThread || !task) {
return false;
}

if (delayTime > 0) {
g_anrThread->PostDelayedTask(std::move(task), delayTime, {});
} else {
g_anrThread->PostTask(std::move(task), {});
}
return true;
}
} // namespace OHOS::Ace

初始化的动作很简单,即启动一个具备事件循环机制的类,用于后面进行事件的循环分发,同时当前平台如果定义了这两个宏情况下OHOS_PLATFORM或者ANDROID_PLATFORM, 那么将会发起第一个事件,用于GC信号的注册。没错,Engine中需要通过信号触发GC,通过注册自定义信号SIGNAL_FOR_GC(60)来进行信号绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码
void InitializeGcTrigger()
{
// Record watch dog thread as signal handling thread
g_signalThread = pthread_self();

int32_t result = BlockGcSignal();
if (result != 0) {
LOGE("Failed to block GC signal, errno = %{public}d", result);
return;
}

// Start to receive GC signal
signal(SIGNAL_FOR_GC, OnSignalReceive);
// Start check GC signal
CheckGcSignal();
}

CheckGcSignal 通过sigtimedwait函数,用于当一定时间内等待信号来临,如果在时间内有收到信号,那么顺利执行AceEngine::Get().TriggerGarbageCollection();方法进行GC。(sigtimedwait 超时时result会小于0同时errno会被设置为EAGAIN,同时判断EINTR的目的是其他信号来临时也会打断sigtimedwait调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
scss复制代码void CheckGcSignal()
{
// Check if GC signal is in pending signal set
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet, SIGNAL_FOR_GC);
struct timespec interval = {
.tv_sec = 0,
.tv_nsec = 0,
};
int32_t result = sigtimedwait(&sigSet, nullptr, &interval);
if (result < 0) {
if (errno != EAGAIN && errno != EINTR) {
LOGE("Failed to wait signals, errno = %{public}d", errno);
return;
}
} else {
ACE_DCHECK(result == SIGNAL_FOR_GC);

// Start GC
LOGE("Receive GC signal");
AceEngine::Get().TriggerGarbageCollection();
}

// Check again
AnrThread::AnrThread::PostTaskToTaskRunner(CheckGcSignal, GC_CHECK_PERIOD);
}

至此,WatchDog事件循环机制已经完成初始化,可以接受后面的“埋炸弹”与“拆炸弹”动作了

ANR机制

WatchDog 通过暴露Register 方法,提供给Engine以外的模块进行注册,注册之后就可以使用WatchDog的监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码void WatchDog::Register(int32_t instanceId, const RefPtr<TaskExecutor>& taskExecutor, bool useUIAsJSThread)
{
Watchers watchers = {
.jsWatcher = AceType::MakeRefPtr<ThreadWatcher>(instanceId, TaskExecutor::TaskType::JS),
.uiWatcher = AceType::MakeRefPtr<ThreadWatcher>(instanceId, TaskExecutor::TaskType::UI, useUIAsJSThread),
};
watchers.uiWatcher->SetTaskExecutor(taskExecutor);
if (!useUIAsJSThread) {
watchers.jsWatcher->SetTaskExecutor(taskExecutor);
} else {
watchers.jsWatcher = nullptr;
}
const auto resExecutor = watchMap_.try_emplace(instanceId, watchers);
if (!resExecutor.second) {
LOGW("Duplicate instance id: %{public}d when register to watch dog", instanceId);
}
}

在ArkTS环境中,WatchDog只会创建uiWatcher并赋值给结构体(Watchers的uiWatcher),它是一个ThreadWatcher对象

ThreadWatcher对象初始化的时候,将启动检查,通过AnrThread::PostTaskToTaskRunner启动了一个检查任务

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码ThreadWatcher::ThreadWatcher(int32_t instanceId, TaskExecutor::TaskType type, bool useUIAsJSThread)
: instanceId_(instanceId), type_(type), useUIAsJSThread_(useUIAsJSThread)
{
InitThreadName();
AnrThread::PostTaskToTaskRunner(
[weak = Referenced::WeakClaim(this)]() {
auto sp = weak.Upgrade();
CHECK_NULL_VOID(sp);
调用了ThreadWatcherCheck方法
sp->Check();
},
NORMAL_CHECK_PERIOD);
}

Check方法是整个ANR机制中最核心的实现,下面我们来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
ini复制代码
void ThreadWatcher::Check()
{
int32_t period = NORMAL_CHECK_PERIOD;
if (!IsThreadStuck()) {
if (state_ == State::FREEZE) {
RawReport(RawEventType::RECOVER);
}
freezeCount_ = 0;
state_ = State::NORMAL;
canShowDialog_ = true;
showDialogCount_ = 0;
} else {
if (state_ == State::NORMAL) {
HiviewReport();
RawReport(RawEventType::WARNING);
state_ = State::WARNING;
period = WARNING_CHECK_PERIOD;
} else if (state_ == State::WARNING) {
RawReport(RawEventType::FREEZE);
state_ = State::FREEZE;
period = FREEZE_CHECK_PERIOD;
DetonatedBomb();
} else {
if (!canShowDialog_) {
showDialogCount_++;
if (showDialogCount_ >= ANR_DIALOG_BLOCK_TIME) {
canShowDialog_ = true;
showDialogCount_ = 0;
}
}

if (++freezeCount_ >= 5) {
RawReport(RawEventType::FREEZE);
freezeCount_ = 0;
}
period = FREEZE_CHECK_PERIOD;
DetonatedBomb();
}
}
check任务完成后,继续进行check任务
AnrThread::PostTaskToTaskRunner(
[weak = Referenced::WeakClaim(this)]() {
auto sp = weak.Upgrade();
CHECK_NULL_VOID(sp);
sp->Check();
},
period);
}

为了理解上面的代码,我们简单总结一下上面提到的三种状态,分别是NORMAL,WARNING,FREEZE

image.png

NORMAL

NORMAL状态是正常的状态,我们可以看到,当IsThreadStuck返回false时,state变量就会被设置为NORMAL状态,我们看一下IsThreadStuck方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码bool ThreadWatcher::IsThreadStuck()
{
...
关键的判断逻辑在这里
if (((loopTime_ - threadTag_) > (lastLoopTime_ - lastThreadTag_)) && (lastTaskId_ == taskId)) {
std::string abilityName;
if (AceEngine::Get().GetContainer(instanceId_) != nullptr) {
abilityName = AceEngine::Get().GetContainer(instanceId_)->GetHostClassName();
}
LOGE("thread stuck, ability: %{public}s, instanceId: %{public}d, thread: %{public}s, looptime: %{public}d, "
"checktime: %{public}d",
abilityName.c_str(), instanceId_, threadName_.c_str(), loopTime_, threadTag_);
res = true;
}
lastTaskId_ = taskId;
lastLoopTime_ = loopTime_;
lastThreadTag_ = threadTag_;
}
CheckAndResetIfNeeded();
PostCheckTask();
return res;
}

这里面涉及了非常关键的两个变量loopTime_ ,与threadTag_ 。我们可以想一下,ANR如果发生时,必定是消息循环的某个消息执行时间过长才会导致的,那么如何判断消息执行时间呢?就靠这两个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
scss复制代码
void ThreadWatcher::PostCheckTask()
{
auto taskExecutor = taskExecutor_.Upgrade();
if (taskExecutor) {
// post task to specified thread to check it
taskExecutor->PostTask(
[weak = Referenced::WeakClaim(this)]() {
auto sp = weak.Upgrade();
CHECK_NULL_VOID(sp);
每次真正执行一个task,threadTag_ 才会自增
sp->TagIncrease();
},
type_);
std::unique_lock<std::shared_mutex> lock(mutex_);
每次调用PostCheckTask的时候,loopTime_都会自增
++loopTime_;
....
}

void ThreadWatcher::TagIncrease()
{
std::unique_lock<std::shared_mutex> lock(mutex_);
++threadTag_;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码        } else if (state_ == State::WARNING) {
RawReport(RawEventType::FREEZE);
state_ = State::FREEZE;
period = FREEZE_CHECK_PERIOD;
DetonatedBomb();
} else {
if (!canShowDialog_) {
showDialogCount_++;
if (showDialogCount_ >= ANR_DIALOG_BLOCK_TIME) {
canShowDialog_ = true;
showDialogCount_ = 0;
}
}

if (++freezeCount_ >= 5) {
RawReport(RawEventType::FREEZE);
freezeCount_ = 0;
}
period = FREEZE_CHECK_PERIOD;
DetonatedBomb();
}

“引爆炸弹”&“埋炸弹”&“拆炸弹”

我们上面说到的“引爆炸弹”,其实就是指DetonatedBomb函数,它用于触发ANR任务,如果满足条件的情况下。

当然,DetonatedBomb并不是调用了就会产生ANR弹窗,而是会判断inputTaskIds_中第一个任务与当前运行任务的时间差值是否大于ANR_INPUT_FREEZE_TIME(5000 即5s),如果大于这个阈值那么毫无疑问是一个ANR,否则就只是一个卡顿。如果canShowDialog_为true,那么就调用ShowDialog方法弹出ANR弹窗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
c复制代码void ThreadWatcher::DetonatedBomb()
{
std::shared_lock<std::shared_mutex> lock(mutex_);
会先判断inputTaskIds_这个队列是否为空
if (inputTaskIds_.empty()) {
return;
}

uint64_t currentTime = GetMilliseconds();
uint64_t bombId = inputTaskIds_.front();

if (currentTime - bombId > ANR_INPUT_FREEZE_TIME) {
LOGE("Detonated the Bomb, which bombId is %{public}s and currentTime is %{public}s",
std::to_string(bombId).c_str(), std::to_string(currentTime).c_str());
if (canShowDialog_) {
ShowDialog();
canShowDialog_ = false;
showDialogCount_ = 0;
} else {
LOGE("Can not show dialog when detonated the Bomb.");
}
ANR判定成功后会把整个炸弹队列清除
std::queue<uint64_t> empty;
std::swap(empty, inputTaskIds_);
}
}

inputTaskIds_变量其实是一个队列

1
arduino复制代码std::queue<uint64_t> inputTaskIds_;

使用者可以通过BuriedBomb进行“埋炸弹”,用于关键的流程进行ANR判断

1
2
3
4
5
arduino复制代码void ThreadWatcher::BuriedBomb(uint64_t bombId)
{
std::unique_lock<std::shared_mutex> lock(mutex_);
inputTaskIds_.emplace(bombId);
}

当然,使用者也可以通过DefusingBomb方法进行“拆炸弹”

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码void ThreadWatcher::DefusingBomb()
{
auto taskExecutor = taskExecutor_.Upgrade();
CHECK_NULL_VOID(taskExecutor);
taskExecutor->PostTask(
[weak = Referenced::WeakClaim(this)]() {
auto sp = weak.Upgrade();
if (sp) {
sp->DefusingTopBomb();
}
},
type_);
}

本质都是对这个队列的元素进行增删操作,因为后续触发DetonatedBomb方法的时候,会先判断inputTaskIds_是否为空,如果为空的情况下,那么其实就算消息延迟也不算为ANR。

总结

通过本章,我们学习到Engine提供的WatchDog机制以及其ANR实现的原理,通过学习这些源码,我们将会对整个ArkUIEngine更加的熟悉,方便我们进行后续的监控或者优化。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%