最近公司来了一个新的需求:将一些机车参数在app端用进度、动画的效果显示出来,于是在网上找了一大堆自定义View实现的进度条代码,最终搞出来了。界面中有四个控件是通过自定义view实现的,数据接收的频率是500ms一条数据,收到数据之后使用handler更新到UI线程直接显示数据。但是遇到一个问题,当绘制的时间比较长时,大约超过半个小时左右界面上的数值变化就会明显变慢,出现界面延迟伴有卡顿的情况。于是最近这几天一点点慢慢的优化,效果才有明显好转。接下来我来讲讲我具体优化了哪些方面的东西。
一、绘制onDraw()的优化
1、onDraw的频繁调用会使内存不断地增加,所以需要在onDraw方法中,在之前,先对paint和path进行重置,如果没有使用到Path,重置Paint就可以了。在重置之后记得给Paint重新设置值。
();
();
之前的代码(运行4-6个小时,会有绘制慢的问题):
private void initPaint() {
//圆弧画笔
arcPaint = new Paint();
(true);//抗锯齿
();//风格
(strokeWidthDial);//转盘中风宽度
//指针画笔
pointerPaint = new Paint();
(true);//抗锯齿
(textSizeDial);//文字大小
();//排成一行 居中
fontMetrics = ();//获得字体度量
//标题画笔
titlePaint = new Paint();
(true);//抗锯齿
();//排成一行 居中
(true);//设置黑体
//指针条
pointerPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
(canvas);
drawArc(canvas);//画弧
drawPointerLine(canvas);//画指针线
drawTitleDial(canvas);//画标题
drawPointer(canvas);//画指针
}
//画弧
private void drawArc(Canvas canvas) {
//画布转换
(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
(colorDialLower);//转盘下游颜色
(mRect, 135, 54, false, arcPaint);
(colorDialMiddle);//转盘中游颜色
(mRect, 189, 162, false, arcPaint);
(colorDialHigh);//转盘高游颜色
(mRect, 351, 54, false, arcPaint);
}
优化后的代码(运行40个小时没有出现绘制慢的问题):
private void initPaint() {
//圆弧画笔
arcPaint = new Paint();
//指针画笔
pointerPaint = new Paint();
//标题画笔
titlePaint = new Paint();
//指针条
pointerPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
(canvas);
reset();
drawArc(canvas);//画弧
drawPointerLine(canvas);//画指针线
drawTitleDial(canvas);//画标题
drawPointer(canvas);//画指针
}
/**
* 重置paint
*/
private void reset() {
();
(true);//抗锯齿
();//风格
(strokeWidthDial);//转盘中风宽度
();
(true);//抗锯齿
(textSizeDial);//文字大小
();//排成一行 居中
fontMetrics = ();//获得字体度量
();
();
(true);//抗锯齿
();//排成一行 居中
(true);//设置黑体
(titleDialColor);
(titleDialSize);
}
//画弧
private void drawArc(Canvas canvas) {
//画布转换
(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
(colorDialLower);//转盘下游颜色
(mRect, 135, 54, false, arcPaint);
(colorDialMiddle);//转盘中游颜色
(mRect, 189, 162, false, arcPaint);
(colorDialHigh);//转盘高游颜色
(mRect, 351, 54, false, arcPaint);
}
优化自定义view最主要的是要对Paint或者Path进行重置。重置可以解决大部分自定义view控件长时间运行卡顿的问题。其次就是一些细节方面的优化了。
2、在每次数据更新之后,都会调用一次invalidate()或者postInvalidate()来更新UI,让onDraw()进行重新绘制。所以在onDraw()中不要创建新的局部对象。onDraw()方法执行的频率比较高,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存,而且还会导致系统更加频繁gc,降低了程序的执行效率。
原先的代码:
每次执行onDraw都会创建一个Matrix对象。
@Override
protected void onDraw(Canvas canvas) {
Matrix mDynamicMatrix = new Matrix();
(startAngele, () / 2, () / 2);
(mDynamicMatrix);
(mDynamicShader);
}
优化后的代码:
进行非空判断,Matrix只创建一次。
private Matrix mDynamicMatrix = null;
@Override
protected void onDraw(Canvas canvas) {
if (mDynamicMatrix == null) {
mDynamicMatrix = new Matrix();
}
(startAngele, () / 2, () / 2);
(mDynamicMatrix);
(mDynamicShader);
}
总言之,在onDraw()中尽量避免对象的重复创建。
二、动画的优化
原先的代码:
()会多次创建ValueAnimator对象。
private void startAnimator(float start, float end, long animTime) {
mAnimator = (start, end);
(animTime);
(new () {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) ();
mValue = mPercent * mMaxValue;
invalidate();
}
});
();
}
优化后的代码:
初始化的时候创建ValueAnimator,然后在使用的时候通过setFloatValues()赋值。
//初始化的时候创建ValueAnimator,注册监听
private void initAnim() {
mAnimator = new ValueAnimator();
(new () {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) ();
mValue = mPercent * mMaxValue;
invalidate();
}
});
}
private void startAnimator(float start, float end, long animTime) {
(start, end);
(animTime);
();
}
原理是避免对象的重复创建。
查看ValueAnimator.ofFloat(start, end)的源码:
/**
* Constructs and returns a ValueAnimator that animates between float values. A single
* value implies that that value is the one being animated to. However, this is not typically
* useful in a ValueAnimator object because there is no way for the object to determine the
* starting value for the animation (unlike ObjectAnimator, which can derive that value
* from the target object and property being animated). Therefore, there should typically
* be two or more values.
*
* @param values A set of values that the animation will animate between over time.
* @return A ValueAnimator object that is set up to animate between the given values.
*/
public static ValueAnimator ofFloat(float... values) {
ValueAnimator anim = new ValueAnimator();
(values);
return anim;
}
可以看出ofFloat()每执行一次,会创建一个ValueAnimator()对象。在初始化的时候创建一次ValueAnimator对象,更新数据直接使用ValueAnimator的setFloatValues()方法进行数据更新,可以避免ValueAnimator对象的重复创建。
三、invalidate()方法的执行时机
原先的代码:
onAnimationUpdate()监听1秒执行6次、4次、3次,次数不等,invalidate()方法也执行了多次,多次无必要的绘制造成大量资源的浪费。
//初始化的时候创建ValueAnimator,注册监听
private void initAnim() {
mAnimator = new ValueAnimator();
(new () {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) ();
mValue = mPercent * mMaxValue;
invalidate();
}
});
}
//启动动画
private void startAnimator(float start, float end, long animTime) {
(start, end);
(animTime);
();
}
优化之后:
增加一个动画开始、结束的监听,把invalidate()放到动画结束之后onAnimationEnd()方法中执行。前面提到数据接收的频率是500ms一条数据,动画的时间我这里设置的1,也就是1ms,可以忽略不计。1秒内动画执行两次,打印出来正好是执行了两次invalidate()。
//初始化的时候创建ValueAnimator,注册监听
private void initAnim() {
mAnimator = new ValueAnimator();
(new () {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) ();
mValue = mPercent * mMaxValue;
}
});
(new () {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
invalidate();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
invalidate()每执行一次,onDraw()方法就会跟着执行一次,重绘ui会占用较多的内存。为了避免资源的浪费,我们应该尽量减少invalidate()方法的调用频率。
四、invalidate和postInvalidate的区别
自定义view中实现view的更新有两种方式,一种是invalidate(),另一种是postInvalidate()。
invalidate()是在UI线程中使用,而postInvalidate()是在非UI线程中使用。
1、invalidate()
看一下invalidate()的源码:
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw()} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
从方法中的注释中看,我们知道invalidate方法会刷新整个View,并且当这个View的可见性为VISIBLE的时候,View的onDraw()方法将会被调用。另外注意的是这个方法只能在UI线程中去调用。
上面就能够基本知道invalidate方法是干什么的了。我们往下接着看源码:
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
* @hide
*/
@UnsupportedAppUsage
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
我们看到,invalidate()方法中是调用invalidate(true),参数true的意思是需要整体刷新,当View的内容和大小没有任何变化时我们可以传入false。
2、postInvalidate()
接下来看下postInvalidate()的实现:
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
* Use this to invalidate the View from a non-UI thread.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @see #invalidate()
* @see #postInvalidateDelayed(long)
*/
public void postInvalidate() {
postInvalidateDelayed(0);
}
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event
* loop. Waits for the specified amount of time.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
*
* @see #invalidate()
* @see #postInvalidate()
*/
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
(this, delayMilliseconds);
}
}
/**
* <p>Cause an invalidate of the specified area to happen on a subsequent cycle
* through the event loop. Waits for the specified amount of time.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
* @param left The left coordinate of the rectangle to invalidate.
* @param top The top coordinate of the rectangle to invalidate.
* @param right The right coordinate of the rectangle to invalidate.
* @param bottom The bottom coordinate of the rectangle to invalidate.
*
* @see #invalidate(int, int, int, int)
* @see #invalidate(Rect)
* @see #postInvalidate(int, int, int, int)
*/
public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
int right, int bottom) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
final info = ();
= this;
= left;
= top;
= right;
= bottom;
(info, delayMilliseconds);
}
}
从上面的方法注释中可以知道,postInvalidate是可以在非UI线程中去调用刷新UI的,那是如何做到的呢?从上面的方法调用栈中可以看出来,调用postInvalidate方法最后会调用View中的mHander发送一个MSG_INVALIDATE的消息。mHandler是ViewRootHandler的一个实例,从ViewRootHandler的handleMessage()方法中一探究竟(方法较长,只截取部分):
@Override
public void handleMessage(Message msg) {
switch () {
case MSG_INVALIDATE:
((View) ).invalidate();
break;
case MSG_INVALIDATE_RECT:
final info =
() ;
(, , , );
();
break;
case MSG_PROCESS_INPUT_EVENTS:
mProcessInputEventsScheduled = false;
doProcessInputEvents();
break;
在Handler中最后还是会调用View的invalidate()方法去刷新,只不过postInvalidate()方法是通过Handler将刷新事件通知发到Handler的handlerMessage中去执行invalidate的。
"invalidate和postInvalidate的区别" 出处:简单讲下postInvalidate与invalidate的区别-腾讯云开发者社区-腾讯云