Android视图体系简介
在Android中,你看到的每一个按钮、文字、图片,甚至整个屏幕,都是由一种叫做"视图"(View)的东西构成的。你可以把"视图"想象成一块矩形的画布,你可以在上面画出你想要的任何东西。
这些"视图"可以互相包含,就像一棵树一样,我们把它叫做"视图树"。在这棵树里,每个"视图"都有自己的大小、形状、颜色和位置。就像你在画画时,每个颜色都有自己的位置和大小。
在Android中,"视图"有两种类型:"View"和"ViewGroup"。所有你能看到的东西(比如按钮、文字等)都是"View",而那些用来放其他"View"的地方(比如整个屏幕或者一个窗口)我们叫它"ViewGroup"。
从XML到屏幕视图的过程
在Android中,XML到屏幕视图的过程主要涉及以下步骤:
- XML布局文件 :在Android开发中,界面通常在XML文件中定义。这些文件位于应用项目的
res/layout
目录下。每个XML布局文件都对应一个Android界面。 - 解析XML:当应用运行时,系统会使用LayoutInflater类来解析这些XML文件,并将其中定义的元素转换为Android中的View和ViewGroup对象。
- 生成视图树:LayoutInflater解析XML并创建相应的View和ViewGroup对象后,会按照XML中元素的嵌套关系,生成一棵视图树(View Tree)。
- 测量和布局:生成视图树后,系统会进行测量和布局过程。在这个过程中,系统会确定每个视图的大小和位置。
- 绘制:最后,系统会调用每个视图的draw()方法将其绘制到屏幕上。
这个过程用大白话来说就是 : 1.确定屏幕上要画点啥东西(xml->view) 2.看看每个东西画多大(onMeasure) 3.看看每个东西放哪里(onLayout) 4.把他画出来(onDraw)
XML布局文件
在Android中,我们通常使用XML布局文件来定义用户界面。XML布局文件是一种特殊的文档,它使用标签(tag)来描述视图和视图组的属性。这些标签看起来就像这样:
xml
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击我!"/>
在上面的例子中,<Button>
是一个标签,它代表一个按钮。android:layout_width
和android:layout_height
是属性,它们定义了按钮的宽度和高度。android:text
则定义了按钮上显示的文字。
你可能注意到了,所有的属性名都以"android:"开头。这是因为这些属性是Android系统预先定义好的。当然,你也可以自己定义新的属性。
在XML布局文件中,我们可以使用嵌套标签来创建视图树。比如下面的例子:
xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击我!"/>
</LinearLayout>
在这个例子中,"LinearLayout"是一个"ViewGroup",它包含了一个"Button"视图。
XML解析
常用的XML解析技术
在Android开发中,XML是一种常见的数据格式,我们经常需要解析XML数据。以下是三种常用的XML解析技术:
-
SAX(Simple API for XML):SAX是一种基于事件的解析方法。它在读取XML文档时会触发一系列事件,如开始文档、开始元素、字符数据、结束元素和结束文档等。你可以通过实现相应的事件处理方法来获取你需要的数据。
-
DOM(Document Object Model):DOM是一种将整个XML文档读入内存并构建成一个树形结构的解析方法。这使得你可以方便地对XML文档进行增删改查操作。然而,由于DOM需要加载整个文档,因此它不适合用来解析大型XML文件。
-
Pull:Pull解析器是Android特有的一种XML解析技术。它结合了SAX和DOM的优点:既可以像SAX那样逐步解析XML文件,又可以像DOM那样随时查找和操作数据。
以下是一个简单的使用Pull解析器解析XML文件的示例:
java
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlData));
int eventType = xmlPullParser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
String nodeName = xmlPullParser.getName();
switch (eventType) {
// 开始解析某个节点
case XmlPullParser.START_TAG: {
if ("name".equals(nodeName)) {
Log.d("MainActivity", "name is " + xmlPullParser.nextText());
}
break;
}
// 完成解析某个节点
case XmlPullParser.END_TAG: {
break;
}
default:
break;
}
eventType = xmlPullParser.next();
}
} catch (Exception e) {
e.printStackTrace();
}
Android中的XML解析
在Android开发中,XML被广泛用于定义界面布局、资源和配置文件。因此,理解Android如何解析XML文件是非常重要的。
以下是Android中解析XML文件的主要步骤:
-
加载XML文件:首先,Android会从APK的资源表中加载XML文件。这个过程通常由Resources类进行。
-
解析XML文件:然后,Android会使用一个XML解析器来读取和解析XML文件。在Android中,默认使用的是Pull解析器。
-
创建视图或对象 :根据XML文件中的元素和属性,Android会创建相应的视图或对象。例如,当解析到一个
<Button>
元素时,Android会创建一个Button视图;当解析到一个<string>
元素时,Android会创建一个字符串资源。 -
应用样式和主题:在创建视图时,Android会考虑当前的样式和主题,并将相应的属性值应用到新创建的视图上。
XML文件的加载和解析过程
在Android中,XML文件的加载和解析过程是一项复杂的任务,它涉及到资源管理、XML解析和视图创建等多个步骤。以下是这个过程的详细说明:
- 资源管理:在Android系统中,所有的资源文件(包括XML文件)都会在编译时被转换为二进制格式,并存储在APK包的resources.arsc文件中。当你需要加载一个资源时,Android会通过AssetManager类来读取这个文件并找到相应的资源。
AssetManager类有一个名为openXmlResourceParser()
的方法,它可以打开一个XML资源并返回一个XmlResourceParser对象。XmlResourceParser是XmlPullParser的一个实现,它添加了一些额外的方法来处理Android特有的资源引用。
java
public XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException {
// ...
}
- XML解析 :使用XmlResourceParser对象,你可以按照标准的Pull解析模式来读取和解析XML数据。你可以调用
getEventType()
方法来获取当前事件类型,然后根据事件类型进行相应的处理。
java
XmlResourceParser xrp = resources.getLayout(R.layout.activity_main);
while (xrp.getEventType() != XmlPullParser.END_DOCUMENT) {
if (xrp.getEventType() == XmlPullParser.START_TAG) {
String tagName = xrp.getName();
// 处理开始标签
} else if (xrp.getEventType() == XmlPullParser.END_TAG) {
// 处理结束标签
}
xrp.next();
}
LayoutInflater介绍
XML文件解析完成后,便需要把解析结果转换成视图. 在Android中,我们通常使用一个叫做LayoutInflater的工具来将XML布局文件转换成视图。你可以把它想象成一个工厂,它的工作就是读取XML布局文件,然后按照文件中的描述创建视图。
LayoutInflater会遍历XML布局文件中的每一个标签,并为每个标签创建对应的视图对象。例如,当LayoutInflater遇到<Button>
标签时,它就会创建一个Button对象;当遇到<LinearLayout>
标签时,就会创建一个LinearLayout对象。
LayoutInflater还会读取标签中的属性,并设置给对应的视图对象。例如,在<Button android:text="点击我!"/>
这个标签中,LayoutInflater会创建一个Button对象,并设置其text属性为"点击我!"。
在实际开发中,我们通常不直接使用LayoutInflater,而是通过Activity或Fragment的setContentView()
方法来加载XML布局文件。这个方法内部其实就是使用了LayoutInflater来完成工作。
LayoutInflater解析XML布局文件的过程
LayoutInflater是一个非常重要的类,在Android中负责将XML布局文件转换为实际的视图对象。它是View子系统的核心部分,几乎所有需要动态加载布局的地方都会用到它。
LayoutInflater工作的过程可以简单地分为两个步骤:
解析XML:首先,LayoutInflater会读取XML布局文件,并解析其中的每一个元素(标签)。对于每一个标签,LayoutInflater都会创建一个与之对应的视图或视图组对象。例如,标签会被转换为一个TextView对象,标签会被转换为一个LinearLayout对象。
设置属性:然后,LayoutInflater会读取每个标签中定义的属性,并将这些属性设置给对应的视图对象。例如,如果一个TextView标签中定义了android:text="Hello",那么创建出来的TextView对象就会显示"Hello"这个文本。
值得注意的是,虽然我们通常不直接使用LayoutInflater,但理解它的工作原理对于理解Android如何加载和显示用户界面是非常有帮助的。
另外,你可能还听说过inflate()方法。这个方法就是LayoutInflater用来将XML布局文件转换为视图对象的。当你调用inflate()方法时,你需要传入两个参数:一个是要加载的XML布局文件的资源ID,另一个是新创建的视图将要添加到的父视图(通常是一个ViewGroup)。
在Android源码中,LayoutInflater的主要工作是通过inflate()
方法来完成的。这个方法有几个重载版本,但最主要的一个版本是这样的:
java
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
// ...
}
这个方法接受三个参数:一个XmlPullParser对象(用于解析XML),一个ViewGroup对象(新创建的视图将被添加到这个ViewGroup中),以及一个boolean值(决定新创建的视图是否立即被添加到ViewGroup中)。
在inflate()
方法内部,LayoutInflater会创建一个名为ParseState
的内部类对象。这个对象包含了解析过程中需要用到的一些状态信息。
然后,LayoutInflater会调用rInflate()
方法开始解析XML。这个方法是递归调用的,对于XML中的每一个标签,它都会创建一个对应的视图对象,并设置其属性。
创建视图对象的工作是通过createViewFromTag()
方法完成的。这个方法会根据标签名来确定需要创建哪种类型的视图对象。例如,如果标签名是"Button",那么就会创建一个Button对象。
设置属性的工作则是通过applyAttributes()
方法完成的。这个方法会读取标签中定义的所有属性,并使用反射机制将这些属性设置给视图对象。
下面是createViewFromTag()
和applyAttributes()
两个方法的简化版本:
java
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
// 创建视图对象
View view = createView(name, context, attrs);
// 设置视图属性
applyAttributes(view, attrs);
return view;
}
private void applyAttributes(View view, AttributeSet attrs) {
// 获取视图类
Class<? extends View> viewClass = view.getClass();
// 遍历所有属性
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attributeName = attrs.getAttributeName(i);
String attributeValue = attrs.getAttributeValue(i);
// 使用反射机制设置属性
Field field = viewClass.getField(attributeName);
field.set(view, convertValueToSuitableType(field.getType(), attributeValue));
}
}
注意以上代码是为了方便说明主要过程编写的简化版哈
生成视图树
生成的视图树对象长啥样?
在Android中,视图树(包括ViewGroup和View)是以对象的形式存在于内存中的。每一个View或ViewGroup对象都占据了一部分内存空间。
在一个ViewGroup中,它的所有子视图被保存在一个ArrayList中。这个ArrayList的索引顺序就对应了子视图在XML布局文件中的定义顺序(也就是绘制顺序)。所以,你可以通过调用getChildAt(int index)
方法来获取指定索引位置的子视图。
当你需要查找某个子视图时,系统会遍历这个ArrayList,直到找到匹配的视图为止。例如,你可以通过调用findViewById(int id)
方法来按照ID查找子视图。这个方法会递归地搜索整个视图树,直到找到第一个具有指定ID的视图为止。
需要注意的是,由于查找操作可能涉及到遍历整个视图树,所以如果视图树非常大,那么这个操作可能会比较耗时。因此,在实际开发中,我们应该尽量优化我们的布局结构,避免创建过于复杂的视图树。
视图树查找--findViewById实现
举个例子
假设我们有一个RelativeLayout,它包含一个LinearLayout,而这个LinearLayout又包含两个Button:
xml
<RelativeLayout
android:id="@+id/relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2" />
</LinearLayout>
</RelativeLayout>
在内存中,这个RelativeLayout、LinearLayout和两个Button都是以对象的形式存在的。每个对象都占据了一部分内存空间。
在RelativeLayout对象中,它有一个ArrayList成员变量,用于保存所有子视图(即LinearLayout)。同样地,在LinearLayout对象中,它也有一个ArrayList成员变量,用于保存所有子视图(即两个按钮)。
当我们需要查找某个子视图时,例如通过ID查找第一个按钮,我们可以调用findViewById(int id)
方法:
java
Button button1 = findViewById(R.id.button1);
这个方法会遍历整个视图树,直到找到第一个具有指定ID的视图为止。在这个例子中,系统会先检查RelativeLayout自身,然后检查LinearLayout,接着检查第一个按钮,最后检查第二个按钮。当它找到第一个按钮时,就会停止搜索并返回这个按钮。
以上就是ViewGroup在内存中的存储和查找子视图的方式。
测量 布局和绘制
在Android中,视图的渲染过程是一个复杂的过程,涉及到测量、布局和绘制三个主要步骤。让我们详细看一下这些步骤。
-
测量阶段(Measure) :在这个阶段,系统需要确定每个视图需要多大的空间。这是通过调用每个视图的
measure()
方法来完成的。每个视图都会根据自己的内容和样式(例如文字大小、边距等)以及父视图给出的限制(例如最大可用空间)来决定自己需要多大的空间。这个过程可能会递归地进行,因为一个视图组(ViewGroup)在测量自己的尺寸时,需要先测量其所有子视图的尺寸。 -
布局阶段(Layout) :在这个阶段,系统需要确定每个视图在屏幕上的位置。这是通过调用每个视图的
layout()
方法来完成的。每个视图都会根据自己在父视图中的位置和自己的尺寸来确定自己在屏幕上的位置。同样,这个过程也可能会递归地进行,因为一个视图组在布局自己的子视图时,需要先确定自己的布局。 -
绘制阶段(Draw) :在这个阶段,系统需要让每个视图在屏幕上画出自己。这是通过调用每个视图的
draw()
方法来完成的。每个视图都会根据自己的尺寸和位置来绘制自己的内容。例如,一个TextView会绘制出它包含的文本,一个ImageView会绘制出它包含的图片。
以上就是Android中视图渲染到屏幕上的基本过程。虽然看起来很复杂,但实际上,在大多数情况下,你并不需要直接处理这些过程。Android系统已经为我们提供了一套完整而强大的机制来处理用户界面渲染。
测量
在测量阶段,Android系统需要确定每个视图需要多大的空间。这是通过调用每个视图的measure()
方法完成的。
measure()
方法接收两个参数:widthMeasureSpec
和heightMeasureSpec
。这两个参数是由父视图传递给子视图的,它们包含了尺寸的大小值和模式。
大小值表示可用的空间大小,模式则有三种可能:
-
EXACTLY
:如果模式是这个值,那么大小值就是精确的尺寸。 -
AT_MOST
:如果模式是这个值,那么大小值就是最大可用的尺寸。 -
UNSPECIFIED
:如果模式是这个值,那么大小值没有实际意义,通常情况下不会使用。
当你在XML布局文件中为视图设置"wrap_content"或者"match_parent"属性时,系统就会根据这些属性来生成对应的测量规格(MeasureSpec)。
以下是一个简化版本的measure()
方法:
java
protected void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 解析测量规格
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 计算视图尺寸
int width, height;
if (widthMode == MeasureSpec.EXACTLY) {
// 如果宽度模式是EXACTLY,那么宽度就是精确值
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
// 如果宽度模式是AT_MOST,那么宽度就不能超过最大可用尺寸
width = Math.min(desiredWidth, widthSize);
} else {
// 如果宽度模式是UNSPECIFIED,那么宽度就是期望值
width = desiredWidth;
}
// 高度计算方式与宽度相同
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
// 设置视图尺寸
setMeasuredDimension(width, height);
}
在上面的代码中,desiredWidth
和desiredHeight
是视图期望的宽度和高度。这通常是根据视图的内容和样式来计算的。例如,对于一个TextView,它的期望宽度就是它包含的文本所需要的宽度。
布局
布局阶段是确定所有视图在父视图容器中的位置。每个 View 和 ViewGroup 都有一个 onLayout() 方法,它接受四个参数:left、top、right 和 bottom,这四个参数分别表示视图在父视图容器中的位置。
在Android的布局过程中,计算视图的left、top、right和bottom的过程通常在父容器的onLayout()
方法中进行。这四个参数分别表示视图在父容器中的位置。
以下是一个简单的例子,展示了如何在自定义的ViewGroup中布局子视图:
java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
// 布局子视图
child.layout(left, 0, left + width, height);
left += width;
}
}
}
这个例子中,我们假设所有的子视图都水平排列,并且他们的高度都等于父容器的高度。所以,每个子视图的top和bottom就分别是0和父容器的高度;而left和right则通过不断累加子视图宽度来计算。
这只是一个基本的例子,实际上,如何计算left、top、right和bottom取决于你的布局需求。例如,如果你需要垂直排列子视图,或者需要考虑到子视图间的间距等因素,那么你就需要相应地修改这部分代码
绘制
在Android中,视图的绘制过程是由系统自动完成的,开发者只需要在自定义视图时重写onDraw()
方法即可。下面我们来详细看一下绘制的实现过程和原理。
绘制(Draw)
绘制阶段是将视图显示到屏幕上。每个View都有一个onDraw()
方法,系统会传入一个Canvas对象,你可以使用这个Canvas对象的各种方法来绘制你的视图。
在自定义View时,你需要重写onDraw()
方法,并在其中编写你的绘制代码。以下是一个简单的例子:
java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
}
这个例子中,我们将视图填充为红色。
原理
Android的绘制过程是从根节点开始,递归地对每一个节点进行绘制。对于每一个节点,系统首先调用其draw()
方法,然后调用其onDraw()
方法。draw()
方法会按照背景、主体内容和子视图等的顺序来进行绘制。
布局性能优化
在Android开发中,布局性能优化是一个非常重要的主题。以下是一些常见的优化策略:
-
减少布局层级:每多一层布局,系统就需要多做一次测量、布局和绘制操作。这会消耗更多的CPU和内存资源,导致应用运行变慢。因此,我们应该尽量减少布局的层级。RelativeLayout和ConstraintLayout都可以帮助我们实现复杂的布局,而不需要增加额外的布局层级。
-
避免过度绘制 :过度绘制是指同一个像素在同一帧中被多次绘制的情况。例如,如果你在一个视图上叠放了多个具有不透明背景的视图,那么这个像素就会被过度绘制。过度绘制会浪费GPU资源,降低渲染性能。我们可以通过设置
android:background="@null"
来阻止视图绘制不必要的背景。 -
使用ViewHolder模式 :对于ListView或RecyclerView等可以复用行视图的控件,我们应该使用ViewHolder模式来减少视图的创建和查找操作。ViewHolder模式通过缓存行视图中的子视图引用,避免了每次都需要通过
findViewById()
方法来查找子视图。 -
预加载和延迟加载:对于大量数据,我们可以使用预加载和延迟加载技术来提高滑动流畅性。预加载是指在用户滑动到数据之前就提前加载;延迟加载则是指只有当数据真正需要显示时才进行加载。
-
使用硬件加速:硬件加速可以利用GPU的强大计算能力来提高渲染性能。但是请注意,并非所有的2D绘图操作都支持硬件加速,某些操作可能会因为开启硬件加速而变慢或者出错。
-
理解不同ViewGroup的特性:不同类型的ViewGroup(如LinearLayout, RelativeLayout, ConstraintLayout等)有着不同的布局特性和性能表现。例如,RelativeLayout可能需要进行两次测量过程,因为它的布局依赖关系可能需要在所有子视图都测量完毕后才能确定。而LinearLayout在处理权重(weight)属性时也可能需要进行多次测量。理解这些特性,能帮助我们更好地选择和使用ViewGroup。
以上就是一些更深入的布局性能优化策略。以下是不同策略的详细介绍
如何判断和减少布局层级
在Android中,每个视图都有一个父视图(除了根视图),并且可能有多个子视图。这构成了一个视图树。视图树的深度,就是从根视图到最深的子视图所经过的视图数量。
判断布局层级
你可以通过Android Studio的布局检查器(Layout Inspector)来查看布局层级。使用这个工具,你可以看到当前运行的应用的实时布局层次结构。
另外,在开发者选项中开启"显示布局边界"(Show layout bounds)也可以帮助你理解布局的结构。
减少布局层级
减少布局层级主要有以下几种方法:
-
使用更复杂的ViewGroup:RelativeLayout和ConstraintLayout能够实现更复杂的布局,而不需要增加额外的布局层级。例如,ConstraintLayout就可以实现任意两个视图之间的相对位置关系。
-
避免无用的包装:有时候,我们可能会在一个视图外面包裹一个无用的ViewGroup,只是为了设置一些背景或者边距等样式。这种情况下,我们应该尝试直接将样式设置给子视图,以避免增加额外的布局层级。
-
使用include和merge标签 :如果同一个布局在多处被重用,那么我们可以使用
<include>
标签来引用它,而不是复制粘贴整个布局。此外,如果一个被引用的布局只需要添加到父布局中(而不需要额外的样式设置),那么我们还可以在被引用的布局文件中使用<merge>
标签来替代根元素,这样就可以避免生成一个无用的父视图。
以上就是判断和减少布局层级的基本方法。在实际开发中,我们应该时刻注意保持简洁高效的布局结构。
如何避免过度绘制
过度绘制(Overdraw)是指同一个像素在同一帧中被多次绘制的情况。在Android中,每一帧都需要重新绘制屏幕上的所有像素。如果一个像素被多次绘制,那么就会浪费GPU资源,降低渲染性能。
避免过度绘制的原理
当Android系统渲染视图树时,它会从根视图开始,按照深度优先的顺序来绘制每个视图。也就是说,父视图会先于子视图被绘制,而兄弟视图则按照它们在布局文件中的顺序来绘制。
因此,如果一个视图有背景,并且它的子视图也有背景,那么这个像素就会被重复绘制。同样,如果两个兄弟视图重叠,并且都有不透明的背景或内容,那么重叠部分的像素也会被重复绘制。
避免过度绘制的方法
-
移除不必要的背景 :如果一个视图只是作为容器使用,并且不需要显示任何背景或内容,那么我们应该移除它的背景。这可以通过设置
android:background="@null"
来实现。 -
使用透明或半透明颜色:如果一个视图需要显示背景,但是它的父视图或者下层兄弟视图已经有了背景,那么我们可以考虑使用透明或半透明颜色作为它的背景。
-
合并重叠的视图:如果两个兄弟视图有重叠部分,并且都需要显示不透明的内容,那么我们可以考虑将它们合并为一个单独的自定义视图。
-
使用开发者选项中的过度绘制调试工具:在开发者选项中,有一个"调试GPU过度绘制"选项。当你开启这个选项后,系统会用颜色来标识屏幕上的过度绘制区域。这可以帮助你找到并解决过度绘制问题。
以上就是避免过度绘制的原理和方法。在实际开发中,我们应该时刻注意视图的渲染性能。
布局性能优化 - 使用ViewHolder模式
在Android中,对于可以滚动和复用行视图的控件(如ListView或RecyclerView),系统为了节省内存和提高滑动流畅性,会在滚动时复用已经滚出屏幕的行视图。但是,每次复用行视图时都需要通过findViewById()
方法来查找子视图,这个操作是非常耗时的。
为了解决这个问题,Android引入了ViewHolder模式。ViewHolder模式通过缓存行视图中的子视图引用,避免了每次都需要通过findViewById()
方法来查找子视图。
使用ViewHolder模式
以下是一个简单的例子,展示了如何在RecyclerView的适配器中使用ViewHolder模式:
java
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private List<String> data;
public MyAdapter(List<String> data) {
this.data = data;
}
// 创建新的行视图(被LayoutManager调用)
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View rowView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_layout, parent, false);
return new MyViewHolder(rowView);
}
// 替换行视图的内容(被LayoutManager调用)
public void onBindViewHolder(MyViewHolder holder, int position) {
String item = data.get(position);
holder.textView.setText(item);
}
// 返回数据集的大小
public int getItemCount() {
return data.size();
}
// 提供对行视图中各个视图的引用
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
MyViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView);
}
}
}
在上面的代码中,我们定义了一个名为MyViewHolder的静态内部类,并在其中缓存了行视图中各个子视图的引用。然后,在onBindViewHolder()
方法中,我们可以直接使用这些引用来更新子视图的内容。
以上就是使用ViewHolder模式提升性能的原理和方法。在实际开发中,我们应该尽可能地使用这种模式来提高列表滚动的流畅性。
布局性能优化 - 预加载和懒加载
在Android开发中,对于大量数据的处理,我们可以使用预加载和懒加载技术来提高滑动流畅性。
预加载(Preloading)
预加载是指在用户滑动到数据之前就提前加载。这样当用户滑动到这些数据时,就不需要等待加载,从而提高了滑动的流畅性。
实现预加载的一种常见方法是在RecyclerView或ListView的适配器中,在onBindViewHolder()
方法里判断如果当前位置接近数据集的末尾,就提前请求更多的数据。
以下是一个简单的例子:
java
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
// 绑定数据
String item = data.get(position);
holder.textView.setText(item);
// 如果接近数据集末尾,提前请求更多数据
if (position >= data.size() - 1 - PRELOAD_THRESHOLD) {
requestMoreData();
}
}
在上面的代码中,PRELOAD_THRESHOLD
是一个常数,表示距离数据集末尾多少个位置开始进行预加载。requestMoreData()
方法则是请求更多数据的方法。
懒加载(Lazy Loading)
懒加载则是指只有当数据真正需要显示时才进行加载。这样可以避免一开始就加载大量不必要的数据,从而节省内存和网络资源。
实现懒加载的一种常见方法是在获取数据时进行分页。也就是说,每次只请求一小部分数据。当用户滑动到这些数据的末尾时,再请求下一部分数据。
以下是一个简单的例子:
java
private void requestMoreData() {
// 请求下一页数据
int nextPage = currentPage + 1;
api.getData(nextPage, PAGE_SIZE, new Callback<List<String>>() {
public void onSuccess(List<String> newData) {
// 更新数据集并通知适配器
data.addAll(newData);
notifyDataSetChanged();
currentPage = nextPage;
}
public void onError(Exception e) {
// 处理错误
}
});
}
在上面的代码中,api.getData()
方法是获取数据的API接口。它接收当前页码和每页大小作为参数,并返回对应页码的数据。
以上就是预加载和懒加载提升性能的原理和方法。在实际开发中,我们应该根据具体情况选择合适的加载策略。
布局性能优化 - 硬件加速
在Android中,硬件加速可以利用GPU的强大计算能力来提高渲染性能。当硬件加速开启时,视图的绘制操作会被转换为OpenGL ES命令,然后由GPU执行。这样可以大大提高2D图形的渲染效率。
开启硬件加速
从Android 3.0(API级别11)开始,应用程序可以选择开启硬件加速。你可以在应用程序级别或者单个活动、窗口、视图级别开启硬件加速。
要在应用程序级别开启硬件加速,你需要在AndroidManifest.xml文件中的<application>
标签里添加android:hardwareAccelerated="true"
属性。
要在单个活动、窗口或视图级别开启硬件加速,你可以调用setLayerType(View.LAYER_TYPE_HARDWARE, null)
方法。
硬件加速对渲染性能的影响
硬件加速对渲染性能影响的主要方面如下:
更快的渲染速度:GPU专门设计用于处理图形和图像,因此它在执行这些任务时比CPU更快。当你启用硬件加速时,系统会使用GPU来绘制视图,从而大大提高渲染速度。
更平滑的动画:由于GPU可以更快地绘制视图,因此当你使用硬件加速时,动画会运行得更平滑。这对于创建复杂的动画或实现流畅的滚动效果非常有用。
更高的能源效率:相比于CPU,GPU在执行图形和图像处理任务时通常更节能。因此,使用硬件加速不仅可以提高渲染性能,还可以降低设备的功耗。
然而,硬件加速并不是万能的。虽然它可以提高渲染性能,但也可能导致一些问题:
不是所有的2D绘图操作都支持硬件加速。例如,一些复杂的Canvas操作(如clipPath和drawPath)在硬件加速模式下可能无法正确工作。
硬件加速可能会增加内存消耗。因为每个视图都需要创建一个单独的OpenGL纹理,这可能会占用大量内存。
在低端设备上,启用硬件加速可能会导致性能下降。因为这些设备的GPU可能不够强大,无法有效地处理复杂的渲染任务。
注意事项
虽然硬件加速可以提高渲染性能,但并非所有的2D绘图操作都支持硬件加速。某些操作可能会因为开启硬件加速而变慢或者出错。
例如,以下绘图操作在开启硬件加速时不可用或有限制:
-
Canvas.clipPath()
-
Canvas.drawPath()
-
Paint.setShadowLayer()
-
Paint.setMaskFilter()
如果你的应用使用了这些操作,并且需要开启硬件加速,那么你需要进行额外的测试和调整。例如,你可以对不支持硬件加速的视图单独关闭硬件加速。
此外,虽然GPU比CPU更擅长处理图形计算,但它也有自己的限制。例如,过多的过度绘制(Overdraw)或复杂的着色器可能会导致GPU负载过重。因此,在使用硬件加速时,我们仍然需要注意优化我们的绘制操作。
如何测试硬件加速开关对性能的影响?
在Android中,我们可以通过一些工具和方法来测试和对比硬件加速开启和关闭时的性能差异。
使用Profile GPU Rendering
Profile GPU Rendering是Android开发者选项中的一个工具,它可以显示应用渲染每一帧所花费的时间。当你开启这个工具后,系统会在屏幕上画出一个条形图,显示出最近几十帧的渲染时间。
你可以通过这个工具来观察硬件加速开启和关闭时的性能差异。一般来说,如果条形图更低,那么渲染性能就更好。
使用Systrace
Systrace是Android SDK中的一个强大工具,它可以捕获和显示Android设备上发生的各种系统事件。你可以使用Systrace来深入了解应用在运行时的行为,并找出性能瓶颈。
通过分析Systrace的输出结果,你可以看到硬件加速开启和关闭时,GPU和CPU的使用情况,以及视图渲染的详细过程。
手动测试
除了上述工具外,你还可以通过手动测试来感受硬件加速的效果。例如,你可以试着滚动一个大型列表或者进行一些图形密集型操作,然后比较硬件加速开启和关闭时的流畅度。