共计 14243 个字符,预计需要花费 36 分钟才能阅读完成。
原文链接 Android View 的渲染过程
对于安卓开发猿来说,每天都会跟布局打交道,那么从咱们写的一个布局文件,到运行后可视化的视图页面,这么长的工夫内到底 产生了啥呢?明天咱们就一起来探询这一旅程。
View tree 的创立过程
布局文件的生成过程
个别状况下,一个布局写好了,如果不是特地简单的布局,那么当把布局文件塞给 Activity#setContentView 或者一个 Dialog 或者一个 Fragment,之后这个 View tree 就创立好了。那么 setContentView,其实是通过 LayoutInflater 这个对象来具体的把一个布局文件转化为一个内存中的 View tree 的。这个对象不算太简单,次要的逻辑就是解析 XML 文件,把每个 TAG,用反射的形式来生成一个 View 对象,当 XML 文件解析实现后,一颗 View tree 就生成完了。
然而须要留神,inflate 之后尽管 View tree 是创立好了,然而这仅仅是以单纯对象数据的模式存在,这时去获取 View 的一些 GUI 的相干属性,如大小,地位和渲染状态,是不存在的,或者是不对的。
手动创立
除了用布局文件来生成布局,当然也能够间接用代码来撸,这个就比拟直观了,view tree 就是你创立的,而后再把根节点塞给某个窗口,如 Activity 或者 Dialog,那么 view tree 就创立完事了。
渲染前的筹备工作
View tree 生成的最初一步就是把根结点送到 ViewRootImpl#setView 外面,这里会把 view 增加到 wms 之中,并着手开始渲染,接下来就次要看 ViewRootImpl 这个类了,次要入口办法就是 ViewRootImpl#requestLayout,而后是 scheduleTraversals(),这里会把申请放入到队列之中,最终执行渲染的是 doTraversal,它外面调用的是 performTraversals(),所以,咱们须要重点查看 ViewRootImpl#performTraversals 这个办法,view tree 渲染的流程全在这外面。这个办法相当之长,靠近 1000 行,次要就是三个办法 performMeasure,performLayout 和 performDraw,就是常说的三大步:measure,layout 和 draw。
渲染之 measure
就看 performMeasure 办法,这个办法很简略,就是调用了根 view 的 measure 办法,而后传入 widthSpec 和 heightSpec。measure 的目标就是测量 view tree 的大小,就是说 view tree 在用户可视化角度所占屏幕大小。要想了解透彻 measure,须要了解三个事件,MeasureSpec,View#measure 办法和 View#onMeasure 办法:
了解 MeasureSpec
从文档中能够理解到,MeasureSpec 是从父布局传给子布局,用以代表父布局对子布局在宽度和高度上的束缚,它有两局部一个是 mode,一个是对应的 size,打包成一个 integer。
-
UNSPECIFIED
父布局对子布局没有要求,子布局能够设置任意大小,这个 基本上 不常见。
-
EXACTLY
父布局曾经计算好了一个准确的大小,子布局要严格依照 这个来。
-
AT_MOST
子布局最大能够达到传过来的这个尺寸。
光看这几个 mode,还是不太好了解。因为咱们素日里写布局,在大小(或者说宽和高)这块就三种写法:一个是 MATCH_PARENT,也就是要跟父布局一样大;要么是 WRAP_CONTENT,也就是说子布局想要刚好适合够显示本人就行了;再者就是写死的如 100dp 等。须要把 measure 时的 mode 与 LayoutParams 联合分割起来,能力更好的了解 measure 的过程。
还是得从 performMeasure 这时动手,这个 MeasureSpec 是由父节点传给子节点,追根溯源,最原始的必定是传给整个 view tree 根节点的,也就是调用 performMeasure 时传入的参数值。
根节点的 MeasureSpec
根节点的 MeasureSpec 是由 getRootMeasureSpec 得来的,这个办法传入的是窗口的大小,这是由窗口来给出的,以后的窗口必定 是晓得本人的大小的,以及根节点布局中写的大小。从这个办法就能看出后面说的布局中的三种写法对 MeasureSpec 的影响了:
- 如果 根节点布局是 MATCH_PARENT 的,那么 mode 就是 EXACTLY,大小就是父布局的尺寸,因为根节点的父亲就是窗口,所以就是窗口的大小
- 如果 根节点布局是 WRAP_CONTENT 的,那么 mode 是 AT_MOST,大小仍然会是父布局的尺寸。这个要这样了解,WRAP_CONTENT 是想让子布局本人决定本人多大,然而,你的极限 就是父布局的大小了。
- 其余,其实就是根节点写死了大小的(写布局时是必须 要指定 layout_width 和 layout_height 的,即便某些 view 能够省略一个,也是因为缺省值,而并非不必指定),那么 mode 会是 EXACTLY,大小用根节点指定的值。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
子 View 的 MeasureSpec
MeasureSpec 这个货色是自上而下的,从根节点向子 View 传递。后面看过了根节点的 spec 生成形式,还有必要再看一下子 View 在 measure 过程中是如何生成 spec 的,以更好的了解整体过程。次要看 ViewGroup#getChildMeasureSpec 办法就能够了:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let them have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
单纯从 spec 角度来了解,与下面的是一样的,基本上 WRAP_CONTENT 会是 AT_MOST,而其余都是 EXACTLY。
前面会再具体讨论一下,父布局与子 View 的相互影响。
View#measure 和 View#onMeasure
performMeasure 比较简单,只是调用根节点的 measure 办法,而后把计算出来的根节点的 MeasureSpec 传进去,就完事了,所以 重点要 View#measure 办法。这里须要留神的是整个 View 的设计体系外面一些次要的逻辑流程是不容许子类 override 的,可定制的局部作被动式的办法嵌入在次要逻辑流程中,如 measure 是不能被 override 的,它会调用能够被子类 override 的 onMeasure。onMeasure 是每个 View 必须实现的办法,用传入的父布局的束缚来计算出自已的大小。
为了优化 measure 流程,还有一个 cache 机制,用从父布局传入的 MeasureSpec 作为 key,从 onMeasure 得出的后果 作为 value,保留在 cache 中,当前面再次调用 measure 时,如果 MeasureSpec 未发生变化,那么就间接从 cache 中取出后果,如果 有变动 那么再调用 onMeasure 去计算一次。光看 View#measure 和 onMeasure 这两个办法也没啥啊,或者说常见的 view 或者咱们本人定义的 view 的 onMeasure 办法也没啥啊,都不算太简单,有同学就会问,这里为啥这么吃力 非要搞出一个 cache 呢?这个也好了解,要明确任何一个 view 不光是你本人,还波及到所有你的子 view 啊,如果你只是一个未端的 view(叶子),那当然 无所谓了,但如果是一个 ViewGroup,上面有很多个子 view,那么 如果能少调用一次 onMeasure,还是能节俭不少 CPU 资源的。
ViewGroup 的 onMeasure
每个 View 的自身的 onMeasure 并不简单,只须要关注好自身的尺寸就好了。
简单的在于 ViewGroup 的 onMeasure,简略来了解也并不简单,它除了须要测量本人的宽与高之外,还须要一一遍历子 view 以 measure 子 view。如果 ViewGroup 本身是 EACTLY 的,那么 onMeasure 过程就会简略不少,因为它本身的宽与高是确定的,只须要挨个 measure 子 View 就可了,而且子 View 并不影响它自身。当然,要把 padding 和 margin 思考进来。
最为简单的就是 AT_MOST,ViewGroup 本身的宽与高是由其所有子 View 决定的,这才是最简单的,也是各个 ViewGroup 子类布局器须要重点解决的,而且过程各不相同,因为每个布局器的特点不一样,所以过程并不相同,上面来各自讨论一下。
几种常见的 ViewGroup 的 measure 逻辑
下来来看一下一些十分常见的 ViewGroup 是如何 measure 的:
LinearLayout
它的方向只有两个,能够只剖析一个方向,另外一个方向是差不多的,咱们就看看 measureVertical。
第 1 种状况,也就是 height mode 是 EXACTLY 的时候,这个时候 LinearLayout 布局自身的高度是已知的,挨个遍历子 view 而后 measure 一下就能够。
第 2 种状况,比较复杂的状况,是 AT_MOST 时,这其实也还好,实践上高度就是所有子 view 的高度之和。
对于 LinearLayout,最为简单的状况是解决 weight,这须要很多简单解决,要把残余所有的空间按 weight 来调配,具体比较复杂,有趣味的能够具体去看源码。这也阐明了,为何在线性布局中应用 weight 会影响性能,代码中就能够看出当有 weight 要解决的时候,至多多遍历一遍子 view 以进行相干的计算。
尽管方向是 VERTICAL 时,重点只解决垂直方向,然而 width 也是须要计算的,但 width 的解决就要简略得多,如果其是 EXACTLY 的,那么就已知了;如果是 AT_MOST 的,就要找子 view 中 width 的最大值。
FrameLayout
FrameLayout 其实是最简略的一个布局管理器,因为它对子 view 是没有束缚的,无论程度方向还是垂直方向,对子 view 都是没有束缚,所以它的 measure 过程最简略。
如果是 EXACTLY 的,它自身的高度与宽度是确定的,那么就遍历子 view,measure 一下就能够了,最初再把 margin 和 padding 加一下就完事了。
如果是 AT_MOST 的,那么也不难,遍历子 View 并 measure,而后取子 view 中最大宽为它的宽度,取最大的高为其高度,再加上 margin 和 padding,基本上就做完了。
因为,FrameLayout 的 measure 过程最为简略,因而零碎里很多中央默认用的就是 FrameLayout,比方窗口里的 root view。
RelativeLayout
这个是最为简单的,从设计的目标来看,RelativeLayout 要解决的问题也是提供了长与宽两个维度来束缚子 view。
总体过的过程就是要别离从 vertical 方向和 horizontal 方向,来进行两遍的 measure,同时还要计算具体的坐标,实际上 RelativeLayout 的 measure 过程是把 measure 和 layout 一起做了。
自定义 View 如何实现 onMeasure
如果是一个具体的 View,那就相当简略了,默认的实现就能够了。
如果是 ViewGroup 会绝对简单一些,取决于如何从程度和垂直方向上束缚子 view,而后进行遍历,并把束缚思考进去。能够参考 LinearLayout 和 RelativeLayout 的 onMeasure 实现。
渲染之 layout
measure 是确定控件的尺寸,下一步就是 layout,也就是对控件进行排列。
首先,须要了解古代 GUI 窗口的坐标零碎,假如屏幕高为 height,宽为 width,那么屏幕左上角为坐标原点(0,0),右下角为(width, height),屏幕从上向下为 Y 轴方向,从左向右则是 X 轴方向。安卓当中,也是如此。每一个控件都是一个矩形区域,为了能晓得如何渲染每一块矩形(每 一个控件)就须要晓得它的坐标,在前一步 measure 中,能晓得它的宽与高,如果再能确定它的起始坐标左上角,那么它在整个屏幕中的地位就能够确定了。
对于 Android 来说,view 的渲染的第二步骤就是 layout,其目标就是要确定好它的坐标,每一个 View 都有四个变量 mLeft, mTop,mRight 和 mBottom,(mLeft, mTop)是它的左上角,(mRight, mBottom)是它的右下角,很显著 width=mRight-mLeft,而 height=mBottom-mTop。这些数值是绝对于父布局来说的,每个 View 都是存在于 view tree 之中,晓得绝对于父布局的数值就足够在渲染时应用了,没必要用绝对屏幕的相对数值,而且用绝对父布局的坐标数值再加上父布局的坐标,就能够失去在屏幕上的相对数值,如果须要这样做的话。
layout 过程仍然是从根节点开始的,所以仍要从 ViewRootImpl#performLayout 作为终点来理顺 layout 的逻辑。performLayout 的参数是一个 LayoutParam,以及一个 windowWidth 和 desiredWindowHeight,调用 performLayout 是在 performTraversal 当中,在做完 performMeasure 时,传入的参数其实就是窗口 window 的宽与高(因为毕竟是根节点嘛)。performLayout 中会从根节点 mView 开开对整个 view tree 进行 layout,其实就是调用 mView.layout,传入的是 0, 0 和 view 的通过 measure 后宽与高。
单个 View 的 layout 办法实现较简略,把传入的参数保留到 mLeft,mTop,mRight 和 mBottom 变量,再调用 onLayout 就完事了,这个很好了解,因为子 view 是由父布局确定好的地位,只有在 measure 过程把本人须要的大小通知父布局后,父布局会依据 LayoutParam 做安顿,传给子 view 的就是计算过后的后果,每个子 view 记录一下后果就能够了,不须要做啥额定的事件。
ViewGroup 稍简单,因为它要解决其子 view,并且要依据其设计的特点对子 view 进行束缚排列。还是能够看看常见的三个 ViewGroup 是如何做 layout 的。
LinearLayout
仍然是两个方向,因为 LinearLayout 的目标就是在某一个方向上对子 view 进行束缚。看 layoutVertical 就能够了,程度方向上逻辑是一样的。
遍历一次子 View 即可,从父布局的 left, top 起始,思考子 view 的 height 以及高低的 padding 和 margin,顺次排列就能够了。须要留神的是,对于 left 的解决,实践上子 view 的 left 就应该等于父布局,因为这毕竟是 vertical 的,程度上是没有束缚的,然而也要思考 Gravity,当然也要把 padding 和 margin 思考进来。最初通过 setChildFrame 把排列好的坐标设置给子 view。
总体来看,线性布局的 layout 过程比其 measure 过程要简略不少。
FrameLayout
FrameLayout 对子 view 的排列其实是没有束缚的,所以 layout 过程也不简单,遍历子 view,子 view 的 left 和 top 初始均为父布局,根据其 Gravity 来做一下排布即可,比方如果 Gravity 是 right,那么子 view 就要从父布局的右侧开始计算,childRight=parentRight-margin-padding,childLeft=childRight-childWidth,以次类推,还是比拟好了解的。
RelativeLayout
后面提到过 RelativeLayout 是在 measure 的时候就把坐标都计算好了,它的 layout 就是把坐标设置给子 view,其余啥也没有。
自定义 View 如何实现 onLayout
如果是自定义 View 的话,不须要做什么。
如果是自定义的 ViewGroup 的话,要看设计的目标,是如何排列子 view 的。
总之,layout 过程相较 measure 过程还是比拟好了解的,束缚规定越简单的 view,其 measure 过程越简单,但 layout 过程却不简单。
渲染之 draw
draw 是整个渲染过程的外围也是最简单的一步,后面的 measure 和 layout 只能算作筹备,draw 才会真正进行绘制。
draw 的整个逻辑流程
与 measure 和 layout 的过程十分不一样,尽管在 performTraversals 中也会调用 performDraw,也就是说看似 draw 流程的终点仍是 ViewRootImpl#performDraw,但查看一下这个办法的实现就能够发现,这外面其实并没有调用到 View#draw,就是说它其实也是做一些筹备工作,整个 View tree 的 draw 触发,并不在这里。
从 performDraw 中并没有做间接与 draw 相干的事件,它会调用另外一个办法 draw()来做此事件,在 draw 办法中,它会先计算须要渲染的区域(dirty 区域),而后再针对 此区域做渲染,失常状况下会走硬件加速形式去渲染,这部分比较复杂,它间接与一个叫做 ThreadedRenderer 打交道,稍后再作剖析。
因为各种起因,如果硬件加速未没有胜利,那么会走到软件渲染,这部分逻辑绝对清晰一些,能够先从这里看起,会间接调用到 drawSoftware(),这个办法有助于咱们看清楚渲染的流程。这个办法外面会创立一个 Canvas 对象,是由 ViewRootImpl 持有的一个 Surface 对象中创立进去的,并调用 view tree 根节点的 mView.draw(canvas),由此便把流程转移到了 view tree 下面。
view tree 的 draw 的过程
ViewRootImpl 是间接调用根节点的 draw 办法,那么这里便是整个 view tree 的入口。可先从 View#draw(canvas)办法看起。次要分为四步:1)画背景 drawBackground;2)画本人的内容通过 onDraw 来委派,具体的内容是在 onDraw 外面做的;3)画子 view,通过 dispatchDraw 办法;4)画其余的货色,如 scroll bar 或者 focus highlight 等。能够重点关注一下这些操作的程序,先画背景,而后画本人,而后画子 view,最初画 scroll bar 和 focus 之类的货色。
重点来看看 dispatchDraw 办法,因为其余几个都绝对十分好了解,这个办法次要要靠 ViewGroup 来实现,因为在 View 外面它是空的,节点本人只须要管本人就能够了,只有父节点才须要关注如何画子 View。ViewGroup#dispatchDraw 这个办法做一些筹备工作,如把 padding 思考进来并进行 clip,后会遍历子 View,针对 每个子 view 调用 drawChild 办法,这实际上就 是调用回了 View#draw(canvas,parent,drawingTime)办法,留神这个办法是 package scope 的,也就是说只能供 view 框架外部调用。这个办法并没有做具体的渲染工作(因为每个 View 的具体渲染都是在 onDraw 外面做的),这个办法外面做了大量与动画相干的各种变换。
Canvas 对象是从哪里来的
View 的渲染过程其实大都是 GUI 框架外部的逻辑流程管制,真正波及 graphics 方面的具体的图形如何画进去,其实都是由 Canvas 对象来做的,比方如何画点,如何画线,如何画文字,如何画图片等等。一个 Canvas 对象从 ViewRootImpl 传给 View tree,就在 view tree 中一层一层的传递,每个 view 都把其想要展现的内容渲染到 Canvas 对象中去。
那么,这个 Canvas 对象又是从何而来的呢?从 view tree 的一些办法中能够看到,都是从里面传进来的,view tree 的各个办法(draw, dipsatchDraw 和 drawChild)都只接管 Canvas 对象,但并不创立它。
从下面的逻辑能够看到 Canvas 对象有二个起源:一是在 ViewRootImpl 中创立的,当走软件渲染时,会用 Surface 创立出一个 Canvas 对象,而后传给 view tree。从 ViewRootImpl 的代码来看,它自身就会持有一个 Surface 对象,大略的逻辑就是每一个 Window 对象内,都会有一个用来渲染的 Surface;
另外一个起源就是走硬件加速时,会由 hwui 创立出 Canvas 对象。
draw 过程的触发逻辑
从下面的探讨中能够看出 draw 的触发逻辑有两条路:
一是,没有启用硬件加速时,走的软件 draw 流程,也是一条比拟好了解的简略流程:performTraversal->performDraw->draw->drawSoftware->View#draw。
二是,启用了硬件加速时,走的是 performTraversal->performDraw->draw->ThreadedRenderer#draw,到这里就走进了硬件加速相干的逻辑了。
硬件加速
硬件加速是从 Android 4.0 开始反对的,在此之前都是走的软件渲染,也就是从 ViewRoot(4.0 版本以前是叫 ViewRoot,起初才是 ViewRootImpl)中持有的 Surface 间接创立 Canvas,而后传给 view tree 去做具体的渲染,与后面提到的 drawSoftware 过程相似。
硬件加速则要简单得多,多了好多货色,它又搞出了一套渲染架构,但这套货色是间接与 GPU 分割,有点相似于 OpenGL,把 view tree 的渲染转换成为一系列命令,间接传给 GPU,软件渲染则是须要 CPU 把所有的运算都做了,最终生成 graphic buffer 送给屏幕(当然也是 GPU)。
这一坨货色中最为外围就是 RenderNode 和 RecordingCanvas。其中 RenderNode 是纯新的货色,它是为了构建 一个 render tree(相似于 view tree),用以构建简单的渲染逻辑关系。RecordingCanvas 是 Canvas 的一个子类,它是专门用于硬件加速渲染的,但又为了兼容老的 Canvas(软件渲染),为啥叫 recording 呢?因为硬件加速形式渲染,对于 view tree 的 draw 过程来说就是记录一系列的操作,这其实就是给 GPU 的指令,渲染的最初一步就是把整个 render tree 丢给 GPU,就完了。
后面说的两个是数据结构,还不够,还有 HardwareRenderer 和 ThreadedRenderer,这两个用来建设和治理 render tree 的,也就是说它们外部治理着一组由 RenderNode 组成的 render tree,并且做一些上下文环境的初始化与清理资源的工作。相似于 OpenGL 中 GLSurfaceView 的 RenderThread 做的事件。
硬件加速与原框架的切入点都是 RenderNode 和 RecordingCanvas,View 类中多了一个 RenderNode 成员,当 draw 的时候,从 RenderNode 中失去 RecordingCanvas,其余操作都与原来统一,都是调用 Canvas 的办法进行 graphics 的绘制,这样整体渲染流程就走入到了硬件加速外面。
Choreographer 与 vsync
尽管在 Android 4.0 版本退出了硬件加速的反对,但这还是不够,因为它只是相当于具体的渲染工夫可能快了一些,举例来说,可能是一般火车与高铁之间的差别,尽管的确行程所花工夫变短了,然而对于整体的效率来说晋升并不大。对于整体 GUI 的晦涩度,响应度,特地是动画这一块的流程水平与其余平台(如水果)差距仍是微小的。一个最重要的起因就在于,GUI 整体的渲染流程是短少协同的,仍是按需式渲染:应用层布局加载完了要渲染了,或者 ViewRootImpl 发现 dirty 了,须要重绘了,或者有用户事件了须要响应了,触发整体渲染流程,更新 graphic buffer,屏幕刷新了。
这一过程其实也没有啥大问题,对于惯例的 UI 显示,没有问题,我没有更新,没有变动,当然 不须要重绘了,如果有更新有变动时再按需从新渲染,这显然 没有什么问题。最大的问题在于动画,动画是要求间断不停的重绘,如果仅靠客户这一端(相较于 graphic buffer 和屏幕这一端来说)来触发,显然 FPS(帧率)是不够的,由此造成晦涩度必定不够好。
于是在 Android 4.1(Jelly Bean)中就引入了 Choreographer 以及 vsync 机制,来解决此问题,它们两个并不全完是一回事,Choreographer 是纯软件的,vsync 则是更为简单的更底层的机制,有没有 vsync,Choreographer 都能很好的工作,只不过有了 vsync 会更好,就好比硬件加速之于 View 的渲染,没有硬件加速也能够渲染啊,有了硬件加速渲染会更加的快一些。
Choreographer
它的英文本意是歌舞的编舞者,有点相似于导演,但歌舞个别工夫更短,所以对编舞者要求更高,须要在短时间内把精髓全副展示进去。它的目标就是要协调整个 View 的渲染过程,对输出事件响应,动画和渲染进行工夫上的把控。文档原文是说:Coordinates the timing of animations, input and drawing.,精髓就在于 timing 这个词上。
但其实,这个类自身并不是很简单,相较于其余 frameworks 层的货色来说它算简略的了,它就是负责定时回调,依照肯定的 FPS 来给你回调,简略来说它就是做了这么一件事件。它公开的接口也特地少,就是 postFrameCallback 和 removeFrameCallback,而 FrameCallback 也是一个非常简单的接口 doFrame(long frameTimeNanos),外面的参数是以后帧开始渲染的工夫序列。
所以,它的工作就是在计时,或者叫把控工夫,到了每一帧该渲染的时候了,它会通知你。有了它,那么 GUI 的渲染将不再是按需重绘了,而是有节奏的,能够以固定 FPS 定时刷新。ViewRootImpl 那头也须要做调整,每当有被动重绘时(view tree 有变动,用户有输出事件等),也并不是说立马就去做 draw,而是往 Choreographer 里 post 一个 FrameCallback,在外面做具体的 draw。
vsync(Vertical Synchronization)
垂直同步,是另外一套更为底层的机制,简略来了解就是由屏幕显示零碎间接向软件层派发定时的脉冲信号,用以进步整体的渲染晦涩水平,屏幕刷新,graphic buffer 和 window GUI(view tree)三者在这个脉冲信号下,做到同步。
vsync 是通过对 Choreographer 来发挥作用的。Choreographer 有两套 timing 机制,一是靠它本人实现的一套,另外就是间接传导 vsync 的信号。通过 DisplayEventReceiver(这个类对于 App 层是齐全不可见的被 hide 了)就能够接管到 vsync 的信号了,调用其 sheduleVsync 来通知 vsync 说我想接管下一次同步的信号,而后在重载 onVsync 办法以接管信号,就可能与 vsync 零碎连接起来了。
渲染性能优化
这是一个很大的话题
放弃简略
最最重要的准则就是要放弃简略,比方,UI 页面尽可能的简洁,view tree 的层级要尽可能的少,能用色彩就别用背景图片,能 merge 就 merge。
动画也要尽可能的简略,并且应用规范的 ValueAnimator 接口,而不要简略粗犷的去批改 LayoutParams(如 height 和 width)。
缩小重绘
这个要多用零碎中开发者模式外面的重绘调试工具来做优化,尽可能的缩小重绘。
专项定制
有些时候,对于一些非凡需要的 view 要进行定制优化。举个例子,比方一个巨简单的页面(如某宝的首页),中有一个用于显示倒计时的 view,实现起来并不简单,一个 TextView 就搞定了,一个 Timer 来倒计时,一直的刷新数字 就能够了。然而,这通常会导致整个页面都跟着在重绘。因为数字在变动,会导致 TextView 的大小在变动,进而导致整个 View tree 都在一直的跟着重绘。
像这种 case,如果遇到了,就须要自定义一个专门用于此的 View,并针对数字一直刷新做专门的优化,以不让其影响整个 view tree。
不要在意这个例子的真实性,要晓得,当某个 View 演变成了整个页面的瓶颈的时候,就须要专门针对 其进行非凡定制以优化整体页面的渲染性能。
更多的技巧能够参考这篇文章和前面的参考资料。
参考资料
列举一下对于此话题的比拟好的其余资源
- Android 视图绘制流程齐全解析,带你一步步深刻理解 View
- [Android 性能优化第(四)篇 —Android 渲染机制
](https://www.jianshu.com/p/9ac245657127) - 深刻 Android 渲染机制
- Android 进阶——性能优化之布局渲染原理和底层机制机详解及卡顿本源探索(四)
- View 渲染机制
- Android 屏幕刷新机制
- Android 基于 Choreographer 的渲染机制详解
- Android 图形渲染之 Choreographer 原理
原创不易,打赏 , 点赞 , 在看 , 珍藏 , 分享 总要有一个吧