Android UI刷新机制与SurfaceView

问题:

举例一个Activity的布局文件和逻辑如下:




 

 

 

     


     

 
 



   container.findViewById(R.id.remove_btn).setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             surfaceViewContainer.removeView(surface); //这个会回调到surfaceDestroyed, surfaceView立即就会消失,会出现黑块
             try {
                 Thread.sleep(10000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     });

当我们点击remove_btn时,会出现SurfaceView所在的区域会出现10s黑块的现象,这个现象在我们平时开发中用到SurafceView时常常遇到,往往在主线程同时存在耗时操作和SurfaceView detach操作的时候出现,那么为什么Surfaceview从parent view上面detach的时候容易出现黑块现象呢?开发中遇到SUrfaceView黑块问题又该如何解决呢?下面对这两个问题进行讲解。

回答问题之前,我们先了解下Android 普通View的刷新流程和SurfaceView的刷新有什么区别。

VSync信号的产生:

关于页面渲染,我们经常关注的性能指标就是帧率,一般认为达到60 帧/秒 就可以骗过人眼,给人比较顺滑的视觉体验,在Android中有一个很重要的概念就是VSync信号,一般认为是16ms发送一次,Vsync机制的引入,主要有以下两个作用:

  • 提升UI刷新的优先级,使得UI刷新操作能够及时执行;

  • 在CPU、GPU和Display之间保持同步,减少Jank帧和屏幕渲染延迟。

image.jpeg

VSync信号由硬件产生,决定于显示器的扫描频率,硬件产生原始的VSync信号后,会被转化为两个VSync信号,一个用于通知APP层去刷新UI,一个用于通知SurfaceFlingger取graphic buffer组合处理后给显示屏显示。VSync信号分发流程如下:

image.jpeg

SurfaceFlinger:

SurfaceFlinger是系统进程,用于整合不同APP不同Window的图像,合成之后给硬件显示。

每一个Layer对应java层的Surface,即一个窗口,一个Activity对应一个Surface,一个WindowManager创建出来的小窗对应一个独立的Surface,SurfaceView比较特殊,尽管可以嵌入在Activity的布局中,但实际上它独占一个Surface;这个特性与本文最开始提出的问题息息相关,后文会继续分析。

image.jpeg
image.jpeg

基本流程如下:

image.jpeg

步骤1,2:CPU和GPU处理完之后将buffer放到BufferQueue,并调用onFrameAvailable通知SurfaceFlinger有可用buffer了。

步骤3:SurfaceFlinger再通过内部MessageQueue调用requestNextVsync请求接收下一个VSYNC用于合成。

步骤4,5:下一个VSYNC到了之后回调MessageQueue的handleMessage函数,实际调到SurfaceFlinger的onMessageReceived函数处理如下两种类型消息:

image.jpeg

步骤6,7:在处理REFRESH消息时最终会调用acquireBuffer函数从BufferQueue中将之前APP绘制完成的buffer取出来合成。

从上文可以看出,SurfaceFlinger的组合图层给硬件显示之前,需要先去取graphic buffer,那么graphic buffer又是谁去更新的呢?对于普通View和SurfaceView来说,这个机制会有所差别。

普通View刷新机制:

举例Activity中的一个TextView的更新如下:

如果应用层通过调用TextView的setText方法修改显示的文案,总体的执行流程如下:

image.jpeg

步骤描述:

  1. TextView调用setText方法,会执行到TextView的invalidate方法,这就会递归调用parent的invalidate,一直到ViewRootImpl类的invalidate方法,这个方法会调用到scheduleTraversals

  2. ViewRootImpl通过scheduleTraversals方法会调用到Choreographer的postCallback方法,postCallback会记录ViewRootImpl中的mTraversalRunnable,并向底层注册监听下一个vSync信号

  3. 底层的vSync信号过来之后,才会通过给主线程发送Runnable任务,执行Choreographer的doFrame方法,这里面真正调用执行ViewRootImpl中的doTraversal(包括performMeasure、performLayout、performDraw)流程

  4. draw的具体实现通过ThreadedRenderer类,调用到c++层的RenderThread,实现在render thread执行GPU计算,更新SurfaceFlinger中buffer 队列

  5. 下一次SurfaceFlinger收到Vsync信号的时候,就可以真正将这次setText的内容交给硬件,显示给用户了

因此,Android系统中普通View的渲染,并不是代码执行完立即显示到屏幕上的,而是需要在设置变化之后,等待消费下一次给APP的vSync信号,才能把新的图像更新给SurfaceFlinger,而后才能真正显示出来。

UI刷新通用流程总结如下:

image.jpeg

步骤1:View调用invalidate方法进行重绘时最终会递归调用到ViewRootImpl中。

步骤2: ViewRootImpl并不会立即会View进行绘制,而是调用scheduleTraversals将绘制请求给到Choreographer,并开始同步屏障,保证UI处理的高优先级。

步骤3,4: 通过postCallback将绘制请求给到Choreographer之后,Choreographer最终会将监听下一个VSYNC的请求发送到SurfaceFlinger进程的DispSync这个类,这是VSYNC分发的核心。

