完美体育(中国)-官方Android卡顿掉帧?努比亚技术

 完美真人(中国)官方网站新闻     |      2022-05-12 08:30

  当用户抱怨手机在使用过程中存在卡顿问题的时候,会严重影响用户对手机品牌的好感和应用APP的体验,从而导致用户对手机品牌的忠诚度降低或应用APP的装机留存率下降。

  所以无论是手机设备厂商还是应用APP开发者,又或是Android系统的维护者Google都会对界面卡顿问题非常重视,会将界面的流畅度作为核心性能体验指标进行持续的优化。说到流畅度,本质上就是要解决用户操作手机过程中的界面丢帧问题,本来一秒钟屏幕上需要更新60帧画面,但是由于种种原因,这期间屏幕上只更新了55帧画面,这就是出现丢帧,在用户主观肉眼看来就是感知卡顿。

  那么当出现了丢帧卡顿的问题时,我们该如何着手去分析与优化解决呢?关于这块的内容,笔者将结合多年来的工作经历与理解,分三篇系列文章来讲解:

  在分析任何问题之前,我们都需要先弄清楚其基本原理,也就是要掌握了这个“道”,才能真正着手去分析问题,否则只能是弄得一头雾水,也没法真正的理解和解决问题。所以要想分析并解决界面掉帧卡顿问题,我们就先需要知道在Android系统上应用UI线程到底是如何完成一帧画面的上帧显示动作的(本文讲解的内容主要基于Android原生应用的绘制渲染流程,对于游戏应用和Flutter开发的应用流程会不太一样,由于篇幅所限,本文暂不涉及,可以关注团队后续文章的内容)。由于大部分应用界面的上帧更新画面动作都是由用户手指触摸屏幕而触发,所以本文以手指上下滑动应用界面的操作场景为例,结合Systrace分析一下Android应用上帧显示的原理。

  1. 触摸屏会按照屏幕硬件的触控采样率周期,每隔几毫秒扫描一次,如果有触控事件就会上报到对应的设备驱动;系统封装了一个叫EventHub的对象,它利用inotify和epoll机制/dev/input目录下的input设备驱动节点,通过EventHub的getEvents接口就可以并获取到Input事件;

  3.InputDispatcher在拿到InputReader获取的事件之后,对事件进行包装后,寻找并分发到目标窗口;

  5.OutboundQueue(“oq”)队列里面放的是即将要被派发给各个目标窗口App的事件;

  6. WaitQueue队列里面记录的是已经派发给 App(“wq”),但是 App还在处理没有返回处理成功的事件;

  7.PendingInputEventQueue队列(“aq”)中记录的是应用需要处理的Input事件,这里可以看到input事件已经传递到了应用进程;

  用一张图描述整个过程大致如下(关于这部分详细的Android系统源码实现流程可以参考这篇文章):

  从上面的系统机制的分析可以看出,整个Input触控事件的分发与处理主要涉及到两个进程:一个是system_server系统进程,另一个是当前焦点窗口所属的Setting应用进程。

  2.InputDispatcher被唤醒后会先将事件放到InboundQueue队列(也就是Systrace上看到的“iq”队列)中,然后找到具体处理此input事件的应用目标窗口,并将Input事件放入对应的应用目标窗口的OutboundQueue队列(也就是Systrace上看到的“oq”队列)中,等待进一步通过SocketPair双工信道发送input事件到应用目标窗口中;

  3. 最后当事件发送给具体的应用目标窗口后,会将事件移动到WaitQueue队列中(也就是Systrace上看到的“wq”队列)并一直等待收到到目标应用处理Input事件完成后的反馈后再从队列中移除,如果5秒内没有收到目标应用窗口处理完成此次Input事件的反馈,就会报该应用ANR异常事件。以上整个过程在Android系统AOSP源码中都加有相应的Systrace tag,如下Systrace截图所示:

  1.2.2、应用进程的处理过程:当Input触控事件通过socket传递到Settings应用进程这边后,会唤醒应用的UI线程在ViewRootImpl#deliverInputEvent的流程中进行Input事件的具体分发与处理。具体的处理流程:

  1. 先交给之前在添加应用PhoneWindow窗口时的ViewRootImpl#setView流程中创建的多个不同类型的InputUsage中依次进行处理(比如对输入法处理逻辑的封装ImeInputUsage,某些key类型的Input事件会由它先交给输入法进程处理完后再交给应用窗口的InputUsage处理),整个处理流程是按照责任链的设计模式进行;

  2. 最后会交给负责应用窗口Input事件分发处理的ViewPostImeInputUsage中具体处理,这里面会从View布局树的根节点DecorView开始遍历整个View树上的每一个子View或ViewGroup控件执行事件的分发、拦截、处理的逻辑;

  4. 一次滑动过程的触控交互的InputResponse区域中一般会包含一个Input的ACTION_DOWN事件+多个ACTION_MOVE事件+一个ACTION_UP事件,Settings应用界面中的相关View控件在收到多个ACTION_MOVE触控事件后,经过判断为用户手指滑动行为,一般会调用View#invalidate等相关接口触发UI线程的绘制上帧更新画面的操作,具体流程后文会继续详细分析。以上过程如下Systrace截图所示:

  App应用启动时,在Fork创建进程后会通过反射创建代表应用主线程的ActivityThread对象并执行其main函数,进行UI主线程的初始化工作:

  // 创建主线程的Looper对象,并通过ThreadLocal机制实现与主线程的一对一绑定

  主线程初始化完成后,主线程就进入阻塞状态(进入epoll_wait状态,并释放CPU运行资源),等待 Message,一旦有 Message 发过来,主线程就会被唤醒,处理 Message,处理完成之后,如果没有其他的 Message 需要处理,那么主线程就会进入休眠阻塞状态继续等待。可以说Android系统的运行是受消息机制驱动的,而整个消息机制是由上面所说的四个关键角色相互配合实现的(Handler、Looper、MessageQueue、Message),其运行原理如下图所示:

  在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分:CPU负责计算帧数据,把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把Buffer里的数据呈现到屏幕上。屏幕上显示的内容,是从Buffer图像帧缓冲区中读取的,大致读取过程为:从Buffer的起始地址开始,从上往下,从左往右扫描整个Buffer,将内容映射到显示屏上。如下图所示:

  当然,屏幕上显示的内容需要不断的更新,如果在同一个Buffer进行读取和写入操作,将会导致屏幕显示多帧内容而出现显示错乱。所以硬件层除了提供一个Buffer用于屏幕显示,还会提供了一个Buffer用于后台的CPU/GPU图形绘制与合成,也就是我们常说的双缓冲:让绘制和显示器拥有各自的Buffer:CPU/GPU 始终将完成的一帧图像数据写入到 后缓存区(Back Buffer),而显示器使用前缓存区( Front Buffer),当屏幕刷新时,Front Buffer 并不会发生变化,当Back Buffer准备就绪后,它们才进行交换。如下图所示:

  理想情况下假设前一帧显示完成,后一帧数据就准备好了,屏幕开始读取下一帧内容进行显示,也就是开始读取上图中的后缓冲区的内容:

  此时,前后缓冲区进行一次角色交换,之前的后缓冲区变为前缓冲区,进行图形的显示,之前的前缓冲区则变为后缓冲区,进行图形的绘制合成。然而,理想很丰满,现实很骨感,上面假设“当前一帧显示完毕,后一帧准备好了”的情况,在现实中这两个事件并非同时完成。那么,屏幕读取缓冲区的速度和系统绘制合成帧的速度之间有什么关系呢,带着这个疑惑我们看看下面两个基本概念:

  1.屏幕刷新率(Screen Refresh Rate): 屏幕刷新率是一个硬件的概念,单位是Hz(赫兹),是说屏幕这个硬件刷新画面的频率:举例来说,60Hz 刷新率意思是:这个屏幕在 1 秒内,会刷新显示内容60 次;那么对应的,90Hz 是说在 1 秒内刷新显示内容 90 次。

  2.帧率(Frame Rate): 与屏幕刷新率对应的,帧率是一个软件的概念,单位是FPS(Frame Per Second ),表示 CPU/GPU 在一秒内绘制合成产生的帧数,意思是每秒产生画面的个数,FPS 的值是由软件系统决定的。举例来说,60FPS 指的是每秒产生 60 个画面;90FPS 指的是每秒产生 90 个画面。

  此时,在前缓冲区内容全部映射到屏幕上之后,后缓冲区尚未准备好下一帧,屏幕将无法读取下一帧,所以只能继续显示当前一帧的图形,造成一帧显示多次,也就是卡顿。

  此时,屏幕未完全把前缓冲区的一帧映射到屏幕,而系统已经在后缓冲区准备好了下一帧,并要求读取下一帧到屏幕,将会导致屏幕上半部分是上一帧的图形,而下半部分是下一帧的图形,造成屏幕上显示多帧,也就是屏幕撕裂现象,如下图所示:

  所以上面两种情况,都会导致问题,根本原因就是两个缓冲区的操作速率不一致。解决办法就是:让屏幕控制前后缓冲区的切换时机,让系统帧速率配合屏幕刷新率的节奏。那么屏幕是如何控制这个节奏的呢?

  答案就是垂直同步(VSync):当屏幕从缓冲区扫描完一帧到屏幕上之后,开始扫描下一帧之前,中间会有一个时间间隙,称为Vetrical Blanking Interval (VBI),这个时间点其实就是进行前后缓存区交换的最佳时机,此时屏幕并没有在刷新,也就避免了屏幕撕裂现象的产生,所以在此时发出的一个同步Vsync信号,该信号用来切换前缓冲区和后缓冲区(本质就是内存地址的交换,瞬间即可完成),即可达到最佳效果。

  通过上面的分析可以看出:屏幕的显示节奏是由屏幕刷新率的硬件参数决定且固定的,软件操作系统需要配合屏幕的显示,在固定的时间内准备好下一帧,以供屏幕进行显示,两者通过VSync信号来实现同步(VSync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了)。

  在Android 4.1之前,屏幕刷新也遵循上面介绍的 双缓存+VSync 机制,整个流程与架构借用2012年Google I/O大会上展示的一张图如下所示:

  2. GPU:代表使用OpenGl库指令操作GPU硬件对CPU生成的纹理数据进行渲染和栅格化以及合成等操作;

  • 横轴表示时间,每个长方形表示Buffer的使用,长方形的宽度代表使用时长,VSync代表垂直同步信号。

  1. Display上显示第0帧数据,此时CPU和GPU正在处理准备第1帧的画面,且在Display显示下一帧前完成;

  2. 因为CPU和GPU的处理及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧;

  3. 接着第2帧开始处理,但是CPU并没有立刻开始准备第2帧的数据,而是直到第2个VSync快来前才开始处理的;

  4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,屏幕显示的还是第1帧画面,即产生了丢帧卡顿问题;

  5. 当第2帧数据准备完成后,它并不能立马被显示,而是要等到下一个VSync 带来后,进行前后缓存交换才能显示到屏幕上。

  出现此掉帧卡顿问题的根本原因是:上层的CPU和GPU并不知道Vsync信号的到来,所以在底层屏幕的Vsync信号发出后并没有及时收到并开始下一帧画面的操作处理。根据前面的分析我们知道:双缓存的交换是在Vsyn信号到来时进行,交换后屏幕会读取Front Buffer内的新数据更新显示到屏幕上,而此时的Back Buffer 就可以供GPU准备下一帧数据了。如果 Vsyn到来时 CPU/GPU就开始操作的话,是有完整的Vsync周期时长来处理一帧数据,以避免卡顿的出现。那如何让 CPU/GPU的处理在 Vsyn信号到来时就开始进行呢?

  为了优化系统显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,引入了Project Butter(黄油计划),其中很重要的一点修改就是实现了:在系统收到VSync信号后,上层CPU和GPU马上开始进行下一帧画面数据的处理,完成后及时将数据写入到Buffer中,Google称之为Drawing with Vsync。如下图所示:

  上一节中讲到的,为了优化显示系统性能,Google在Android 4.1系统中对Android Display系统进行了重构,引入了Project Butter(黄油计划),其中很重要的一点修改就是实现了:在系统收到VSync信号后,上层CPU和GPU马上开始进行下一帧画面数据的处理,完成后及时将数据写入到Buffer中。为了实现这个效果,控制上层CPU和GPU在收到Vsync信号后马上开始一帧数据的处理,谷歌为此专门设计了一个名为Choreographer(中文翻译为“编舞者”)的类,来控制上层绘制的节奏。

  Choreographer 的引入,主要是为了配合系统Vsync垂直同步机制,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机。Choreographer 扮演 Android 渲染链路中承上启下的角色:

  // 通过Choreographer往主线程消息队列添加CALLBACK_TRAVERSAL绘制类型的待执行消息,用于触发后续UI线正实现绘制动作

  从以上流程图可以看出上层一般App应用UI中View的绘制流程(包含SurfaceView的游戏应用的绘制流程会有一些差异,篇幅有限此处不再展开分析):

  6. 最后,异步消息的执行者是跑在主线程中的ViewRootImpl#doTraversal,也就是真正开始绘制一帧的操作(包含measure、layout、draw三个过程);

  在前几节中分析了应用UI线程的消息循环机制和Android屏幕刷新机制之后,我们接着1小节中关于Input触控事件的处理流程继续往下分析。在1小节的分析中我们了解到:用户手指在应用界面上下滑动时,应用的UI线程中会收到system_server系统进程发送来的一系列Input事件(包含一个ACTION_DOWN、多个ACTION_MOVE和一个ACTION_UP事件),应用界面布局中的相关View控件在收到多个ACTION_MOVE触控事件后,判断为用户手指的滑动行为后,一般会调用View#invalidate等接口触发UI线程的绘制上帧更新画面的操作。

  在开始分析之前,我们先来看看Android系统的GUI显示系统在APP应用进程侧的核心架构,其整体架构如下图所示:

  • Window是一个抽象类,通过控制DecorView提供了一些标准的UI方案,比如背景、标题、虚拟按键等,而PhoneWindow是Window的唯一实现类,在Activity创建后的attach流程中创建,应用启动显示的内容装载到其内部的mDecor(DecorView);

  • DecorView是整个界面布局View控件树的根节点,通过它可以遍历访问到整个View控件树上的任意节点;

  // 通过Choreographer往主线程消息队列添加CALLBACK_TRAVERSAL绘制类型的待执行消息,用于触发后续UI线正实现绘制动作

  从以上分析可以看出,应用UI线程的绘制最终是通过往Choreographer中放入一个CALLBACK_TRAVERSAL类型的绘制任务而触发,下面的流程就和3.3.3小节中的分析的一致,Choreographer会先向系统申请Vsync信号,待Vsync信号到来后,向应用主线程MessageQueue发送一个异步消息,触发在主线程中执行ViewRootImpl#doTraversal绘制任务动作。我们接着看看ViewRootImpl的doTraversal函数执行绘制流程的简化代码流程:

  // 4.从DecorView根节点出发,遍历整个View控件树,完成整个View控件树的draw测量操作

  // 从mView指向的View控件树的根节点DecorView出发,遍历访问整个View树,并完成整个布局View树的测量工作

  // 如果开启并支持硬件绘制加速,则走硬件绘制的流程(从Android 4.+开始,默认情况下都是支持跟开启了硬件加速的)

  1. 从界面View控件树的根节点DecorView出发,递归遍历整个View控件树,完成对整个View控件树的measure测量操作,由于篇幅所限,本文就不展开分析这块的详细流程;

  2. 界面第一次执行绘制任务时,会通过Binder IPC访问系统窗口管理服务WMS的relayout接口,实现窗口尺寸的计算并向系统申请用于本地绘制渲染的Surface“画布”的操作(具体由SurfaceFlinger负责创建应用界面对应的Layer对象,并通过内存共享的方式通过Binder将地址引用透过WMS回传给应用进程这边);

  3. 从界面View控件树的根节点DecorView出发,递归遍历整个View控件树,完成对整个View控件树的layout布局操作;

  4. 从界面View控件树的根节点DecorView出发,递归遍历整个View控件树,完成对整个View控件树的draw绘制操作,如果开启并支持硬件绘制加速(从Android 4.X开始谷歌已经默认开启硬件加速),则走GPU硬件绘制的流程,否则走CPU软件绘制的流程;

  截止到目前,在ViewRootImpl中完成了对界面的measure、layout和draw等绘制流程后,用户依然还是看不到屏幕上显示的应用界面内容,因为整个Android系统的显示流程除了前面讲到的UI线程的绘制外,界面还需要经过RenderThread线程的渲染处理,渲染完成后,还需要通过Binder调用“上帧”交给surfaceflinger进程中进行合成后送显才能最终显示到屏幕上。本小节中,我们将接上一节中ViewRootImpl中最后draw的流程继续往下分析开启硬件加速情况下,RenderThread渲染线程的工作流程。由于目前Android 4.X之后系统默认界面是开启硬件加速的,所以本文我们重点分析硬件加速条件下的界面渲染流程,我们先分析一下简化的代码流程:

  // 1.从DecorView根节点出发,递归遍历View控件树,记录每个View节点的绘制操作命令,完成绘制操作命令树的构建

  // 2.JNI调用同步Java层构建的绘制命令树到Native层的RenderThread渲染线程,并唤醒渲染线程利用OpenGL执行渲染任务;

  1. 从DecorView根节点出发,递归遍历View控件树,记录每个View节点的drawOp绘制操作命令,完成绘制操作命令树的构建;

  2. JNI调用同步Java层构建的绘制命令树到Native层的RenderThread渲染线程,并唤醒渲染线程利用OpenGL执行渲染任务;

  从以上代码可以看出,构建绘制命令树的过程是从View控件树的根节点DecorView触发,递归调用每个子View节点的updateDisplayListIfDirty函数,最终完成绘制树的创建,简述流程如下:

  经过上一小节中的分析,应用在UI线程中从根节点DecorView出发,递归遍历每个子View节点,搜集其drawXXX绘制动作并转换成DisplayListOp命令,将其记录到DisplayListData并填充到RenderNode中,最终完成整个View绘制命令树的构建。从此UI线程的绘制任务就完成了。下一步UI线程将唤醒RenderThread渲染线程,触发其利用OpenGL执行界面的渲染任务,本小节中我们将重点分析这个流程。我们还是先看看这块代码的简化流程:

  2. 调用OpenGL库API使用GPU硬件,按照构建好的绘制命令完成界面的渲染(具体过程,由于本文篇幅所限,暂不展开分析);

  3. 将前面已经绘制渲染好的图形缓冲区Binder上帧给SurfaceFlinger合成和显示;

  SurfaceFlinger合成显示部分属于Android系统GUI中图形显示的内容,简单的说SurfaceFlinger作为系统中独立运行的一个Native进程,借用Android官网的描述,其职责就是负责接受来自多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。如下图所示:

  从上图可以看出,其实SurfaceFlinger在Android系统的整个图形显示系统中是起到一个承上启下的作用:

  •对上:通过Surface与不同的应用进程建立联系,接收它们写入Surface中的绘制缓冲数据,对它们进行统一合成。

  图形的传递是通过Buffer作为载体,Surface是对Buffer的进一步封装,也就是说Surface内部具有多个Buffer供上层使用,如何管理这些Buffer呢?答案就是BufferQueue,下面我们来看看BufferQueue的工作原理:

  BufferQueue是一个典型的生产者-消费者模型中的数据结构。在Android应用的渲染流程中,应用扮演的就是“生产者”的角色,而SurfaceFlinger扮演的则是“消费者”的角色,其配合工作的流程如下:

  2. 应用进程中拿到这张可用的Buffer之后,选择使用CPU软件绘制渲染或GPU硬件加速绘制渲染,渲染完成后再通过Binder调用queueBuffer接口将缓存数据返回给应用进程对应的BufferQueue(如果是 GPU 渲染的话,这里还有个 GPU处理的过程,所以这个 Buffer 不会马上可用,需要等 GPU 渲染完成的Fence信号),并申请sf类型的Vsync以便唤醒“消费者”SurfaceFlinger进行消费;

  在之前3.3小节关于Android系统屏幕刷新机制中我们分析了Vsync机制的来龙去脉。其实Android系统中的Vsync信号的产生与管理都是由SurfaceFlinger模块统一负责的,Vysnc信号一般分为两种类型:

  1. app类型的Vsync:app类型的Vysnc信号由上层应用中的Choreographer根据绘制需求进行注册和接收,用于控制应用UI绘制上帧的生产节奏。根据3.4小结中的分析:应用在UI线程中调用invalidate刷新界面绘制时,需要先透过Choreographer向系统申请注册app类型的Vsync信号,待Vsync信号到来后,才能往主线程的消息队列放入待绘制任务进行真正UI的绘制动作;

  我们接着3.5.2小节中的分析,应用进程的RenderThread渲染线程在执行完一帧画面的渲染操作的最后,会通过Binder调用queueBuffer接口将一帧数据提交给SurfaceFlinger进程进行消费合成显示。我们结合相关简化的源码流程(这里基于Android 11源代码分析)来看看SurfaceFlinger中是如何处理应用的请求的。

  由上面分析可知,只要有layer上帧,那么就会申请下一次的Vsync-sf信号, 当Vsync-sf信号来时会调用onMessageReceived函数来处理帧数据:

  这里可以看出来,handlePageFlip里一个重要的工作是检查所有的Layer是否有新Buffer提交,如果有则调用其latchBuffer来处理:

  3. 第二次走进来时,主要执行present方法,在这些方法里会和HWC service沟通,调用它的跨进程接口通知它去做图层的合成后送显示器显示。

  后续HWC service的合成以及屏幕的详细显示原理由于篇幅有限就不展开说明,感兴趣的读者可以参考系列文章。

  在本节中我们以用户手指上下滑动应用界面的操作场景为例,结合系统源码和Systrace工具,按照执行顺序分析了Android应用绘制上帧显示的系统运行机制与总体流程,我们以一张图描述如下:

  1. 用户手指触摸屏幕后,屏幕驱动产生Input触控事件;框架system_server进程中的EventHub通过epoll机制到驱动产生的Input触控事件上报,由InputReader读取到Input事件后,唤醒InputDispatcher找到当前触控焦点应用窗口,并通过事先建立的socket通道发送Input事件到对应的应用进程;

  2. 应用进程收到Input触控事件后UI线程被唤醒进行事件的分发,相关View控件中根据多个ACTION_MOVE类型的Input事件判断为用户手指滑动行为后,通过Choreographer向系统注册申请app类型的Vsync信号,并等待Vsync信号到来后触发绘制操作;

  3. app类型的Vsync信号到来后,唤醒应用UI线程并向其消息队列中放入一个待执行的绘制任务,在UI线程中先后遍历执行View控件树的测量、布局和绘制(硬件加速默认开启的状态下会遍历并记录每个View的draw操作生成对应的绘制命令树)操作;

  4. View控件树的绘制任务执行完成后会唤醒应用的RenderThread渲染线程执行界面渲染任务;整个渲染任务中会先同步UI线程中构建好的绘制命令树,然后通过dequeueBuffer申请一张处于free状态的可用Buffer,然后调用SkiaOpenGLPipeline渲染管道中使用GPU进行渲染操作,渲染完成后swapBuffer触发queueBuffer动作进行上帧;

  6. 待sf类型的Vsync信号到来后会唤醒SurfaceFlinger的主线程执行一帧的合成任务,其中会先通过handlePageFlip操作遍历所有的应用Layer找到有上帧操作的处于Queued状态的Buffer进行AcquireBuffer获取标记锁定,然后执行persent动作调用唤醒HWC service进程的工作线程执行具体的图层的合成送显操作;

  7. HWC service中最终会收到SurfaceFlinger的请求后,进行图层合成操作,最终通过调用libDrm库相关接口Commit提交Buffer数据到Kernel内核中的屏幕驱动,并最终送到屏幕硬件上显示。

  根据本节中我们对Android应用上帧显示的原理分析,我们初步可以判断:如果在一个Vsync周期内(60HZ的屏幕上就是16.6ms),按照整个上帧显示的执行的顺序来看,应用UI线程的绘制、RenderThread线程的渲染、SurfaceFlinger/HWC的图层合成以及最终屏幕上的显示这些动作没有全部都执行完成的话,屏幕上就会显示上一帧画面的内容,也就是掉帧,而人的肉眼就可能会感觉到画面卡顿(由于 Triple Buffer 的存在,这里也有可能不掉帧)。

  1. 从现象上来说,在 App 连续的动画播放或者手指滑动列表时(关键是连续),如果连续 2 帧或者 2 帧以上,应用的画面都没有变化,那么我们认为这里发生了卡顿;

  2. 从SurfaceFlinger的角度来说,在 App 连续的动画播放或者手指滑动列表时(关键是连续),如果有一个 Vsync 到来的时候 ,App 没有可以用来合成的 Buffer,那么这个 Vsync 周期SurfaceFlinger就不会走合成的逻辑(或者是去合成其他的 Layer),那么这一帧就会显示 App 的上一帧的画面,我们认为这里发生了卡顿;

  最后推荐一下我做的网站,玩Android:,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!完美手机版官网-完美综合体育app下载