flutter 卡顿侦测

Flutter卡顿侦测

前言

在使用以及开发flutter的过程中,往往会遇到很多的不同的问题。其中,卡顿问题尤为突出,其原因是,如果在某些场景下,造成了卡顿,对用户的使用体验,影响非常大。因此,需要一定的方法,能够在flutter发生卡顿的时候,捕获到flutter的卡顿信息(最好是当前的 main isolate 在干什么),之后便着手进行卡顿的优化项。

如何感知到flutter发生了卡顿?

Flutter单线程模型

回想一下flutter的相关知识:flutter是一个单线程模型的语言,他的所有的任务,在没有特意开启 other isolate 的情况下,都是运行在main isolate 中的。并且,flutter是基于消息的队列的形式进行处理任务的。

flutter任务模型.png

根据上图中可以得知,flutter的运行过程中,会产生两种消息:

  • microtask
  • event

并且可以得知,flutter在处理的时候,是优先处理microtask任务的。

什么是卡顿?

说实话,卡顿其实是人眼觉得屏幕掉帧了,并且感觉到不流畅,就认为是卡顿了。(其实是一个很笼统的概念。- . - )

在Android中,如果屏幕的刷新率是60hz的,那么在主线程中,如果每一个帧超过了 16.66ms, 那么认为该app在运行的过程中,发生了丢帧的行为,也就是发生了卡顿。这个时候,如果突然间卡了几秒中,就会出现了 Android 开发者都遇到过的 ANR 的行为了。

Android中如何侦测卡顿?

在Android的侦测卡顿中,它是侦测 Looper 中的函数执行时间的,而这个 Looper ,是 main looper。如何做? 在Looper执行的消息循环里面,取出每一个消息,之后记录下执行前的时间,随后,在处理完成这个消息的时候,记录了下执行后的时间。如果执行前的时间与执行后的时间的差值是大于了某一个阈值的,那么任务该函数方法执行过长,将会导致卡顿的发生。

public static void loop () {
	...
    for (;;) {
        Message msg = queue.next();
        if (msg == null) {
            return;
        }
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println("...");
        }
        
        ...
        
        if (logging != null) {
            logging.println("...");
        }
    }
}

因此,在Android中,完全可以通过侦测 dispatchMessage(msg) 的执行时间,计算到函数的执行时间差值,判断是否发生了卡顿。

到达了这个时间阈值之后,我们便可以读取当前的线程的堆栈信息,之后便能得到了当前的卡顿数据。之后爱怎么搞就随你啦~

Flutter中如何侦测卡顿?

根据Android的卡顿侦测方法推演,那么,是否可以在flutter的框架下,侦测到flutter的代码执行流程?

貌似真的可以。

我们在main isolate 中打开一个worker isolate,并且在worker isolate中不断的发送一些microtask信息到消息队列中。这个时候,如果main isolate要是有空的话,会优先执行 microtask 的信息,之后再执行 event。因此,可以做一个简易版本的 ping - pong形式的卡顿侦测。因为这样只能侦测到 event 事件执行所带来的卡顿,并不能检测到 microtask 所带来的卡顿数据。(这是简易版本,也是目前我所做的东西,后面再升级优化吧.....)

flutter超时炸弹发送流程图.jpg

通过这样的形式,就能做一个简单的flutter的卡顿侦测工具了。并且能够简单的感知到flutter的卡顿了。

如何获取堆栈信息?

堆栈?

什么是堆栈?为什么称程序执行的函数叫做堆栈信息?

翻开小本本,它上面写道:

  • 堆栈是计算机中广泛运用的技术,通常用于保存中断断点、保存子程序调用返回点、保存CPU现场数据等,也用于程序间传递参数。
  • 堆(Heap)
    • 主要用来存放对象,为栈提供数据存储服务。堆是动态分配和释放的,因此可以用来存储需要动态分配和释放的数据。例如,在函数调用中,如果函数中有局部变量,这些变量通常会在栈上创建。而如果在函数中动态分配了内存(例如通过malloc函数),这些内存通常会在堆上创建。
  • 栈(Stack)
    • 主要用来执行程序。栈上的数据是有限的,大小通常由操作系统设定。当一个函数被调用时,会在栈上为其分配一块内存,用来存储局部变量和函数调用的上下文信息。当函数调用结束时,这块内存会被自动释放。因此,栈可以用来存储需要在函数间共享的数据,但是需要注意避免栈溢出的问题。

简而言之就是

    • 控制一段自己的存储空间,叫堆空间
    • 程序运行时动态分配的,一般是程序运行时,请求操作空间分配给自己内存
    • 操作系统建立某个进程或者线程时,为线程建立的存储空间
    • 会存储着函数的执行顺序
    • 特性,栈顶进入,栈顶出

CPU是如何执行代码的?

其实,在使用任何一门语言时,在程序执行的过程中,机器都会一条条的执行你的命令。CPU会首先读取【程序计数器PC】的值,之后CPU的【控制单元】操作【地址总线】,访问这个【程序计数器】的内存地址,紧接着通知内存准备数据。数据准备好之后将【数据总线】的指令传给CPU,之后CPU收到数据,传入【指令寄存器】中。

CPU执行完成了指令之后,【程序计数器】自增,指向代码片段的下一条指令。自增是由CPU的架构所决定的。一般情况下,32位的CPU自增4(需要4个内存地址存放),64位CPU自增8(需要8个内存地址存放).

