Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
安卓ui测试_android软件开发工具,希望能够帮助你!!!。
请「关注」我的头条号:「安卓技术精选」,我会定期更新相关的 Android 开发技术,以免错过。
UI 渲染性能是 Android 应用的一个很重要的指标,尤其对于一些精品 APP 来说,更是如此。平时的开发中,可能大家并不会很在意这一类的指标,因为随着 Android 手机硬件性能飙升,即使不去刻意的优化,一般来说,界面一样还是比较流畅的。但是并不代表我们不需要去了解它,假设哪天真的遇到了 UI 渲染卡顿、丢帧,该如何解决呢?
本文将带大家去学习并了解整个 UI 渲染相关的知识,并给出一些最佳实践,平时在开发过程中按照最佳实践去执行即可。本文将从如下几个章节去展开
(一)measure、layout、draw 的过程
(二)UI 性能检测工具
(三)实战
(四)最佳实践
本文先来说说 View 的 measure、layout、draw 的过程。
第一章 Measure、Layout、Draw
一个 xml 布局文件配置的界面是怎么显示到界面上的呢?主要有四步:inflate、measure、layout 和 draw。inflate 主要是解析 xml 文件,形成控件树,并得到一个根 view。当这个 view 被添加到一个父控件中去之后,即可根据父控件布局信息来该 view 以及其子控件来进行排版渲染的工作。measure 主要就是根据父控件的布局信息以及自身的布局参数来进行大小测量。layout 是根据父控件的布局信息以及自身的大小来进行位置调整。draw 是将自身按照 layout 的到的位置信息绘制到屏幕上。
为何先从这个过程开始说起呢?因为它将涉及到 UI 性能优化的始终,UI 性能优化的很多指标也是围绕这三个步骤来做的。本文中主要是介绍 Measure、Layout、Draw 这三个步骤里面的主要过程,不去展示内部代码是如何实现的。一方面我们的目的是知道整体的过程,后面的 UI 性能优化实践也会针对这几个主要的过程去展开。另一方面,如果展示代码的话,篇幅就太长而且文章的重点也会跑偏了。
Android 中所有 UI 控件的基类是 View 类,ViewGroup 继承 View 类。大家都知道,Activity 中具体实现界面的 Window 类是 PhoneWindow,PhoneWindow 中的 DecorView 是整个 UI 的根 View,它继承 ViewGroup,这里具体的细节我们就不展开了。总之,Android 上可见的控件都是继承自 View 类,父控件都是继承 ViewGroup,所以各个控件的 measure、layout、draw 的过程最终的实现都在 View 类和 ViewGroup 类中。
整个 View 排版渲染系统的思路是:先对所有控件进行一次 measure 操作,得到所有控件的大小。然后再根据大小以及各个控件的 margin、padding 等这些信息对各个控件设定位置。最后根据控件在屏幕中的位置将控件绘制出来。
1.1 Measure 的过程与原理
measure 的主要过程为
View:measure -> onMeasure
ViewGroup:没有做任何处理,提供了 measureChild、measureChildWithMargins 方法
ViewGroup 子类:measure(View) -> onMeasure -> measureChild / measureChildWithMargins -> child.measure
这里 View 的 measure 为 final 的,不可重载,留给子类重载的方法为 onMeasure,子类还会有一次更改大小的机会。ViewGroup 不需要重载,默认情况下,它就使用 View 设定的大小即可,它本身提供了 measureChild、measureChildWithMargins 方法供子类使用。ViewGroup 的子类如 LinearLayout 则需要重载 onMeasure 方法,因为具体的这些布局控件是需要根据所有子控件的大小再来设定自己的大小的,如果定义了 layout_width 为 wrap_content 的话。
通常情况下,measure 的结果就是一个普通的 int 数值即可。在 Android 中,布局系统支持 wrap_content、match_parent 和具体制定一个值三种方式,在一个 int 值中怎么去体现呢?Android 中是用一个 int 值的前 2 个 bit 位表示 measure mode,后 30 个 bit 位表示 measure size,下文用 mode | size 这样的一对符号来表示。mode 的各个定义为
UNSPECIFIED:不限定大小
EXACTLY:确切的大小
AT_MOST:可使用的最大的大小
如果某控件 measure 完成,宽度得到的结果为 AT_MOST | 30,它表示该控件最多可占据 30 像素宽。View 的 measure 方法定义如下
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
这里可以看到,每一个控件的 MeasureSpec 是由外部传入的,那这个值是从哪里来的呢?
首先最初的 MeasureSpec 会由 ViewRoot 传入整个控件树的根 View 中,具体值为 EXACTLY | windowSize,调用到整个控件树根 View 的 measure 方法中,该 View 为一个 ViewGroup 的实例,则调用其 onMeasure 方法,该方法又会调用 measureChild、measureChildWithMargins 方法来进行子控件的大小测量。这两个方法会调用 getChildMeasureSpec 方法获取子控件的 MeasureSpec,获取完成之后调用子控件的 meausre 方法。measureChildWithMargins 具体过程如下
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
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) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
...
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这里没有完全展示这个部分,它还有另外两种情况的处理。所以主要的 measure 的逻辑其实是在 ViewGroup 的子类中,如 LinearLayout、RelativeLayout 等。它们在 onMeasure 中对子控件大小进行测量,完成之后会将所有控件大小之和计算出来,如果它们自身的 layout 参数为 wrap_content 的话,则会根据子控件大小之和来设定自己的 MeasureSpec。这样不断地循环,整个控件树的 measure 的过程就完成了。
measure 完成之后,控件树上的每一个控件都会得到 measureWidth 和 measureHeight 两个值,其前 2 个 bit 位为 measure mode,后 30 个 bit 位为 measure size。
1.1.1 小结
控件的 measure 值为一个 int 值,前 2 个 bit 位为 measure mode,后 30 个 bit 位为 measure size。mode 包括 EXACTLY、EXACTLY、UNSPECIFIED 三种
View 的调用过程为 measure -> onMeasure,ViewGroup 不重载 onMeasure,ViewGroup 的子类重载 onMeasure
最初的 MeasureSpec 为 EXACTLY | windowSize,会由 ViewRoot 传入整个控件树的根 View 中。measure 操作从根 View 开始,调用 View 的 meausre,根 View 为一个 ViewGroup 的子类,则重载 onMeasure,循环调用 ViewGroup 提供的 measureChild、measureChildWithMargins 的方法对子控件进行 measure 操作,完成之后的到子元素占据的大小,来设定自己所占据的大小。
1.2 Layout 的过程与原理
layout 的主要过程是
View:layout -> onLayout
ViewGroup:不做任何处理
ViewGroup 子类:layout(View) -> onLayout -> child.layout
经过了 measure 之后,每个控件都获得一个 measure size,布局的过程就是调用 layout 方法,将 left、top、right、bottom 几个值设定到控件的 mLeft、mTop、mRight、mBottom 上,其定义如下
public void layout(int l, int t, int r, int b) {
...
setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
}
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
同理,最初的 left, top, right, bottom 值会由 ViewRoot 调用到控件树的根 View 上,该根 View 为一个 ViewGroup 的子类,则进入重载的 onLayout 中,在 onLayout 中根据子控件的 measure 的值依次计算剩余的位置大小,依次调用 child.layout 设定子控件的上下左右的坐标值,该值是相对于父控件的,这里一定要谨记。
它这个 layout 参数的设计有个好处就是,即使实现不同的布局组件,不需要子元素的代码做任何的改变和支持,所有复杂的操作全部封装在了布局组件的 onLayout 方法中即可。
1.3 Draw 的过程与原理
draw 的主要过程
View:draw -> drawBackgroud -> onDraw -> dispatchDraw -> onDrawForeground
ViewGroup:dispatchDraw -> drawChild
ViewGroup 子类:没有做任何处理
相比 measure、layout 来说,draw 是最复杂的操作了,这里只说主要的步骤与过程以及对 UI 性能有影响的地方。这里主要有几步
public void draw(Canvas canvas) {
drawBackground(canvas);
onDraw(canvas);
dispatchDraw(canvas);
onDrawForeground(canvas);
}
首先,drawBackground 绘制背景,这个是引起 GPU 过度绘制的主要元凶,因为它占据了几乎整个控件的大小,绘制面积很大。然后调用 onDraw 函数,留给子类处理自身的绘制。dispatchDraw 主要是给 ViewGroup 用的,个人觉得这个方法的设计并不好,不应该放在这里。一方面它破坏了 measure -> onMeasure,layout -> onLayout, draw -> onDraw 这样的一致性,不完美。另一方面,dispatchDraw 本身是给子控件绘制用的,一个 View 并不是 ViewGroup,在这一层上,其实是不应该有子控件的相关操作的。最后调用 onDrawForeground,主要是绘制滚动条,这个滚动条设计放在 View 这个层面我也不是很赞同,对很多常用控件来说,根本不需要,简直就是累赘,应该设计到需要用到的控件中,比如 ScrollView 这类控件中。
View 的 draw 方法主要就是包括这几步。ViewGroup 中的 dispatchDraw 会相对复杂一点。主要是以下几步
@Override
protected void dispatchDraw(Canvas canvas) {
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
for(int i = 0; i < childrenCount; i++) {
drawChild(canvas, transientChild, drawingTime);
}
for(int i = disappearingCount; i >= 0; i--) {
drawChild(canvas, child, drawingTime);
}
}
这里主要是看是否有 layoutAnimation 存在,如果定义了的话,则需要启动动画。动画的启动放到 draw 里面,感觉也是怪怪的,个人觉得动画应该由一个动画管理器来驱动,而不是在 draw 方法中去驱动(我自己写过 UI 渲染引擎,Github 地址为 https://github.com/myz7656/smart-ui,这里有完整的 measure、layout、draw 的过程以及动画的实现)。后面两个 for 循环第一个是绘制子控件,第二个是绘制将要消失的子控件,这里这样写的原因是假如删除的控件存在动画,则需要慢慢消失,但是又不能响应消息,所以需要和一般的子控件区分开来。
drawChild 中会调用
child.draw(Canvas canvas, ViewGroup parent, long drawingTime)
主要作用是:(1)应用 child 中的变换矩阵。(2)将 canvas 的坐标原点移动到 (mLeft, mTop) 这个点上,这样在 draw 中绘制的时候就是以 (0, 0) 点去绘制的了。(3)设置 clipRect 为子控件的区域,这样在 draw 方法中绘制就不会超出子控件的区域了。(4)设定好 canvas 的各个参数之后,调用真正的 draw(Canvas canvas) 方法去做绘制。
至此,ViewGroup 中的 dispatchDraw 方法就完成了,其中对 UI 性能影响比较大的一个点就是 drawBackground,很多人喜欢给布局管理器加背景,其实很多时候是没有必要的。这些过程中,很多点我们并没有办法优化,可以做的是,少定义 backgroud,其次是自己实现的 onDraw 里面尽量不要有重叠绘制。
下一章:UI 性能检测工具的使用,请「关注」我的头条号:「安卓技术精选」,我会定期更新相关的 Android 开发技术,以免错过。
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。