Flutter视图绘制(2)
[TOC]
前言
 Flutter视图绘制(1)写的是自带Skia的flutter如何“安排”绘制的步骤,接下来的(2),(3),(4),(5),我想首先通过回顾和总结Android的视图绘制,再来反观flutter的实现,最后对比他们的异同。所以虽然这篇文章的题目是“Flutter视图绘制(2)”但是是”挂羊头卖狗肉”的,本文讲的是Android。
五个问题
谁来发令?
Android的手机屏幕有一个固定的刷新频率用于显示最新的画面,如60hz。而GPU生成一帧的图像也会有它自己的速率,那么如何做到它们的步调统一,不出乱子呢?

这是最终的解决方案。假如Display里的A,B,C是一个连续的动画,那么它将十分流畅的呈现出来。背后的设计就是”双缓存”+“VSYNC”。

- 双缓存:用于Display和用于Produce 各有一个Frame Buffer,各自工作,互不干扰。
- VSYNC:Display和Produce都受VSYNC信号控制。当发出信号后,交换两者的Frame Buffer,Display开始显示新帧,Produce开始生产下一帧。
接下来我们用回退的方法把一个个条件去掉,来阐明为什么要这么设计。
- 假如去掉双缓存的条件。  - 观察第一个VSYNC后的Display显示,就有可能出现A+B(如下图)的情况  
- 假如去掉VSYNC的条件。  
发现虽然B的生产只需要2ms,但还是出现了连续两个A(也就是卡顿)的现象,究其原因就是GPU闲置或者去忙别的事情去了,没有及时的生产B Buffer。
- 假如Produce超过了16ms的生产周期
    
那么这就要开发者优化自己的代码了。
怎么转换?
我们编写的xml布局文件是如何转换成屏幕上一个个的像素点的呢?

CPU: 屏幕上的view通过measure测量出宽高,通过layout计算出左上角位置,通过draw生成DisplayList(指令优化),通过将图像转化成多边形和纹理,上传至GPU。
GPU:将图像栅格化也就是一个个的像素点。
谁站C位?
假如要画一张Android视图绘制全景图,那在这幅图的中心位置应该是什么?
SurfaceFlinger
- 它是合成者 - 通过一个dumpsys SurfaceFlinger命令。  - 可以清楚的看到有几个layer(对应的是应用层的一个window),它们所在的屏幕坐标,以及层级关系等等。 
- 它是消费者 - 取走了由应用层Surface生产的Grapic Buffer。 
- 它是转化者 - 由于屏幕的显示必须是硬件的Frame Buffer数据,所以surfaceFlinger在取出应用层的Grapic Buffer 后,首先将它合成到自己的Grapic Buffer 中,然后(FramebufferSurface)渲染到硬件的Fram Buffer中。 
如何协作?
在整个视图绘制系统中至少有两处用到了生产-消费者模型,所以理解它可以更好的帮助我们理解系统的运作。

但凡surface内部都有一个BufferQueue,应用层生产GrapicBuffer,SurfaceFlinger消费GrapicBuffer,这样就构成了一个基本的模型。其中每个Buffer有四种状态:
1.Free:队列0状态(可以被生产者使用)
2.Dequeued:生产中
3.Queued:队列1状态(GraphicBuffer生产完成,等待被消费)
4.Acquired:消费中
Buffer的一次流动过程大致是一个逆时针的环:
1.队列0状态到生产中
2.生产完成到队列1状态
3.队列1状态到消费中
4.消费完成回到0状态
画布哪来?
假如你写过自定义View,都会用到onDraw里面的Canvas这个画笔,那么这个画笔是画在哪个画布上呢?
- 我们知道ViewRootImpl是所有View的Parent,所以先从ViewRootImpl的draw方法找起 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- private void draw(boolean fullRedrawNeeded) { 
 Surface surface = mSurface;
 //......
 
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
 return;
 }
 }
 }
 //......
 }- 然后查看drawSoftware代码 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32- private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, 
 boolean scalingRequired, Rect dirty) {
 
 // Draw with software renderer.
 final Canvas canvas;
 try {
 //.....
 
 canvas = mSurface.lockCanvas(dirty);
 
 //.....
 try {
 canvas.translate(-xoff, -yoff);
 if (mTranslator != null) {
 mTranslator.translateCanvas(canvas);
 }
 canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
 attachInfo.mSetIgnoreDirtyState = false;
 
 mView.draw(canvas);
 
 drawAccessibilityFocusedDrawableIfNeeded(canvas);
 } finally {
 if (!attachInfo.mSetIgnoreDirtyState) {
 // Only clear the flag if it was not set during the mView.draw() call
 attachInfo.mIgnoreDirtyState = false;
 }
 }
 }
 //.....
 return true;
 }- 发现这里ViewRootImpl里的全局变量 mSurface就是我们要找的。解决了来源,然后来解决唯一性的问题,否则一个界面上的View就会被画到不同的画布上了。 - 我们以Activity举例,知道ActivityThread在handleResumeActivity的时候会去做UI显示,所以从这里入手开始 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- final void handleResumeActivity(IBinder token, 
 boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
 r = performResumeActivity(token, clearHide, reason);
 
 if (r != null) {
 //......
 if (a.mVisibleFromClient) {
 if (!a.mWindowAdded) {
 a.mWindowAdded = true;
 wm.addView(decor, l);
 }
 //........
 }- 我们找到了wm.addView操作,这里的wm其实是一个WindowManagerImpl的实例,所以接着往下看 - 1 
 2
 3
 4- public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 
 applyDefaultToken(params);
 mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
 }- 发现实现类其实是WindowManagerGlobal的addView - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24- public void addView(View view, ViewGroup.LayoutParams params, 
 Display display, Window parentWindow) {
 //....
 
 root = new ViewRootImpl(view.getContext(), display);
 
 view.setLayoutParams(wparams);
 
 mViews.add(view);
 mRoots.add(root);
 mParams.add(wparams);
 
 // do this last because it fires off messages to start doing things
 try {
 root.setView(view, wparams, panelParentView);
 } catch (RuntimeException e) {
 // BadTokenException or InvalidDisplayException, clean up.
 if (index >= 0) {
 removeViewLocked(index, true);
 }
 throw e;
 }
 }
 }- 通过ViewRootImpl的构造函数,我们确定了它存有WMS端的seesion代理对象,这样就可以去做真正的视图添加了。 - 通过root.setView方法,我们确定了ViewRootImpl和DecorView的一一对应关系,保证了Activity上的所有view都画在一块画布上。 
 
举例
本想在最后举一个TextView的例子,但利用Google搜索发现两篇非常详尽的文章,就不费那力了,双手奉上。
- 我们的Button是如何一步一步变成FrameBuffer数据的? - https://www.inovex.de/blog/android-graphics-pipeline-from-button-to-framebuffer-part-1/ 
- RecylceView的一个Item被点击之后,视图绘制做了哪些事儿?