面试问你屏幕刷新机制、垂直同步信号

547 阅读7分钟

一:人眼刷新频率

  • 12fps:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒10-12帧,认为是连贯。
  • 24fps:有声电影拍摄及播放帧率为24hz。
  • 30fps:电子游戏,帧率少于30fps,肉眼会觉得不连贯。
  • 60fps:在与手机交互中,如触摸和反馈60帧以下是可以感觉出来(卡顿),但如果大于60fps不能察觉变化(之快)。

HZ:是频率的单位,频率是指电脉冲、交流电波形、电磁波等1秒钟重复的次数。

fps:是图像领域中的定义,是指画面每秒传输帧数。

因此Android系统每隔16ms发出VSYNC信号,触发对UI的渲染,如果每次渲染都成功,这样就能够达到流畅画面所需的60fps,为了能够实现60fps,因此每次绘制都要在16ms内完成。

二:刷新屏幕机制

  1. 应用程序向系统服务申请一块buffer(缓存),系统服务返回buffer,应用拿到buffer后就可以进行绘制。
  2. 应用在buffer上绘制好了之后,将buffer提交给系统服务。
  3. 系统服务将buffer写到屏幕的一块缓存区,屏幕会以一定的帧率刷新,每次刷新时,就会从缓存区将图像数据读取显示出来。
  4. 如果缓存区没有新的数据,就一直用旧的数据,这样屏幕看起来就没有变。 AEEFA7B9-D6C8-4DBD-9904-E6FBD1ADA3D3.png 在这个过程中:
  • CPU:负责计算帧数据,measure、layout,负责把UI组件计算成Texture纹理,然后交给GPU进行栅格化渲染。
  • GPU:对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)中存起来,栅格化数据。栅格化是绘制那些button、shape、bitmap等组件最基础的操作,他把那些组件拆分到不同的像素上显示,这是一个很费事的操作,GPU的引入就是为了加快栅格化(Rasterization)的操作。
  • Display:屏幕或者显示器,负责把buffer中的数据呈现到屏幕上 9B4951A7-2D33-4FDB-B7C1-520FAD6652B6.png

buffer个数问题:

  • 单buffer:当buffer数据正在被屏幕读取显示时,下一帧数据写入buffer,将会导致屏幕显示多帧内容。
  • 2个buffer:显示屏上的内容,是从硬件帧缓冲区读取的。从buffer的起始地址开始,从上往下从左至右扫描整个buffer,将内容映射到显示屏上。一个FrontBuffer用于提供屏幕显示内容,backBuffer用于后台合成下一帧图形。当frontBuffer内容显示完毕,交换Front和Back角色,front作为下一帧写入缓冲,back则给屏幕提供数据。

51B3F255-29EB-4954-BC62-F793C1911F1E.png

三:VSync垂直同步机制

页面刷新分为:Display刷新(从buffer中读取数据) + CPU/GPU合成帧数据(帧数据写入buffer)。

如果display刷新频率和GPU/CPU合成帧率不同步,则会出现以下问题:

B6B08A7C-E055-4788-AFEE-3FBBB9925E16.png 在该图中,由于第2帧CPU开始处理帧数据晚了,导致GPU处理第2帧也晚了,导致第1帧数据展示了2次(出现掉帧)。如果第二帧CPU处理时间能提前到红色框的时间点,也许不会出现掉帧现象。这就是Display刷新频率和CPU/GPU合成频率不一致导致的。

引入Vsync信号同步后:

11.png 可以看到所有的帧都在vsync信号到来后,CPU就开始处理帧数据,也就解决了刷新不同步的问题。

四:choreography

之前,屏幕的vsync信号只是用来控制帧缓冲区的切换,并未控制上层的绘制节奏,也就是我们刚刚说的display刷新频率和CPU/GPU合成频率是脱离的。

22.png

因此google加入了上层接受垂直同步信号的逻辑:

33.png

主要就是通过Choreographer来实现的,流程如下:

4.png

5.png

五:DisplayList

