在Android开发中,UI布局可以说是每个App使用频率很高的,随着UI越来越多,布局的重复性、复杂度也会随之增长,这样使得UI布局的优化,显得至关重要,UI布局不慎,就会引起过度绘制,从而造成UI卡顿的情况,本篇博客,我就来总结一下UI布局优化的相关技巧。
学会使用布局标签优化布局
(1)<include>
标签
include标签常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利。例如我们在进行App开发时基本每个页面都会有标题栏,在不使用include的情况下你在每个界面都需要重新在xml里面写一个顶部标题栏,工作量无疑是巨大的,使用include标签,我们只需要把这个会被多次使用的顶部栏独立成一个xml文件,然后在需要使用的地方通过include标签引入即可。
下面以在一个布局main.xml中用include引入另一个布局foot.xml为例。main.mxl代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/simple_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="80dp" />
<include
android:id="@+id/my_foot_ly"
layout="@layout/foot" />
</RelativeLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
其中include引入的foot.xml为公用的页面底部,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_above="@+id/title_tv"/>
<TextView
android:id="@+id/title_tv"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_alignParentBottom="true"
android:text="@string/app_name" />
</RelativeLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
<include>
标签唯一需要的属性是layout属性,指定需要包含的布局文件。可以定义android:id和android:layout_*属性来覆盖被引入布局根节点的对应属性值。注意重新定义android:id后,子布局的顶结点i就变化了。
注意:
使用include最常见的问题就是findViewById查找不到目标控件,这个问题出现的前提是在include时设置了id,而在findViewById时却用了被include进来的布局的根元素id。例如上述例子中,include时设置了该布局的id为my_foot_ly
,而my_foot_ly.xml中的根视图的id为my_foot_parent_id。此时如果通过findViewById来找my_foot_parent_id这个控件,然后再查找my_foot_parent_id下的子控件则会抛出空指针。代码如下 :
View titleView = findViewById(R.id.my_foot_parent_id) ; // 此时 titleView 为空,找不到。此时空指针
TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; titleTextView.setText("new Title");
- 1
- 2
其正确的使用形式应该如下:
// 使用include时设置的id,即R.id.my_title_ly
View titleView = findViewById(R.id.my_foot_ly) ;
// 通过titleView找子控件 TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; titleTextView.setText("new Title");
- 1
- 2
- 3
或者更简单的直接查找它的子控件
TextView titleTextView = (TextView)findViewById(R.id.title_tv) ; titleTextView.setText("new Title");
- 1
- 2
那么使用findViewById(R.id.my_foot_parent_id)为什么会报空指针呢? 我们来分析它的源码看看吧。对于布局文件的解析,最终都会调用到LayoutInflater的inflate方法,该方法最终又会调用rInflate方法,我们看看这个方法。
inflate方法中关键代码
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
- 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
rInflate方法关键代码
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}
- 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
- 33
- 34
- 35
- 36
- 37
- 38
- 39
这个方法其实就是遍历xml中的所有元素,然后挨个进行解析。例如解析到一个标签,那么就根据用户设置的一些layout_width、layout_height、id等属性来构造一个TextView对象,然后添加到父控件(ViewGroup类型)中。标签也是一样的,我们看到遇到include标签时,会调用parseInclude函数,这就是对标签的解析,我们看看吧。
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
if (parent instanceof ViewGroup) {
// Apply a theme wrapper, if requested. This is sort of a weird
// edge case, since developers think the <include> overwrites
// values in the AttributeSet of the included View. So, if the
// included View has a theme attribute, we'll need to ignore it.
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
// If the layout is pointing to a theme attribute, we have to
// massage the value to get a resource identifier out of it.
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
if (value == null || value.length() <= 0) {
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// Attempt to resolve the "?attr/name" string to an identifier.
layout = context.getResources().getIdentifier(value.substring(1), null, null);
}
// The layout might be referencing a theme attribute.
if (mTempValue == null) {
mTempValue = new TypedValue();
}
if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
layout = mTempValue.resourceId;
}
if (layout == 0) {// include标签中没有设置layout属性,会抛出异常
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
throw new InflateException("You must specify a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
} else {
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
while ((type = childParser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty.
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(childParser.getPositionDescription() +
": No start tag found!");
}
// 1、解析include中的第一个元素
final String childName = childParser.getName();
// 如果第一个元素是merge标签,那么调用rInflate函数解析
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
// 2、我们例子中的情况会走到这一步,首先根据include的属性集创建被include进来的xml布局的根view // 这里的根view对应为my_foot_layout.xml中的RelativeLayout
rInflate(childParser, parent, context, childAttrs, false);
} else {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
ViewGroup.LayoutParams params = null;
try {
// 获3、取布局属性
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// Inflate all children.解析所有子控件
rInflateChildren(childParser, view, childAttrs, true);
// 5、将include中设置的id设置给根view,因此实际上my_foot_layout.xml中的RelativeLayout的id会变成include标签中的id,include不设置id,那么也可以通过relative的找到.
if (id != View.NO_ID) {
view.setId(id);
}
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
LayoutInflater.consumeChildElements(parser);
}
- 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
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
整个过程就是根据不同的标签解析不同的元素,首先会解析include元素,然后再解析被include进来的布局的root view元素。在我们的例子中对应的root view就是id为my_foot_parent_id的RelativeLayout,然后再解析root view下面的所有元素,这个过程是从上面注释的2~4的过程,然后是设置布局参数。我们注意看注释5处,这里就解释了为什么include标签和被引入的布局的根元素都设置了id的情况下,通过被引入的根元素的id来查找子控件会找不到的情况。我们看到,注释5处的会判断include标签的id如果不是View.NO_ID的话会把该id设置给被引入的布局根元素的id,即此时在我们的例子中被引入的id为my_foot_parent_id的根元素RelativeLayout的id被设置成了include标签中的id,即RelativeLayout的id被动态修改成了”my_foot_ly”。因此此时我们再通过“my_foot_parent_id”这个id来查找根元素就会找不到了!
所以结论就是: 如果include中设置了id,那么就通过include的id来查找被include布局根元素的View;如果include中没有设置Id, 而被include的布局的根元素设置了id,那么通过该根元素的id来查找该view即可。拿到根元素后查找其子控件都是一样的。
(2)<viewstub>
标签
viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存。
viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。
下面以在一个布局main.xml中加入网络错误时的提示页面network_error.xml为例。main.mxl代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
……
<ViewStub
android:id="@+id/network_error_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/network_error" />
</RelativeLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
其中network_error.xml为只有在网络错误时才需要显示的布局,默认不会被解析,示例代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/network_setting"
android:layout_width="@dimen/dp_160"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="@string/network_setting" />
<Button
android:id="@+id/network_refresh"
android:layout_width="@dimen/dp_160"
android:layout_height="wrap_content"
android:layout_below="@+id/network_setting"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/dp_10"
android:text="@string/network_refresh" />
</RelativeLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
在java中通过(ViewStub)findViewById(id)找到ViewStub,通过stub.inflate()展开ViewStub,然后得到子View,如下:
private View networkErrorView;
private void showNetError() {
// not repeated infalte
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
return;
}
ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
if(stub !=null){
networkErrorView = stub.inflate();
Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);
Button refresh = (Button)findViewById(R.id.network_refresh);
}
}
private void showNormal() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.GONE);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
在上面showNetError()中展开了ViewStub,同时我们对networkErrorView进行了保存,这样下次不用继续inflate。这就是后面第三部分提到的减少不必要的infalte。
注意这里我对ViewStub的实例进行了一个非空判断,这是因为ViewStub在XML中定义的id只在一开始有效,一旦ViewStub中指定的布局加载之后,这个id也就失败了,那么此时findViewById()得到的值也会是空
viewstub标签大部分属性同include标签类似。
上面展开ViewStub部分代码
ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
networkErrorView = stub.inflate();
- 1
- 2
- 3
也可以写成下面的形式
View viewStub = findViewById(R.id.network_error_layout);
viewStub.setVisibility(View.VISIBLE); // ViewStub被展开后的布局所替换
networkErrorView = findViewById(R.id.network_error_layout); // 获取展开后的布局
- 1
- 2
- 3
注意:
View 的可见性设置为 gone 后,在inflate 时,这个View 及其子View依然会被解析的。使用ViewStub就能避免解析其中指定的布局文件,从而节省布局文件的解析时间,及内存的占用。另外需要提醒大家一点,ViewStub所加载的布局是不可以使用<merge>
标签的
(3)<merge>
标签
在使用了include后可能导致布局嵌套过多,多余不必要的layout节点,从而导致解析变慢,不必要的节点和嵌套可通过hierarchy viewer(下面布局调优工具中有具体介绍)或设置->开发者选项->显示布局边界查看。
merge标签可用于两种典型情况:
a. 布局顶结点是FrameLayout且不需要设置background或padding等属性,可以用merge代替,因为Activity内容试图的parent view就是个FrameLayout,所以可以用merge消除只剩一个。
b. 某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。
以(1) 标签的示例为例,用hierarchy viewer查看main.xml布局如下图:
可以发现多了一层没必要的RelativeLayout,将foot.xml中RelativeLayout改为merge,如下:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_above="@+id/text"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40"
android:layout_alignParentBottom="true"
android:text="@string/app_name" />
</merge>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
运行后再次用hierarchy viewer查看main.xml布局如下图:
这样就不会有多余的RelativeLayout节点了。
去除不必要的嵌套和View节点
(1) 首次不需要使用的节点设置为GONE或使用viewstub
(2) 使用RelativeLayout代替LinearLayout
大约在Android4.0之前,新建工程的默认main.xml中顶节点是LinearLayout,而在之后已经改为RelativeLayout,因为RelativeLayout性能更优,且可以简单实现LinearLayout嵌套才能实现的布局。
4.0及以上Android版本可通过设置->开发者选项->显示布局边界打开页面布局显示,看看是否有不必要的节点和嵌套。4.0以下版本可通过hierarchy viewer查看。
减少不必要的infalte
(1)对于inflate的布局可以直接缓存,用全部变量代替局部变量,避免下次需再次inflate
如上面ViewStub示例中的
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
return;
}
- 1
- 2
- 3
- 4
布局调优工具
(1) hierarchy viewer
hierarchy viewer可以方便的查看Activity的布局,各个View的属性、measure、layout、draw的时间,如果耗时较多会用红色标记,否则显示绿色。
Hierarchy Viewer是随Android SDK发布的工具,位于Android SDK/tools/hierarchyviewer.bat (Windows操作系统,mac上显示的为hierarchyviewer),使用起来也是超级简单,通过此工具可以详细的理解当前界面的控件布局以及某个控件的属性(name、id、height等)。
1)连接设备真机或者模拟器
2)启动你要观察的应用。
3)打开Hierarchyviewer,点击hierarchyviewer文件即可。
4)双击最上面的,如下图的<Focused Window>
,这个是当前窗口,加载完毕后会显示当前界面层次结构。
5)观察层次结构图,这个图有点大,可以拖动。View Hierarchy窗口显示了Activity的所有View对象,选中某个View还可以查看View的具体信息,最好选择工具中的Show Extras选项。
View Hierarcy 同时能帮助你识别渲染性能比较低的部分。View节点中带有红色或黄色的点代表速度较慢的View对象。如单步运行应用程序那样,你可以这样来判断某个View 速度一直很慢,还是只在某个特定环境下速度才慢。
请注意,低性能并不表示一定有问题,特别像是ViewGroup对象,View的子节点越多,结构越复杂,性能越差。
View Hierarchy 窗口还可以帮助你找到性能问题。只要看每个View节点的性能指标(颜色点)就可以,你可以看到测量(布局或绘制)最慢的View对象是哪个,这样你就能快速确定,要优先察看哪个问题。
(2)Lint
先来段developer的官方引用:
Android Studio provides a code scanning tool called Lint that can help you to easily identify and correct problems with the structural quality of your code, without having to execute the app or write any test cases.
- 1
该图诠释了Lint工具是如何检测应用源代码的:
Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。
Android Lint使用
Lint简要来说,有以下的作用:
布局性能(以前是 layoutopt工具,可以解决无用布局、嵌套太多、布局太多)
未使用到资源
不一致的数组大小
国际化问题(硬编码)
图标的问题(重复的图标,错误的大小)
可用性问题(如不指定的文本字段的输入型)
manifest文件的错误
内存泄露 — 如:handle的不当使用 。
占内存的资源及时回收 — 如:cursor未关闭等
Analyze”菜单中选择“Inspect Code”,其中可以选择scope,即检测范围,也可以选择不同的检测配置,我们先进行默认的配置检测吧。检测需要一定的时间,结果会在EventLog显示:
可以看到,我们的项目有很多问题,比如我选择了Correctness中的Using dp instead of sp for text sizes属性,发现应用中有2处在textSize中误用了dp,其给出了类的具体位置和解决方案。可能你觉得这些无关大雅,那么可以查看Probable bugs项,在其中找到一项 String comparison using ‘==’,instead of ‘equals()’,可以看到SecurityBankCardListActivity类中的有一行代码:
this.mBankCard.getCardId() == mBankCard.getCardId()//cardId为String类型
- 1
在此就不一一列举。
可能你会觉得Lint分析的太过详细,我无法迅速找到问题,那么你可以点击,其分为四类,我们应只关注前2类。
AS的Lint配置
打开设置对话框,找到Editor,然后是Inspections,选择某一个Lint选项,修改严重等级,如图:
最后贴一下Lint检查的常见类型:
最后贴一下Lint检查的常见类型:
1.Correctness:Messeges
(1)字符串国际化不完全
(2)国际化的字符串,在默认位置(default locale),没有定义
2.Correctness
(1)Xml中view的id重名
(2)代码中使用的某些API高于Manifest中的Min SDK
(3)字符串国际化中,同一名字的的String-Array对应的item值不相同 (4)Activity没有注册到Manifest
(5)使用已经废弃的api
(6)避免使用px,使用dp
(7)添加不需要的权限
3.Performance
(1) 避免在绘制或者解析布局(draw/layout)时,分配对象。eg,Ondraw()中实例化Paint().
(2)Layout中无用的参数。
(3)可优化的布局:如一个线性布局(一个Imageview和一个TextView),可被TextView和一个Compound Drawable代替。
(4)可优化的代码:如SparseArray可代替一个Interger2Object的Hashmap
(5)优化layout,比如如果子view都是wrap_content,则设置android:baselineAligned为false,则When set to false, prevents the layout from aligning its children’s baselines.
(6)使用FloatMath代替Math,执行sin()和ceil(),以避免float的两次转换。
(7)Nested weight (内外均有weight)将拖累执行效果
(8)未被使用的资源
(9)Overdraw 即指定theme的activity会自己绘制背景,但是布局中会再一次设置背景
(10)View或view的父亲没有用
4.Security
(1)设置setJavascriptEnable将导致脚本攻击漏洞(XSS vulnerabilities)
5.Usability:Icons
(1) 图片尺寸在转换成不同dpi时,存在不能整除的问题,比如2*24px
(2)显示有些base 和browser的资源名不同,但图片内容完全相同。
6.Usability
(1)自定义view缺少默认的构造方法
7.Usability:Typography
(1)特殊字符需用编码代替,如“_”需要用“–”
8.Accessibility
(1)ImageView缺少src内容
检查Overdraw
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。这样就会浪费大量的CPU以及GPU资源。
手机原本为了保持视觉的流畅度,其屏幕刷新频率是60hz,即在1000/60=16.67ms内更新一帧。如果没有完成任务,就会发生掉帧的现象,也就是我们所说的卡顿。
debug GPU overdraw
在Android系统内部也有一个神器可以查看app的UI的过度绘制情况,在开发者选项中有个debug GPU overdraw(调试GPU过度绘制),打开之后有off(关闭),show overdraw areas(显示过度绘制区域),show areas for Deuteranomaly(为红绿症患者显示过度绘制区域)
我们选择show overdraw areas,发现整个手机界面的颜色变了,在打开过度绘制选项后,其中的蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
Profile GPU rendering
其次android系统还内置了Profile GPU rendering工具,这个工具也是在开发者选项中打开,它能够以柱状图的方式显示当前界面的渲染时间
蓝色代表测量绘制的时间,或者说它代表需要多长时间去创建和更新你的DisplayList.在Android中,一个视图在可以实际的进行渲染之前,它必须被转换成GPU所熟悉的格式,简单来说就是几条绘图命令,复杂点的可能是你的自定义的View嵌入了自定义的Path. 一旦完成,结果会作为一个DisplayList对象被系统送入缓存,蓝色就是记录了需要花费多长时间在屏幕上更新视图(说白了就是执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象).
橙色部分表示的是处理时间,或者说是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理.
红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List.这些API有效地将数据发送到GPU,最总在屏幕上显示出来.
下面我们通过一个小demo来实践一下
刚打开这个项目,我们就发现了在第一个有过度绘制问题,效果如下
存在问题
在按钮overdraw上面就有个红色的过度绘制区域
在文本框This is test的布局中也是红色过度绘制区域
解决方法
要解决这个问题,我们首先需要分析这是怎么引起的。分析到activity_main.xml的布局文件时,发现这里使用了多个嵌套的LinearLayout布局,而且每个LinearLayout都会使用一次android:background设置一次自己的背景颜色,他们造成了过度绘制。
仔细分析在其中一个嵌套ImageView的LinearLayout布局背景颜色与最外层的背景颜色是一样的,属于不需要的背景色,因此将这个LinearLayout中的android:background属性删除,这时发现文本框布局已经不再是红色了
咋看之下一切都很完美,但其实整个ui其实还含有一个隐含的绘制效果,那边是在activity中,使用setContentView(R.layout.activity_main)设置布局的时候,android会自动填充一个默认的背景,而在这个UI中,我们使用了填充整个app的背景,因此不需要默认背景,取消也很简单,只需要在activity中的onCreate方法中添加这么一句就行了
现在看最终优化效果
OVERDRAWVIEW页面的问题
在overdrawviewactivity中只有一个自定义的图案,而这个自定义的图案引起了过度绘制的问题
解决方法
首先这个也是填充了整个ui界面的绘制图片,因此我们也在activity中的onCreate方法中添加getWindow().setBackgroundDrawable(null);取消默认绘制。
继续研究,发现过度绘制问题是由于OverDrawView类中的ondraw方法中多次绘制了矩形导致的,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
通过分析得知,颜色为GRAY的矩形的高度其实不需要设置为整个屏幕的高度,它的高度只需要设置为它所显示范围的高度就可以了,因此可以设为height/4。
其他的矩形也是同样的道理,因此更改这里的代码为:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height/4, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height/3, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height/2, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
优化的界面
至此,布局优化的内容就到此结束了,有不足的地方,欢迎大家评论指出