Android性能优化系列-腾讯matrix卡顿优化之TouchEventLagTracer源码分析

这是性能优化系列之matrix框架的第14篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的小伙伴可以去我主页查看所有关于matrix的分享。

前言

应用导致的卡顿问题,从源头上来讲,基本可以分为三种类型。

首先是Message的生产与消费过程中产生的卡顿,此类型的卡顿监控方案实现来源于消息队列中的Printer机制,通过给Looper设置一个Printer对象从而获取到每个消息执行的耗时情况,再结合方法执行的耗时信息定位卡顿,它监控的是dispatchMessage方法执行的耗时-Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之LooperMonitor源码分析

其次是我们上一篇分享的,IdleHandler导致的卡顿类型,这种卡顿发生在消息队列空闲时,它的监控方案是通过hook拿到每个IdleHandler执行queueIdle的时机,然后做处理,它发生在MessageQueue的next方法拿不到消息的情况下,与第一种消息执行时的时机互为补充-Android性能优化系列-腾讯matrix卡顿优化之IdleHandlerLagTracer源码分析

最后就是我们今天要分享的,点击事件导致的卡顿,这种类型的卡顿问题,前两种监控方案都无法覆盖到,所以需要有单独的方案进行监控。熟悉Android系统输入事件的读者应该清楚,当屏幕发生触摸事件时,是由系统native层的两个线程先进行处理的,InputReader线程负责从输入设备中读取输入事件,拿到事件后进行包装,再由InputDispatcher线程负责将事件分发给可以处理当前事件的窗口。这个分发的过程使用的socket进行通信,过程如下(图片来源于互联网):

所以一个点击事件从产生到完成的过程就可以从socket的发送与接收入手。通过hook到libinput.so中的recvfrom和sendto方法就可以实现对点击事件的卡顿监控,matrix中的TouchEventLagTracer就是按照这个思路实现的。 。

接下来我们进入代码分析,同样还是这几个关键入口:

  • 构造方法
  • onStartTrace
  • onStopTrace

构造方法

arduino 复制代码
public TouchEventLagTracer(TraceConfig config) {
    traceConfig = config;
}

onStartTrace

onStartTrace会调用到onAlive方法。onAlive方法中判断如果开启了TouchEventTrace开关,则调用nativeInitTouchEventLagDetective方法进入native层初始化监控,我们前边提到了的hook socket的方案,就是在这里进行的处理。

scss 复制代码
@Override
public synchronized void onAlive() {
    super.onAlive();
    if (traceConfig.isTouchEventTraceEnable()) {
        nativeInitTouchEventLagDetective(traceConfig.touchEventLagThreshold);
    }
}

nativeInitTouchEventLagDetective方法对应的native层的实现在MatrixTracer.cc中,我们进入这个类找到它。可以看到这里hook了libinput.so的三个方法:__sendto_chk,sendto,recvfrom。

scss 复制代码
static void nativeInitTouchEventLagDetective(JNIEnv *env, jclass, jint threshold) {
    //hook了libinput.so的三个方法:__sendto_chk,sendto,recvfrom。
    xhook_grouped_register(HOOK_REQUEST_GROUPID_TOUCH_EVENT_TRACE, ".*libinput\\.so$", "__sendto_chk",
                           (void *) my_sendto, (void **) (&original_sendto));
    xhook_grouped_register(HOOK_REQUEST_GROUPID_TOUCH_EVENT_TRACE, ".*libinput\\.so$", "sendto",
                           (void *) my_sendto, (void **) (&original_sendto));
    xhook_grouped_register(HOOK_REQUEST_GROUPID_TOUCH_EVENT_TRACE, ".*libinput\\.so$", "recvfrom",
                           (void *) my_recvfrom, (void **) (&original_recvfrom));
    xhook_refresh(true);
    //启动子线程,传入阈值,超过阈值则认为发生了卡顿
    TouchEventTracer::start(threshold);
}

接下来的关键点就是这三个了。我们先看看两个hook点做了什么。

  • my_sendto
  • my_recvfrom
  • TouchEventTracer

my_recvfrom

拦截到recvfrom方法后,先调用原来的额recefrom方法完成系统调用,然后判断当条件满足时,执行TouchEventTracer的touchRecv方法。

arduino 复制代码
ssize_t my_recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen) {
    long ret = original_recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
    if (currentTouchFd == sockfd && inputHasSent && ret > VALIDATE_RET) {
        TouchEventTracer::touchRecv(sockfd);
    }
    return ret;
}

my_sendto

拦截到sendto方法,调用系统的sendto完成调用,后边才是关键,将标记为inputHasSent设置为true,然后调用TouchEventTracer的touchSendFinish方法,此方法和touchRecv成对出现,标志着一次触摸事件的完成。

