关于Android 架构 的MVI 初级体Android UI刷新机制与SurfaceView【转载】

104 阅读7分钟

问题:

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

**

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical"
 tools:ignore="MissingDefaultResource"
 android:background="@android:color/holo_red_dark">

 <FrameLayout
     android:id="@+id/surfaceView_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center"
     android:padding="10dp"
     android:background="@android:color/holo_blue_bright">

 <SurfaceView
     android:id="@+id/surfaceView"
     android:layout_gravity="center"
     android:layout_width="200dp"
     android:layout_height="200dp"></SurfaceView>

 <LinearLayout
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="bottom"
     android:orientation="horizontal">

     <Button
         android:id="@+id/gone_btn"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="set container gone"></Button>


     <Button
         android:id="@+id/remove_btn"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="remove surfaceView"></Button>

 </LinearLayout>
 </FrameLayout>


</FrameLayout>

**

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

image.png

SurfaceFlinger:

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

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

image.png

image.png

基本流程如下:

image.png

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

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

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

image.png

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

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

普通View刷新机制:

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

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

image.png

步骤描述:

  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.png

步骤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.png

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.png

挖洞流程如下:

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

image.png

销毁:

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

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

image.png

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

回到文初的问题:

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

image.png

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



作者:平凡小天地
链接:www.jianshu.com/p/22bf19d3f…