Android 自定义view长时间运行延时、卡顿的问题解决方案

最近公司来了一个新的需求:将一些机车参数在app端用进度、动画的效果显示出来,于是在网上找了一大堆自定义View实现的进度条代码,最终搞出来了。界面中有四个控件是通过自定义view实现的,数据接收的频率是500ms一条数据,收到数据之后使用handler更新到UI线程直接显示数据。但是遇到一个问题,当绘制的时间比较长时,大约超过半个小时左右界面上的数值变化就会明显变慢,出现界面延迟伴有卡顿的情况。于是最近这几天一点点慢慢的优化,效果才有明显好转。接下来我来讲讲我具体优化了哪些方面的东西。

一、绘制onDraw()的优化

1、onDraw的频繁调用会使内存不断地增加,所以需要在onDraw方法中,在canvas.draw之前,先对paint和path进行重置,如果没有使用到Path,重置Paint就可以了。在重置之后记得给Paint重新设置值。

mPath.reset();
mPaint.reset();

之前的代码(运行4-6个小时,会有绘制慢的问题):

    private void initPaint() {
        //圆弧画笔
        arcPaint = new Paint();
        arcPaint.setAntiAlias(true);//抗锯齿
        arcPaint.setStyle(Paint.Style.STROKE);//风格
        arcPaint.setStrokeWidth(strokeWidthDial);//转盘中风宽度

        //指针画笔
        pointerPaint = new Paint();
        pointerPaint.setAntiAlias(true);//抗锯齿
        pointerPaint.setTextSize(textSizeDial);//文字大小
        pointerPaint.setTextAlign(Paint.Align.CENTER);//排成一行 居中
        fontMetrics = pointerPaint.getFontMetrics();//获得字体度量

        //标题画笔
        titlePaint = new Paint();
        titlePaint.setAntiAlias(true);//抗锯齿
        titlePaint.setTextAlign(Paint.Align.CENTER);//排成一行 居中
        titlePaint.setFakeBoldText(true);//设置黑体

        //指针条
        pointerPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawArc(canvas);//画弧
        drawPointerLine(canvas);//画指针线
        drawTitleDial(canvas);//画标题
        drawPointer(canvas);//画指针
    }

    //画弧
    private void drawArc(Canvas canvas) {
        //画布转换
        canvas.translate(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
        arcPaint.setColor(colorDialLower);//转盘下游颜色
        canvas.drawArc(mRect, 135, 54, false, arcPaint);
        arcPaint.setColor(colorDialMiddle);//转盘中游颜色
        canvas.drawArc(mRect, 189, 162, false, arcPaint);
        arcPaint.setColor(colorDialHigh);//转盘高游颜色
        canvas.drawArc(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) {
        super.onDraw(canvas);
        reset();
        drawArc(canvas);//画弧
        drawPointerLine(canvas);//画指针线
        drawTitleDial(canvas);//画标题
        drawPointer(canvas);//画指针
    }

    /**
     * 重置paint
     */
    private void reset() {
        arcPaint.reset();
        arcPaint.setAntiAlias(true);//抗锯齿
        arcPaint.setStyle(Paint.Style.STROKE);//风格
        arcPaint.setStrokeWidth(strokeWidthDial);//转盘中风宽度

        pointerPaint.reset();
        pointerPaint.setAntiAlias(true);//抗锯齿
        pointerPaint.setTextSize(textSizeDial);//文字大小
        pointerPaint.setTextAlign(Paint.Align.CENTER);//排成一行 居中
        fontMetrics = pointerPaint.getFontMetrics();//获得字体度量

        pointerPath.reset();
        titlePaint.reset();
        titlePaint.setAntiAlias(true);//抗锯齿
        titlePaint.setTextAlign(Paint.Align.CENTER);//排成一行 居中
        titlePaint.setFakeBoldText(true);//设置黑体
        titlePaint.setColor(titleDialColor);
        titlePaint.setTextSize(titleDialSize);
    }

    //画弧
    private void drawArc(Canvas canvas) {
        //画布转换
        canvas.translate(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
        arcPaint.setColor(colorDialLower);//转盘下游颜色
        canvas.drawArc(mRect, 135, 54, false, arcPaint);
        arcPaint.setColor(colorDialMiddle);//转盘中游颜色
        canvas.drawArc(mRect, 189, 162, false, arcPaint);
        arcPaint.setColor(colorDialHigh);//转盘高游颜色
        canvas.drawArc(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();
       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对象的重复创建。

三、invalidate()方法的执行时机

原先的代码:

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()方法的调用频率

四、invalidate和postInvalidate的区别

自定义view中实现view的更新有两种方式,一种是invalidate(),另一种是postInvalidate()。

invalidate()是在UI线程中使用,而postInvalidate()是在非UI线程中使用。

1、invalidate()

看一下invalidate()的源码:


    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} 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) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(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 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的区别" 出处:简单讲下postInvalidate与invalidate的区别-腾讯云开发者社区-腾讯云

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

窗台的花花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值