arduino 复制代码
ssize_t my_sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen) {
    //拦截到sendto方法,调用系统的sendto完成调用
    long ret = original_sendto(sockfd, buf, len, flags, dest_addr, addrlen);
    if (ret >= 0) {
        inputHasSent = true;
        TouchEventTracer::touchSendFinish(sockfd);
    }
    return ret;
}

既然发送和接收的处理都在TouchEventTracer中,那么我们将目光转向TouchEventTracer这个类,看看它的内部实现。

TouchEventTracer

在前边初始化hook的时候,先调用了start方法开启线程,线程开启了一个无限循环recvQueueLooper。

start

ini 复制代码
void TouchEventTracer::start(int threshold) {
    LAG_THRESHOLD = threshold / 1000;
    loopRunning = true;
    thread recvThread = thread(recvQueueLooper);
    recvThread.detach();
}

recvQueueLooper是一个无限循环,它会不断的去检测lastRecvTouchEventTimeStamp这个值,当这个值与当前时间间隔的差值超过我们设置的阈值LAG_THRESHOLD后,就认为发生了卡顿,然后调用onTouchEventLagDumpTrace方法,onTouchEventLagDumpTrace方法会回调到Java层TouchEventLagTracer中的onTouchEventLagDumpTrace方法获取到当前堆栈的信息。

scss 复制代码
void recvQueueLooper() {
    queueMutex.lock();
    while (loopRunning) {
        if (time(nullptr) - lastRecvTouchEventTimeStampNow >= LAG_THRESHOLD && startDetect) {
            lagFd = currentFd;
            //回到Java层获取堆栈信息
            onTouchEventLagDumpTrace(currentFd);
            queueMutex.lock();
        }
    }
}

上边我们提到了recvQueueLooper会不断的去检测lastRecvTouchEventTimeStamp这个值,那么lastRecvTouchEventTimeStamp这个值来自哪?接下来我们看下TouchEventTracer的另外两个方法实现。

touchRecv

这个方法被调用说明应用收到了触摸事件,在这个方法中更新了lastRecvTouchEventTimeStamp的值,这里更新值之后,因为此时循环线程一直在不断的检测当前时间和lastRecvTouchEventTimeStamp的差值是否超过LAG_THRESHOLD,超过了之后就会记录堆栈信息。

ini 复制代码
void TouchEventTracer::touchRecv(int fd) {
    currentFd = fd;
    lagFd = 0;
    if (lagFd == fd) {
        lastRecvTouchEventTimeStamp = 0;
    } else {
        //更新当前时间值
        lastRecvTouchEventTimeStamp = time(nullptr);
        queueMutex.unlock();
    }
}

touchSendFinish

这个方法被调用说明触摸事件已经被消费完成,假如此时循环线程中判断时长超过了设定阈值,那么卡顿发生,则lagFd == fd条件满足,调用Java层上报卡顿信息。

ini 复制代码
void TouchEventTracer::touchSendFinish(int fd) {
    if (lagFd == fd) {
        //上报信息
        reportLag();
    }
    lagFd = 0;
    //更新为0
    lastRecvTouchEventTimeStamp = 0;
    startDetect = true;
}

onStopTrace

onStopTrace会调用到onDead方法,可以看到这里没做什么逻辑。

typescript 复制代码
@Override
public void onDead() {
    super.onDead();
}

总结

总结一下,触摸事件的卡顿监控实现逻辑很明确,就是通过hook底层的socket发送的sendto和recvfrom方法,从而实现对一次触摸事件执行消耗时长的监控。当调用到了recvfrom方法时,说明应用接收到了Touch事件,当调用到了sendto方法时,说明这个Touch事件已经消费掉了,两者的时间相差过大时即说明产生了一次触摸事件的卡顿。

相关推荐
Lee川31 分钟前
从回调地狱到同步之美:JavaScript异步编程的演进之路
javascript·面试
二流小码农1 小时前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少2 小时前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker2 小时前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋3 小时前
Android 协程时代,Handler 应该退休了吗?
android
哈里谢顿13 小时前
1000台裸金属并发创建中的重难点问题分析
面试
哈里谢顿13 小时前
20260303面试总结(全栈)
面试
火柴就是我16 小时前
让我们实现一个更好看的内部阴影按钮
android·flutter
over69718 小时前
从 LLM 到全栈 Agent:MCP 协议 × RAG 技术如何重构 AI 的“做事能力”
面试·llm·mcp
bluceli19 小时前
前端性能优化实战指南:让你的网页飞起来
前端·性能优化