【Android 性能优化】—— UI篇
本文同时发布在CSDN上,欢迎查看
1. 前言
随着最近几年移动市场蓬勃发展,引来大批人员投入到Android、IOS的开发前线,与此同时全国各大培训机构每月都培养出成千上万名号称拥有2到3年工作经验的开发者。当然,这都已经不是什么秘密了,从目前来看,中国IT行业的主力军基本上都走过培训的道路。
但问题是,这号称23年工作经验者,使招聘单位错误的认为:23年开发经验和刚刚结束的培训经历,基本上划等号。这就导致了企业大幅度提高用人标准,造成了为何如今移动开发市场依旧火热,但是工作却不好找的现状。
最悲惨的例子恐怕就是前几年IOS如日中天,可时间就过了一年开发人员就出现了井喷的情况,大量IOS开发者找不到工作。
总的来说:工作机会的确是很多,但是企业把用人要求都大大提高了。如何在万千人群中脱颖而出,走上人生巅峰,迎娶白富美,没有亮点,是万万不行滴。。。
接下来我就一起学习Android UI优化吧
2. Android渲染机制分析
大家在开发应用的时候或多或少都遇到过可感知的界面卡顿现象,尤其是在布局层次嵌套太多,存在不必要的绘制,或者onDraw方法中执行了过多耗时操作、动画执行的次数过多等情况下,很容易造成此类情况。如今APP设计都要求界面美观、拥有更多的动画、图片等时尚元素从而打造良好的用户体验。但是大量复杂的渲染工作很可能造成Android系统压力过大,无法及时完成渲染工作。那么多久执行一次渲染,才能让界面流畅运行呢?
一图胜千言如上图所示,Android系统每隔16ms就会发送一个VSYNC信号(VSYNC:vertical synchronization 垂直同步,帧同步),触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的正常帧率:60fps。一旦这时候系统正在做大于16ms的耗时操作,系统就会无法响应VSYNC信号,执行渲染工作,导致发生丢帧现象。
丢帧啦。。。。大家在察觉到APP卡顿的时候,可以看看logcat控制台,会有
drop frames
类似的警告
本引用来自: Android UI性能优化实战 识别绘制中的性能问题
例如上图所示:如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,只能等待下一个VSYNC信号(第二个16ms)才能执行渲染工作。那么用户在32ms内看到的会是同一帧画面。(我就是感觉google给的图给错了,明明是 32ms,怎么给标了一个34ms,难道是有其他寓意我没有理解上去???)
用户容易在UI执行动画、ListView、RecyclerView滑动的时候感知到界面的卡顿与不流畅现象。所以开发者一定要注意在设计布局时不要嵌套太多层,多使用 include
方法引入布局。同时不要让动画执行次数太多,导致CPU或者GPU负载过重。
看到这里同学可能会疑问:为什么是16ms渲染一次,和60fps有什么关系呢?下面让我们看一下原理:
16ms意味着着1000/60hz,相当于60fps。
那么只要解释为什么是60fps,这个问题就迎刃而解:
这是因为人眼和大脑之间的写作无法感知超过60fps的画面更新,12fps大概类似手动快速翻动书籍的帧率,这是明显可以感知到不够顺滑的。
24fps使得人眼感知的是连续的线性运动,这其实是归功于运动模糊效果,24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。
但是低于30fps是
无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的
本引用来源:Google 发布 Android 性能优化典范 - 开源中国社区
3.1 界面卡顿的主要元凶—— 过度绘制(Overdraw)
3.1 什么是过度绘制?
过渡绘制是指屏幕上某个像素在同一帧的时间内绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制操作,这就会导致某些像素区域被绘制了多次,这就是很大程度上浪费了CPU和GPU资源。最最常见的过度绘制,就是设置了无用的背景颜色!!!
3.2 如何发现过度绘制?
对于Overdraw这个问题还是很容易发现的,我们可以通过以下步骤打开显示GPU过度绘制(Show GPU Overrdraw)选项
设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制
打开以后之后,你会发现屏幕上有各种颜色,此时你可以切换到需要检测的程序与界面,对于各个色块的含义,请看下图:
Overdraw的参考图蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,
蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。
绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。
淡红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。
深红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。
我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
3.3 解决问题的工具和方法
通过Hierarchy Viewer去检测渲染效率,去除不必要的嵌套
通过Show GPU Overdraw去检测Overdraw,最终可以通过移除不必要的背景。
4. UI优化实践
4.1 移除不必要的background
(由于公司项目还处于保密阶段,所以摘取了Android UI性能优化实战 识别绘制中的性能问题的部分示例)
下面看一个简单的展示ListView的例子:
- activity_main
?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:background="@android:color/white"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/narrow_space"
android:textSize="@dimen/large_text_size"
android:layout_marginBottom="@dimen/wide_space"
android:text="@string/header_text"/>
<ListView
android:id="@+id/id_listview_chats"
android:layout_width="match_parent"
android:background="@android:color/white"
android:layout_height="wrap_content"
android:divider="@android:color/transparent"
android:dividerHeight="@dimen/divider_height"/>
</LinearLayout>
- item的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingBottom="@dimen/chat_padding_bottom">
<ImageView
android:id="@+id/id_chat_icon"
android:layout_width="@dimen/avatar_dimen"
android:layout_height="@dimen/avatar_dimen"
android:src="@drawable/joanna"
android:layout_margin="@dimen/avatar_layout_margin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:orientation="vertical">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:textColor="#78A"
android:orientation="horizontal">
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:padding="@dimen/narrow_space"
android:text="@string/hello_world"
android:gravity="bottom"
android:id="@+id/id_chat_name" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textStyle="italic"
android:text="@string/hello_world"
android:padding="@dimen/narrow_space"
android:id="@+id/id_chat_date" />
</RelativeLayout>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/narrow_space"
android:background="@android:color/white"
android:text="@string/hello_world"
android:id="@+id/id_chat_msg" />
</LinearLayout>
</LinearLayout>
- Activity的代码
package com.zhy.performance_01_render;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
/**
* Created by zhy on 15/4/29.
*/
public class OverDrawActivity01 extends AppCompatActivity
{
private ListView mListView;
private LayoutInflater mInflater ;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_overdraw_01);
mInflater = LayoutInflater.from(this);
mListView = (ListView) findViewById(R.id.id_listview_chats);
mListView.setAdapter(new ArrayAdapter<Droid>(this, -1, Droid.generateDatas())
{
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder = null ;
if(convertView == null)
{
convertView = mInflater.inflate(R.layout.chat_item,parent,false);
holder = new ViewHolder();
holder.icon = (ImageView) convertView.findViewById(R.id.id_chat_icon);
holder.name = (TextView) convertView.findViewById(R.id.id_chat_name);
holder.date = (TextView) convertView.findViewById(R.id.id_chat_date);
holder.msg = (TextView) convertView.findViewById(R.id.id_chat_msg);
convertView.setTag(holder);
}else
{
holder = (ViewHolder) convertView.getTag();
}
Droid droid = getItem(position);
holder.icon.setBackgroundColor(0x44ff0000);
holder.icon.setImageResource(droid.imageId);
holder.date.setText(droid.date);
holder.msg.setText(droid.msg);
holder.name.setText(droid.name);
return convertView;
}
class ViewHolder
{
ImageView icon;
TextView name;
TextView date;
TextView msg;
}
});
}
}
实体的代码
package com.zhy.performance_01_render;
import java.util.ArrayList;
import java.util.List;
public class Droid
{
public String name;
public int imageId;
public String date;
public String msg;
public Droid(String msg, String date, int imageId, String name)
{
this.msg = msg;
this.date = date;
this.imageId = imageId;
this.name = name;
}
public static List<Droid> generateDatas()
{
List<Droid> datas = new ArrayList<Droid>();
datas.add(new Droid("Lorem ipsum dolor sit amet, orci nullam cra", "3分钟前", -1, "alex"));
datas.add(new Droid("Omnis aptent magnis suspendisse ipsum, semper egestas", "12分钟前", R.drawable.joanna, "john"));
datas.add(new Droid("eu nibh, rhoncus wisi posuere lacus, ad erat egestas", "17分钟前", -1, "7heaven"));
datas.add(new Droid("eu nibh, rhoncus wisi posuere lacus, ad erat egestas", "33分钟前", R.drawable.shailen, "Lseven"));
return datas;
}
}
现在的效果是:
注意,我们的需求是整体是Activity是个白色的背景。
开启Show GPU Overdraw以后:
对比上面的参照图,可以发现一个简单的ListView展示Item,竟然很多地方被过度绘制了4X 。 那么,其实主要原因是由于该布局文件中存在很多不必要的背景,仔细看上述的布局文件,那么开始移除吧。
- 不必要的Background 1
我们主布局的文件已经是background为white了,那么可以移除ListView的白色背景
- 不必要的Background 2
Item布局中的LinearLayout的android:background="@android:color/darker_gray"
- 不必要的Background 3
Item布局中的RelativeLayout的android:background="@android:color/white"
- 不必要的Background 4
Item布局中id为id_msg的TextView的android:background="@android:color/white"
这四个不必要的背景也比较好找,那么移除后的效果是:
对比之前的是不是好多了~~~接下来还存在一些不必要的背景,你还能找到吗?
- 不必要的Background 5
这个背景比较难发现,主要需要看Adapter的getView的代码,上述代码你会发现,首先为每个icon设置了背景色(主要是当没有icon图的时候去显示),然后又设置了一个头像。那么就造成了overdraw,有头像的完全没必要去绘制背景,所有修改代码:
Droid droid = getItem(position);
if(droid.imageId ==-1)
{
holder.icon.setBackgroundColor(0x4400ff00);
holder.icon.setImageResource(android.R.color.transparent);
}else
{
holder.icon.setImageResource(droid.imageId);
holder.icon.setBackgroundResource(android.R.color.transparent);
}
ok,还有最后一个,这个也是非常容易被忽略的。
- 不必要的Background 6
记得我们之前说,我们的这个Activity要求背景色是白色,我们的确在layout中去设置了背景色白色,那么这里注意下,我们的Activity的布局最终会添加在DecorView
中,这个View会中的背景是不是就没有必要了,所以我们希望调用mDecor.setWindowBackground(drawable)
;,那么可以在Activity
调用getWindow().setBackgroundDrawable(null);
setContentView(R.layout.activity_overdraw_01);
getWindow().setBackgroundDrawable(null);
ok,一个简单的listview显示item,我们一共找出了6个不必要的背景,现在再看最后的Show GPU Overdraw 与最初的比较。
ok,对比参照图,基本已经达到了最优的状态。
4.2 使用布局标签优化布局
4.2.1 <include>标签
相信大家使用的最多的布局标签就是 <include>
了。 <include>
的用途就是将布局中的公共部分提取出来以供其他Layout使用,从而实现布局的优化。这种布局的编写方式大大便利了开发,个人感觉这种思想和React Native中的面向组件编程思想有着异曲同工之妙,都是将特定功能抽取成为一个独立的组件,只要控制其中传入的参数就可以满局不同的需求。例如:我们在编辑Android界面的时候常常需要添加标题栏,如果在不使用<include>
的情况下,只能在每一个需要显示标题栏的xml文件中编写重复的代码,费时费力。但是只要我们将这个需要多次被使用的标题栏布局抽取成一个独立的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>
其中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"
android:id="@+id/my_foot_parent_id">
<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>
<include>
使用起来很简单,只需要指定一个layout属性为需要包含的布局文件即可。当然还可以根据需求指定 android:id
、 android:height
、android:width
属性来覆盖被引入根节点属性。
注意
在使用<include>
标签最常见的问题就是 findViewById
查找不到<include>
进来地控件的跟布局,而这个问题出现的前提就是在include的时候设置了id
。当设置id后,原有的foot.xml跟布局Id已经被替换为在 <include>
中指定的id
了,所以在 findViewById
查找原有id的时候就会报空指针异常。
View titleView = findViewById(R.id.my_foot_parent_id) ; // 此时id已经被覆盖 titleView 为空,找不到。此时空指针
View titleView = findViewById(R.id.my_foot_ly) ; //重写指定id即可
<include>
标签简单的说就是相当与将layout
指定的布局整体引入到main.xml中。所以我们就和操作直接在main.xml中的布局是一样的只不过有一个上面提到的更布局id
被覆盖的问题。
4.2.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>
其中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>
在代码中通过(ViewStub)findViewById(id)找到ViewStub,通过stub.inflate()展开ViewStub,然后得到子View,如下:
private View networkErrorView;
private void showNetError() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
}else{
ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
if(stub !=null){
networkErrorView = stub.inflate();
// 效果和上面是一样的
// stub.setVisibility(View.VISIBLE); // ViewStub被展开后的布局所替换
// networkErrorView = findViewById(R.id.network_error_layout); // 获取展开后的布局
}
}
}
private void showNormal() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.GONE);
}
}
在上面showNetError()中展开了ViewStub,同时我们对networkErrorView进行了保存,这样下次不用继续inflate。
注意这里我对ViewStub的实例进行了一个非空判断,这是因为ViewStub在XML中定义的id只在一开始有效,一旦ViewStub中指定的布局加载之后,这个id也就失败了,那么此时findViewById()得到的值也会是空
viewstub标签大部分属性同include标签类似。
注意:
根据需求我们有时候需要将View的可讲性设置为GONE,在inflate时,这个View以及他的字View还是会被解析的。所以使用<ViewStub>
就能避免解析其中的指定的布局文件。从而加快布局的解析时间,节省cpu内存等硬件资源。同时ViewStub所加载的布局是不可以使用<merge>
标签的
4.2.3 <merge>标签
在使用了include后可能会导致布局嵌套太多,导致视图节点太多,减慢了解析速度。
merge标签可用于两种典型情况:
- 布局顶接点是
FrameLayout
并且不需要设置background
或者padding
等属性,可使用merge
代替,因为Activity
内容视图的parent view
就是一个FrameLayout
,所以可以用merge
消除只能一个。 - 某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时,顶结点会自动被忽略,而其自己点全部合并到主布局中。
以【4.2.1 <include>标签 】中的代码示例为例,使用用hierarchy viewer查看main.xml布局如下图:
[引自:Android性能优化系列之布局优化](http://blog.csdn.net/u012124438/article/details/54564659)可以发现多了一层没必要的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>
运行后再次用hierarchy viewer查看main.xml布局如下图:
[引自:Android性能优化系列之布局优化](http://blog.csdn.net/u012124438/article/details/54564659)
这样就不会有多余的RelativeLayout节点了。
参考:
Android UI性能优化实战 识别绘制中的性能问题
Google 发布 Android 性能优化典范
Android性能优化系列之布局优化