看下面的cpp代码,以及cpp代码汇编形式,来说明程序的执行流程。

#include <iostream>

int functionB(int a, int b)
{
    return a + b;
}

int functionA(int a, int b)
{
    int c = a + b;
    int d = a - b;
    return functionB(c, d);
}

int main()
{
    int a = 1, b = 2;
    functionA(a, b);
    return 0;
}

这是他的汇编代码。

  • 请注意,我是在MacOS Sonoma 14.7,系统上进行编译的,并且我的晶片是:Apple M2 Pro,如下代码隶属于arm64的代码汇编。
  • 可以使用 g++ -S -o demo.s demo.cpp 进行从.cpp编译出.s汇编语言

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 14, 0	sdk_version 14, 2
	.globl	__Z9functionBii                 ; -- Begin function _Z9functionBii ; functionB 的开始
	.p2align	2							; 对齐4字节边界
__Z9functionBii:                        ; @_Z9functionBii
	.cfi_startproc							; 开始函数的CFI指令
; %bb.0:
	sub	sp, sp, #16							; 局部变量分配16字节的栈空间
	.cfi_def_cfa_offset 16					; 更新CFA偏移
	str	w0, [sp, #12]						; 保存第一个参数 w0 到 sp 栈指针偏移12个字节的地方
	str	w1, [sp, #8]						; 保存第二个参数 w1 到 sp 栈指针偏移8个字节的地方      ; 这个存储位置,可以看出这个数据是占据4个字节的
	ldr	w8, [sp, #12]						; 从 sp 栈指针偏移 12个字节,加载第一个参数到 w8 寄存器
	ldr	w9, [sp, #8]						; 从 sp 栈指针偏移 8个字节,加载第一个参数到 w9 寄存器
	add	w0, w8, w9							; w0 = w8 + w9
	add	sp, sp, #16							; 恢复栈指针
	ret										; 返回
	.cfi_endproc
                                        ; -- End function
	.globl	__Z9functionAii                 ; -- Begin function _Z9functionAii
	.p2align	2
__Z9functionAii:                        ; @_Z9functionAii	;方法 functionA
	.cfi_startproc						; 开始函数的CFI指令
; %bb.0:
	sub	sp, sp, #32						; sp指针分配32空间,为局部变量分配空间。
	.cfi_def_cfa_offset 32
	stp	x29, x30, [sp, #16]             ; 16-byte Folded Spill	; 将x29和x30寄存器的值,保存在栈中
	add	x29, sp, #16					; 更新x29为当前栈顶指针,也就是fp
	.cfi_def_cfa w29, 16				; 
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	stur	w0, [x29, #-4]				; 
	str	w1, [sp, #8]
	ldur	w8, [x29, #-4]
	ldr	w9, [sp, #8]
	add	w8, w8, w9
	str	w8, [sp, #4]
	ldur	w8, [x29, #-4]
	ldr	w9, [sp, #8]
	subs	w8, w8, w9
	str	w8, [sp]
	ldr	w0, [sp, #4]
	ldr	w1, [sp]
	bl	__Z9functionBii
	ldp	x29, x30, [sp, #16]             ; 16-byte Folded Reload
	add	sp, sp, #32
	ret
	.cfi_endproc
                                        ; -- End function
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #32
	.cfi_def_cfa_offset 32
	stp	x29, x30, [sp, #16]             ; 16-byte Folded Spill
	add	x29, sp, #16
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp]                        ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	mov	w8, #1
	str	w8, [sp, #8]
	mov	w8, #2
	str	w8, [sp, #4]
	ldr	w0, [sp, #8]
	ldr	w1, [sp, #4]
	bl	__Z9functionAii
	ldr	w0, [sp]                        ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #16]             ; 16-byte Folded Reload
	add	sp, sp, #32
	ret
	.cfi_endproc
                                        ; -- End function
.subsections_via_symbols


在汇编代码很容易能够看出,使用 [ sp ] 这个东西,基本上整段代码程序都有。因此,在进行堆栈还原时,如何获取 sp 这个值,以及获取存储 sp 的值的寄存器 fp,显得尤为重要。简单归类一个流程就是,

透漏一下:

  • SP: 堆栈寄存器的指针。SP的作用就是指示当前要出栈或入栈的数据,并在操作执行后自动递增或递减。

    至于是入栈递增还是入栈递减,由CPU的生产厂家确定。CPU执行命令的过程

  • 计算机每执行一条指令,大体上可以分为三个阶段:取指令 -> 分析指令 -> 执行指令

    • 取指令
      • 根据程序计数器PC中的值,从程序存储器中读出指令。送到指令寄存器。
    • 分析指令
      • 将指令寄存器的指令取出后,进行转码,分析指令性质。
    • 执行指令
      • 根据对应的指令性质,执行指令内容。之后重复上述操作。
  • FP: ...

  • LR:...

在了解SP指针的作用下,我们又了解了CPU的指令执行形式,并且认清了程序执行的过程中,其指令的执行形式之后,我们便可以开始堆栈获取了。

附录:

参考文章:

Demo:https://gitee.com/wildleolemon/flutter-tracker

posted @ 2024-11-08 10:52  野生的Lemon柠檬  阅读(16)  评论(0编辑  收藏  举报

呱呱呱呱呱🐸