Android需要把XML布局文件转换成GPU能够识别并绘制的对象,这个操作是在DisplayList的帮助下完成的。Display List是一个缓存绘制命令的buffer,他本质是一个缓冲区,里面记录了将要执行的绘制命令序列。

Display List是视图的基本绘制元素,包含元素原始属性(位置、尺寸、透明度等),对应Canvas的drawXXX方法。

视图信息传递流程:Canvas(Java API)-> OpenGL(C/C++ Lib) -> 驱动程序 ->GPU

在硬件加速渲染环境中,Android应用程序窗口的UI渲染是分两步进行的:

  • 第一步是构建DisplayList,CPU在measure、layout、draw之后,生成了DisplayList,是在主线程中运行的。
  • 第二步是渲染Display List,是在应用程序进程的RenderThread中进行的。增加Render Thread线程,也是为了避免UI线程任务过重,用于提高渲染性能。

DisplayList命令最终会转化为Open GL命令,由GPU执行,这意味着我们在调用CanvasAPI绘制UI时,实际上只是将Canvas API调用及其参数记录在DisplayList中。

6.png

五:surface、surfaceFlinger

每一个Activity组件都关联一个或若干个窗口,每一个窗口都对应一个surface。有了这个surface之后,应用程序就可以在上面渲染窗口的UI。

最终这些已经绘制好的surface都会被统一提交给surface管理服务SurfaceFlinger进行合成,最后显示在屏幕上面。

  • surface:android应用的每个窗口对应一个画布(canvas),即surface。surface是一个接口,供生产方与使用方交换缓冲区。
  • surface flinger:android系统服务,在开机时初始化该服务,该服务也会注册到ServiceManager中,负责android系统的帧缓冲区,即显示屏幕。

surfaceFlinger用来管理消费当前可见的Surface,所有被渲染的可见Surface都会被Surface Flinger通过WindowManager提供的信息合成(使用Open GL和Hardware Composer)提交到屏幕的后缓冲区,等待屏幕的下一个VSync到来,再显示到屏幕上。

7.png

六:具体代码流程

8.png

9.png

10.png

设置同步屏障可以保证在vsync回来时,可以立马开始在主线程进行计算绘制数据,避免超时。

a.png

b.png

c.png

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {

    public void scheduleVsync() {
        nativeScheduleVsync(mReceiverPtr);  //调用native请求vsync回调
    }
    // Called from native code.
    private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) {   //该方法被native回调
        onVsync(timestampNanos, physicalDisplayId, frame);
    }    

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        //...忽略部分code,发送一个消息,去执行this这个runnable,也就是会触发到该类的run方法
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

执行doFrame函数,会执行第二段代码里的mTraversalRunnable代码。

final class TraversalRunnable implements Runnable {

    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal这个方法里会去执行performTraversal。所以measure、layout、draw是在onVsync回调后才执行。

\

应用程序通过调用viewRootImpl.requestLayout发起重绘,通过choreography发送异步消息,请求同步vsync信号。即在下一次vsync信号过来时,系统服务surfaceFlinger在第一时间通知我们,触发UI绘制。虽然可以多次调用requestLayout,但是在一个vsync周期内,requestLayout只会执行一次。

七:常见问题

1.丢帧一般是什么原因引起的?

答:主线程有耗时操作,耽误了view的绘制

2.Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次?

答: 60帧/秒也是vsync信号的频率,但不一定每次vsync信号都会去绘制,先要应用端主动发起重绘,才会向SurfaceFlinger请求接收vsync信号,这样当vsync信号来的时候,才会真正去绘制。

3.onDraw执行完之后屏幕会马上刷新么?

答: 不会马上刷新,会等到下一次vsync信号时才会刷新。

4.如果界面没有重绘,还会每隔16ms刷新屏幕么?

答:界面没有重绘,应用就不会收的vsync信号,屏幕还是会刷新,画面数据用的是旧的,看起来没什么变化而已

5.如果屏幕快要刷新的时候才去onDraw绘制会丢帧么?

答: 重绘不会立即执行,而是等到下一次vsync信号来时才开始, 所以什么时候发起重绘影响不大