步骤5,6:当下一个VSYNC到来之后会回调Choreographer的onVsync方法,onVsync中调用doFrame,doCallbacks处理View的绘制请求。

步骤7:View绘制请求的入口即ViewRootImpl的performTraversals,这个方法会依次执行View的onMeasure,onLayout,onDraw开始View的绘制流程。

步骤8:硬件加速引入之后UI的具体绘制会在一个单独的渲染线程RenderThread,CPU为View构建DisplayList(包含绘制指令和数据)之后将数据共享给GPU,剩下的绘制操作由GPU在RenderThread线程完成。

步骤9,10,11:向BufferQueue中dequeue一块可用GraphicBuffer之后由GPU对这个块buffer进行操作,完成之后交换buffer(dequeue的是back buffer,front buffer用于显示,back buffer绘制完成之后和front buffer交换)。

步骤12:此时CPU和GPU对buffer的绘制已经完成(概念上已经完成,实际上GPU可能还在操作,依赖Fence进行同步),接着通过queueBuffer函数将buffer转移到BufferQueue,然后通知SurfaceFlinger有可用buffer了。

CPU、GPU、SurfaceFlinger如何协作:

image.jpeg

SurfaceView的刷新与销毁:

挖洞与绘制:

前面提到过,SurfaceView与普通的View有很大的区别,它可以嵌入到Activity的布局中,但是它是一个独立的Surface(Layer),内容的刷新流程也跟普通的View完全不一样。SurfaceView在Activity中的布局,只决定它的显示位置。如果没有设置setZOrderOnTop为true,SurfaceView的窗口在Activity窗口的下面,SurfaceView这个Layer的显示,依赖于ViewRootImpl中挖洞的逻辑(gatherTransparentRegion),在ViewRootImpl类中performLayout逻辑执行完之后,会收集SurfaceView需要透出的区域,并把这个信息传递给底层,将这个区域设置为透明,这样Actvity这一层的Layer就不会遮挡下面SurfaceView的Layer。

image.jpeg

挖洞流程如下:

image.jpeg

SurfaceView支持在后台线程直接绘制内容,基本绘制流程如下,调用了unlockCanvasAndPost之后,便会将在Canvas上绘制的内容通过独立的RenderProxy处理后提交给SurfaceFlinger合成,后面就可显示出来了。也可以通过holder.getSurface()获取到Surface之后,直接通过OpenGl渲染。

image.jpeg

销毁:

这里讲SurfaceView的销毁主要指的是将SurfaceView对应的Layer从SurfaceFlinger中移除。一般可以通过直接设置这个SurfaceView本身不可见(注意设置这个SurfaceView的父View不可见不会触发Layer的移除)或者将这个SurfaceView从ViewTree上remove掉实现。如VC中使用的是从父View remove这个SurfaceView的方法实现SurfaceView资源的释放和视图的刷新。

当调用parent.removeView将SurfaceView移除时,流程如下:

image.jpeg

可以看出,当SurfaceView被从父View上remove掉时,是直接调用代码,将自己对应的Layer从的SurfaceFlinger中移除掉了。并不像普通的View更新一样,需要等待下一个vSync信号,在主线程插入Runnable任务触发doTraversal的流程,然后再将这个变化反应给SurfaceFlinger。

回到文初的问题:

结合前面的调用流程,可以知道,在refreshAllUnit的过程中,由于这个方法总体耗时较长,并且在主线程执行,这期间Choreographer没办法插入任务去执行doTraversal的流程,因此Activity对应的代码执行了,但实际上并没有更新显示。而SurfaceView被remove掉之后,会直接更新显示,这中间就有一个时间差,导致SurfaceView原来显示的区域出现了黑块(挖出来的洞)。

image.jpeg

那么如何解决SurfaceView黑块的问题呢?我们可以在调用SurfaceView的detach方法之前,插入16ms的延时,先让SurfaceView的parent视图区域变得不可见,切换为新的视图成功之后,再调用SurfaceView的detach方法。

参考:

https://www.mtyun.com/library/hardware-accelerate

https://developer.android.com/guide/topics/graphics/hardware-accel?hl=zh-cn

https://sharrychoo.github.io/blog/android-source/surfaceflinger-vsync-dispatch

https://source.android.google.cn/devices/graphics/implement-vsync

https://www.youtube.com/watch?v=zdQRIYOST64

https://blog.csdn.net/qq_31339141/article/details/108503315

https://mp.weixin.qq.com/s/IIh2g1i6Y4rZeCTY-t6_8w

https://www.codenong.com/cs107053967/

https://source.android.google.cn/devices/graphics/implement-vsync

https://blog.csdn.net/qq_34211365/article/details/107996767

https://juejin.cn/post/7004420015038414885

https://blog.csdn.net/u010164190/article/details/80185469

https://huanle19891345.github.io/en/android/system/%E7%B3%BB%E7%BB%9F%E7%BB%98%E5%88%B6/%E7%A1%AC%E4%BB%B6%E5%8A%A0%E9%80%9F%E7%BB%98%E5%88%B6/

你可能感兴趣的:(Android UI刷新机制与SurfaceView)