UI渲染的优化,我们应该知道UI怎么渲染的,在这个过程中会出现什么问题,我们才会去优化,针对这个问题我们怎么去优化。我们将从这几个部分进行探讨,并总结一些小的优化技巧。
1.UI的渲染过程
UI被渲染到屏幕上我们需要两个关键的组件:CPU和GPU,它们共同的工作,在屏幕上绘制图片,每一个组件都有自己固定的流程。
-
CPU:准备需要显示的数据。这个过程有测量,布局,记录,和执行.
-
GPU:把CPU计算好的DisplayList控件进行栅格化处理转换成纹理和3D图片显示到屏幕上
GPU使用一些指定的基础指令集,主要是多边形和纹理,也就是图片,CPU在屏幕上绘制图像前会向GPU输入这些指令,这一过程通常使用的API就是Android的OpenGL ES,这就是说,在屏幕上绘制UI对象时无论是按钮、路径或者复选框,都需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行格栅化。
- 完美的处理方法:Android系统每隔16ms发出VSYNC信号
2.绘制过程中的问题
我们应用程序的流畅性是很总要的。但是我们经常会出现应用的卡顿,那卡顿什么这么造成的。大部分是卡到了主进程。这就就要从两方面讨论。
- 1.外部(计算优化)
1.在View显示的过程中是是否有大量的内存分配和释放也就是内存抖动。 2.是否有一个很耗时的操作。
- 2.内部(渲染优化) view本身的卡顿。
1.CPU方面:最常见的性能问题是不必要的布局和失效布局,这些内容缺必须在视图层次结构中进行测量、清除并重新创建。这就会引起重建显示列表次数多,花费太多的时间做不必要的绘制。(布局优化) 2.GPU方面,最常见的问题是过度绘制,通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。(过度)
3.优化
3.1.计算优化
对于内存的抖动我们可以使用两个工具进行问题的定位
3.1.1使用android profiler来定位大致的情况
图中我们可以看到在一段时间内内存出现了大量的分配和释放
通过分析工具我们知道在这段时间内内存分配最多的函数。我们可以去看代码分析错误。
3.1.2可以使用TraceView来确定详细的问题所在
参数
名称 | 意义 |
---|---|
Name | 方法的详细信息,包括包名和参数信息 |
Incl Cpu Time | Cpu执行该方法该方法及其子方法所花费的时间 |
Incl Cpu Time % | Cpu执行该方法该方法及其子方法所花费占Cpu总执行时间的百分比 |
Excl Cpu Time | Cpu执行该方法所话费的时间 |
Excl Cpu Time % | Cpu执行该方法所话费的时间占Cpu总时间的百分比 |
Incl Real Time | 该方法及其子方法执行所话费的实际时间,从执行该方法到结束一共花了多少时间 |
Incl Real Time % | 上述时间占总的运行时间的百分比 |
Excl Real Time % | 该方法自身的实际允许时间 |
Excl Real Time | 上述时间占总的允许时间的百分比 |
Calls+Recur | 调用次数+递归次数,只在方法中显示,在子展开后的父类和子类方法这一栏被下面的数据代替 |
Calls/Total | 调用次数和总次数的占比 |
Cpu Time/Call | Cpu执行时间和调用次数的百分比,代表该函数消耗cpu的平均时间 |
Real Time/Call | 实际时间于调用次数的百分比,该表该函数平均执行时间 |
==主要使用方法是:在find中查找自己应用包名中,使用CPU时间长的,和是否有递归调用的。==
可以通过优化算法和更改数据结构的方法进行代码的优化。
比如:
StringBuffer 与 String
String是对象不是常量,在字符进行拼接时候,会新产生一个String
在需要大量有规律计算数据的情况下,特别是有乘除计算的时候,我们可以通过打表的方法进行优化。也就是在PC上计算好数据放在表中,使用的时候只要进行读取。
3.2.布局优化
3.2.1Hierarchy Viewer检测
三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。
- 1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。
- 2)黄色:渲染速度比较慢的50%。
- 3)红色:渲染速度非常慢。
==优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。==
3.2.2解决方法
- 减少嵌套
(1)在不响应层级深度的情况下,如果使用LinearLayout和RelativeLayout都可以实现相同的效果,那么建议使用Linearlayout。如果使用LinearLayout不能完成某些效果,那么使用RelativeLayout,而不是两层或更多层的LinearLayout,这样就可以尽量少的View层级,提高布局性能。
(2)尽量不使用LinearLayout的权重属性,因为它会让LinearLayout多进行一次measure。
(3)RelativeLayout的子View高度尽量和RelativeLayout相同,因为如果不同,会导致RelativeLayout在onMeasure()方法中做横向测量时,纵向的测量结果尚未完成,只好暂时使用自己的高度传入子View系统。而父View给子View传入的值没有变化的时候,是不会做无谓的测量的,所以RelativeLayout的子View如果高度和RelativeLayout相同可以进行一些布局上的优化,如果实在不行可以使用padding代替margin。
参考:LinearLayout和RelativeLayout性能区别
2.使用<include/>
<include>标签可以把一个布局中加载到另外一个布局,使代码结构清晰,又可统一修改使用。
注意:include标签仅支持layout_开头的属性(和id),且android:layout_width和android:layout_height属性必须存在,才能使用其它属性(如:android:layout_grivity、android:layout_align...)
3.<merge/>
<merge/>标签通常和<include/>标签一起使用,以减少View树的层级,从而达到优化Android布局的目的。
(1)比如说我们的RelativeLayout中只有一个TextView,这个TextView不需要指定任何针对父视图的布局属性,只添加到父视图上并显示,这种情况如果把<RelativeLayout/>标签改为<merge/>标签,那么层级结构里就少了RelativeLayout这层布局。
(2)还有就是比如LinearLayout里面使用include嵌入一个布局,而这个嵌入的布局的根节点也是LinearLayout,这样就多了一层没有用的嵌套,这个时候如果我们使用<merge/>作为嵌入布局的根标签就可以避免重复嵌套的问题。
当我们的布局是用的FrameLayout的时候,我们可以把它改成merge 可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)
4.ViewStub
ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。
当ViewStub被设置为可见或调用了ViewStub.inflate()的时候,ViewStub所指向的布局才会被Inflate和实例化
(ViewStub的布局属性会传给它所指向的布局,ViewStub对象会被置空,此时查看布局结构ViewStub是不存在的,取而代之的是被inflate的Layout。
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
……
<ViewStub
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/hint_fail_view"
android:inflatedId="@+id/hint_fail_view"
android:layout="@layout/fail_view"/>
</merge>
private View hintFailView;
if (网络异常) {
if (hintFailView == null) {
ViewStub viewStub = (ViewStub)this.findViewById(R.id.hint_fail_view);
hintFailView = viewStub.inflate(); //注意这里
TextView textView = (TextView) hintFailView.findViewById(R.id.tv);
textView.setText("网络异常");
}
hintFailView.setVisibility(View.VISIBLE);
}else{
//网络正常
if (hintFailView!= null) {
hintFailView.setVisibility(View.GONE);
}
//业务逻辑
}
3.3.绘制过度优化
原因:屏幕上的某个像素点在同一帧的时间内被绘制了多次
1.去掉不必要的背景图片
手机开发者选项里面找到工具:Debug GPU overdraw
由于我们布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景。
解决的办法:将主题添加的背景去掉getWindow().setBackgroundDrawable(null);
2.对应listview中的item的背景图片
if (判断是否有图片) {
Picasso.with(getContext()).load(android.R.color.transparent).into(chat_author_avatar);
chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
} else {
Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
chat_author_avatar);
chat_author_avatar.setBackgroundColor(Color.TRANSPARENT);
}
3.自定义控件绘制图片的时候如果有重叠可以对画板进行裁剪
canvas.save();
mPaint.setColor(Color.CYAN);
//先在屏幕的0,0处绘制一个与我们Bitmap宽高相等的蓝色矩形
canvas.drawRect(0, 0, width, height, mPaint);
canvas.restore();
canvas.save();
//裁剪画布,左上角为0,0 右下角为指定宽高的2倍和1.5倍
canvas.clipRect(0, 0, width*2, height*3/2);
//以width,height为左上角绘制我们的Bitmap,由于图片的下半部分在裁剪画布之外所以不显示
canvas.drawBitmap(mBmp, width, height, mPaint);
canvas.restore();
4.总结优化
- 用TextView同时显示图片和文字
android:drawableLeft android:drawableRight
- 使用CompoundDrables
//Sets the Drawables (if any) to appear to the left of, above, to the right of, and below the text.
//Use null if you do not want a Drawable there. The Drawables must already have had setBounds(Rect) called.
//可以在上、下、左、右设置图标,如果不想在某个地方显示,则设置为null
//但是Drawable必须已经setBounds(Rect)
//意思是你要添加的资源必须已经设置过初始位置、宽和高等信息
Drawable drawable= getResources().getDrawable(R.drawable.res);
drawable.setBounds( 0, 0, drawable.getMinimumWidth(),dra.getMinimumHeight());
tv.setCompoundDrawables(null, null, drawable, null);
- 使用LinearLayout自带的分割线
android:divider="@drawable/divider"
android:showDividers="middle"
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="1dp"
android:height="1dp"/>
<solid android:color="#e1e1e1"/>
</shape>
showDividers 是分隔线的显示位置,beginning、middle、end分别代表显示在开始、中间、末尾。
还有dividerPadding属性
- 使用Space控件
预留空白
<Space
android:layout_width="match_parent"
android:layout_height="15dp"/>
- 使用TextView的行间距
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="100dp"
android:background="@color/white"
android:layout_width="match_parent">
<ImageView
android:padding="25dp"
android:src="@drawable/kd_1"
android:layout_width="100dp"
android:layout_height="match_parent"/>
<TextView
android:textSize="14dp"
android:lineSpacingExtra="8dp"
android:gravity="center_vertical"
android:text="揽件方式:上门取件\n快递公司:顺丰快递\n预约时间:9月6日 立即取件\n快递费用:等待称重确定价格"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
可以看到我们仅仅利用android:lineSpacingExtra="8dp"这一行代码就省去了3个TextView。
lineSpacingExtra属性代表的是行间距,默认是0,是一个绝对高度值,同时还有lineSpacingMultiplier属性,它代表行间距倍数,默认为1.0f,是一个相对高度值。如果两者同时设置高度计算规则为mTextPaint.getFontMetricsInt(null) * 行间距倍数 + 行间距。
- 使用Spannable
String text = String.format("¥%s 门市价:¥%s", 18.6, 22);
int z = text.lastIndexOf("门");
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
//颜色
ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#afafaf")), z, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#32BBA9")), 0, z,Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
//字号
ssb.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this,10)), 0, text.length(),Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this,16)), 1, z,Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setText(ssb);