最近公司来了一个新的需求:将一些机车参数在app端用进度、动画的效果显示出来,于是在网上找了一大堆自定义View实现的进度条代码,最终搞出来了。界面中有四个控件是通过自定义view实现的,数据接收的频率是500ms一条数据,收到数据之后使用handler更新到UI线程直接显示数据。但是遇到一个问题,当绘制的时间比较长时,大约超过半个小时左右界面上的数值变化就会明显变慢,出现界面延迟伴有卡顿的情况。于是最近这几天一点点慢慢的优化,效果才有明显好转。接下来我来讲讲我具体优化了哪些方面的东西。
在每次数据更新之后,都会调用一次invalidate()或者postInvalidate()来更新UI,让onDraw()进行重新绘制。所以在onDraw()中不要创建新的局部对象。onDraw()方法执行的频率比较高,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存,而且还会导致系统更加频繁gc,降低了程序的执行效率。
原先的代码:
每次执行onDraw都会创建一个Matrix对象。
@Override
protected void onDraw(Canvas canvas) {
Matrix mDynamicMatrix = new Matrix();
mDynamicMatrix.setRotate(startAngele, canvas.getWidth() / 2, canvas.getHeight() / 2);
mDynamicShader.setLocalMatrix(mDynamicMatrix);
mPaint.setShader(mDynamicShader);
}
优化后的代码:
进行非空判断,Matrix只创建一次。
private Matrix mDynamicMatrix = null;
@Override
protected void onDraw(Canvas canvas) {
if (mDynamicMatrix == null) {
mDynamicMatrix = new Matrix();
}
mDynamicMatrix.setRotate(startAngele, canvas.getWidth() / 2, canvas.getHeight() / 2);
mDynamicShader.setLocalMatrix(mDynamicMatrix);
mPaint.setShader(mDynamicShader);
}
总言之,在onDraw()中尽量避免对象的重复创建。
原先的代码:
ValueAnimator.ofFloat()会多次创建ValueAnimator对象。
private void startAnimator(float start, float end, long animTime) {
mAnimator = ValueAnimator.ofFloat(start, end);
mAnimator.setDuration(animTime);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
invalidate();
}
});
mAnimator.start();
}
优化后的代码:
初始化的时候创建ValueAnimator,然后在使用的时候通过setFloatValues()赋值。
//初始化的时候创建ValueAnimator,注册监听
private void initAnim() {
mAnimator = new ValueAnimator();
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
invalidate();
}
});
}
private void startAnimator(float start, float end, long animTime) {
mAnimator.setFloatValues(start, end);
mAnimator.setDuration(animTime);
mAnimator.start();
}
原理是避免对象的重复创建。
查看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();
anim.setFloatValues(values);
return anim;
}
可以看出ofFloat()每执行一次,会创建一个ValueAnimator()对象。在初始化的时候创建一次ValueAnimator对象,更新数据直接使用ValueAnimator的setFloatValues()方法进行数据更新,可以避免ValueAnimator对象的重复创建。
原先的代码:
onAnimationUpdate()监听1秒执行6次、4次、3次,次数不等,invalidate()方法也执行了多次,多次无必要的绘制造成大量资源的浪费。
//初始化的时候创建ValueAnimator,注册监听
private void initAnim() {
mAnimator = new ValueAnimator();
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
invalidate();
}
});
}
//启动动画
private void startAnimator(float start, float end, long animTime) {
mAnimator.setFloatValues(start, end);
mAnimator.setDuration(animTime);
mAnimator.start();
}
优化之后:
增加一个动画开始、结束的监听,把invalidate()放到动画结束之后onAnimationEnd()方法中执行。前面提到数据接收的频率是500ms一条数据,动画的时间我这里设置的1,也就是1ms,可以忽略不计。1秒内动画执行两次,打印出来正好是执行了两次invalidate()。
//初始化的时候创建ValueAnimator,注册监听
private void initAnim() {
mAnimator = new ValueAnimator();
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
}
});
mAnimator.addListener(new Animator.AnimatorListener() {
@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()方法的调用频率。
自定义view中实现view的更新有两种方式,一种是invalidate(),另一种是postInvalidate()。
invalidate()是在UI线程中使用,而postInvalidate()是在非UI线程中使用。
看一下invalidate()的源码:
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
*
* 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。
接下来看下postInvalidate()的实现:
/**
* Cause an invalidate to happen on a subsequent cycle through the event loop.
* Use this to invalidate the View from a non-UI thread.
*
* This method can be invoked from outside of the UI thread
* only when this View is attached to a window.
*
* @see #invalidate()
* @see #postInvalidateDelayed(long)
*/
public void postInvalidate() {
postInvalidateDelayed(0);
}
/**
* Cause an invalidate to happen on a subsequent cycle through the event
* loop. Waits for the specified amount of time.
*
* This method can be invoked from outside of the UI thread
* only when this View is attached to a window.
*
* @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) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
/**
* Cause an invalidate of the specified area to happen on a subsequent cycle
* through the event loop. Waits for the specified amount of time.
*
* This method can be invoked from outside of the UI thread
* only when this View is attached to a window.
*
* @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 AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
info.target = this;
info.left = left;
info.top = top;
info.right = right;
info.bottom = bottom;
attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds);
}
}
从上面的方法注释中可以知道,postInvalidate是可以在非UI线程中去调用刷新UI的,那是如何做到的呢?从上面的方法调用栈中可以看出来,调用postInvalidate方法最后会调用View中的mHander发送一个MSG_INVALIDATE的消息。mHandler是ViewRootHandler的一个实例,从ViewRootHandler的handleMessage()方法中一探究竟(方法较长,只截取部分):
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
case MSG_INVALIDATE_RECT:
final View.AttachInfo.InvalidateInfo info =
(View.AttachInfo.InvalidateInfo) msg.obj;
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
break;
case MSG_PROCESS_INPUT_EVENTS:
mProcessInputEventsScheduled = false;
doProcessInputEvents();
break;
在Handler中最后还是会调用View的invalidate()方法去刷新,只不过postInvalidate()方法是通过Handler将刷新事件通知发到Handler的handlerMessage中去执行invalidate的。
"invalidate和postInvalidate的区别" 出处:https://cloud.tencent.com/developer/article/1355393