ArkUI Engine - 深入ANR机制

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

引言

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

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

WatchDog

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

scss 复制代码
Column() {
  Text(this.father.name)
    .width("200vp")
    .onClick(() => {
      while (true){

      }
    })

ANR弹窗如下:

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

WatchDog 初始化

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

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

scss 复制代码
WatchDog::WatchDog()
{
    AnrThread::Start();
#if defined(OHOS_PLATFORM) || defined(ANDROID_PLATFORM)
    AnrThread::PostTaskToTaskRunner(InitializeGcTrigger, GC_CHECK_PERIOD);
#endif
}

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

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)

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)来进行信号绑定

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调用)

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的监控

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启动了一个检查任务

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机制中最核心的实现,下面我们来看一下代码

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

NORMAL

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

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如果发生时,必定是消息循环的某个消息执行时间过长才会导致的,那么如何判断消息执行时间呢?就靠这两个变量

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函数"引爆炸弹"

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弹窗

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_变量其实是一个队列

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

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

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

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

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更加的熟悉,方便我们进行后续的监控或者优化。

相关推荐
GoldKey43 分钟前
gcc 源码阅读---语法树
linux·前端·windows
Xf3n1an2 小时前
html语法
前端·html
张拭心2 小时前
亚马逊 AI IDE Kiro “狙击”Cursor?实测心得
前端·ai编程
烛阴2 小时前
为什么你的Python项目总是混乱?层级包构建全解析
前端·python
CYRUS_STUDIO2 小时前
深入 Android syscall 实现:内联汇编系统调用 + NDK 汇编构建
android·操作系统·汇编语言
@大迁世界3 小时前
React 及其生态新闻 — 2025年6月
前端·javascript·react.js·前端框架·ecmascript
红尘散仙3 小时前
Rust 终端 UI 开发新玩法:用 Ratatui Kit 轻松打造高颜值 CLI
前端·后端·rust
死也不注释3 小时前
【第一章编辑器开发基础第一节绘制编辑器元素_6滑动条控件(6/7)】
android·编辑器
新酱爱学习3 小时前
前端海报生成的几种方式:从 Canvas 到 Skyline
前端·javascript·微信小程序
袁煦丞4 小时前
把纸堆变数据流!Paperless-ngx让文件管理像打游戏一样爽:cpolar内网穿透实验室第539个成功挑战
前端·程序员·远程工作