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
11private 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
32private 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
13final 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
4public 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
24public 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被点击之后,视图绘制做了哪些事儿?