Android UI刷新机制与SurfaceView

问题:

举例一个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.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/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容