Flutter 视图绘制(2)

Flutter视图绘制(2)

[TOC]

前言

​ Flutter视图绘制(1)写的是自带Skia的flutter如何“安排”绘制的步骤,接下来的(2),(3),(4),(5),我想首先通过回顾和总结Android的视图绘制,再来反观flutter的实现,最后对比他们的异同。所以虽然这篇文章的题目是“Flutter视图绘制(2)”但是是”挂羊头卖狗肉”的,本文讲的是Android。

五个问题

谁来发令?

Android的手机屏幕有一个固定的刷新频率用于显示最新的画面,如60hz。而GPU生成一帧的图像也会有它自己的速率,那么如何做到它们的步调统一,不出乱子呢?

vsync_normal

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

two_buffer

  • 双缓存:用于Display和用于Produce 各有一个Frame Buffer,各自工作,互不干扰。
  • VSYNC:Display和Produce都受VSYNC信号控制。当发出信号后,交换两者的Frame Buffer,Display开始显示新帧,Produce开始生产下一帧。

接下来我们用回退的方法把一个个条件去掉,来阐明为什么要这么设计。

  1. 假如去掉双缓存的条件。

    two_buffer

    观察第一个VSYNC后的Display显示,就有可能出现A+B(如下图)的情况

    two_buffer

  2. 假如去掉VSYNC的条件。

    two_buffer

发现虽然B的生产只需要2ms,但还是出现了连续两个A(也就是卡顿)的现象,究其原因就是GPU闲置或者去忙别的事情去了,没有及时的生产B Buffer。

  1. 假如Produce超过了16ms的生产周期

two_buffer

那么这就要开发者优化自己的代码了。

怎么转换?

我们编写的xml布局文件是如何转换成屏幕上一个个的像素点的呢?

two_buffer

CPU: 屏幕上的view通过measure测量出宽高,通过layout计算出左上角位置,通过draw生成DisplayList(指令优化),通过将图像转化成多边形和纹理,上传至GPU。

GPU:将图像栅格化也就是一个个的像素点。

谁站C位?

假如要画一张Android视图绘制全景图,那在这幅图的中心位置应该是什么?

SurfaceFlinger

  1. 它是合成者

    通过一个dumpsys SurfaceFlinger命令。

    dump

    可以清楚的看到有几个layer(对应的是应用层的一个window),它们所在的屏幕坐标,以及层级关系等等。

  2. 它是消费者

    取走了由应用层Surface生产的Grapic Buffer。

  3. 它是转化者

    由于屏幕的显示必须是硬件的Frame Buffer数据,所以surfaceFlinger在取出应用层的Grapic Buffer 后,首先将它合成到自己的Grapic Buffer 中,然后(FramebufferSurface)渲染到硬件的Fram Buffer中。

如何协作?

在整个视图绘制系统中至少有两处用到了生产-消费者模型,所以理解它可以更好的帮助我们理解系统的运作。

model

但凡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这个画笔,那么这个画笔是画在哪个画布上呢?

  1. 我们知道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就会被画到不同的画布上了。

    1. 我们以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搜索发现两篇非常详尽的文章,就不费那力了,双手奉上。

  1. 我们的Button是如何一步一步变成FrameBuffer数据的?

    https://www.inovex.de/blog/android-graphics-pipeline-from-button-to-framebuffer-part-1/

  2. RecylceView的一个Item被点击之后,视图绘制做了哪些事儿?

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