Android 自定义View 性能分析学习 <1>

学习网上面以为厉害的博主的文章,自己跟着学习的节奏,也一步一步的进行学着分析,加深印象,更重要的是自己能够熟悉里面的细节部分,细节决定一些性能.

首先写了一个测试demo程序如下:

<1>  新建Android工程,工程树如下:

Android 自定义View 性能分析学习 <1>_第1张图片

<2> : 程序如下:

DurianMainActivity.java

package com.durian.durianperformanceview;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import com.durian.view.DurianView;

public class DurianMainActivity extends Activity {

    private Button mButton;
    private DurianView mDurianView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.durian);
        
        mDurianView=(DurianView)findViewById(R.id.durianview);
        
        
        mButton=(Button)findViewById(R.id.button);
        mButton.setOnClickListener(new OnClickListener(){

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                mDurianView.updateDraw();
            }
            
        });
    }

}

DurianView.java

/**  
 * @Title: DurianView.java
 * @Package com.durian.view
 * @Description: TODO
 * @author zhibao.liu from durian organization
 * @date 2015-12-28 下午06:25:33
 * @version V1.0  
 */
package com.durian.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;

import com.durian.durianperformanceview.R;
import com.durian.utils.DurianUtils;

/**
 * @ClassName: DurianView
 * @Description: TODO
 * @author zhibao.liu Freelancer
 * @email [email protected]
 * @date 2015-12-28 下午06:25:33
 * 
 */
public class DurianView extends View {

    private final static String TAG = "DurianView";

    private TypedArray mTypeArray;

    private String mDurianText;
    private float mDurianTextWidth;
    private float mDurianTextSize;
    private int mDurianLeft;
    private int mDurianRight;

    private int mDurianPosition;

    private Paint mPaint;

    private Bitmap mBitmap;

    private Shader mRadialGradient = null;

    private Paint mRadialPaint;

    public DurianView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO Auto-generated constructor stub

    }

    public DurianView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub

//        initView(context,attrs);

    }

    public DurianView(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
    }
    
    private void initView(Context context,AttributeSet attrs){
        
        mTypeArray = context.obtainStyledAttributes(attrs,
                R.styleable.durian_view);

        mDurianText = mTypeArray.getString(R.styleable.durian_view_durian_text);
        mDurianTextSize = mTypeArray.getInteger(
                R.styleable.durian_view_durian_textsize, 16);
        mDurianLeft = mTypeArray.getInteger(
                R.styleable.durian_view_durian_leftpadding, 0);
        mDurianRight = mTypeArray.getInteger(
                R.styleable.durian_view_durian_rightpadding, 0);
        mDurianPosition = mTypeArray.getInteger(
                R.styleable.durian_view_durian_labelpos, 0);

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(DurianUtils.px2sp(context, mDurianTextSize));

        mDurianTextWidth = mPaint.measureText(mDurianText);

        Drawable map = mTypeArray
                .getDrawable(R.styleable.durian_view_durian_src);
        mBitmap = DurianUtils.drawableToBitmap(map);

        // 创建RadialGradient对象
        // 第一个,第二个参数表示渐变圆中心坐标
        // 第三个参数表示半径
        // 第四个,第五个,第六个与线性渲染相同
        mRadialGradient = new RadialGradient(250, 250, 100, new int[] {
                Color.GREEN, Color.RED, Color.BLUE, Color.WHITE }, null,
                Shader.TileMode.REPEAT);

        mRadialPaint = new Paint();
        mRadialPaint.setShader(mRadialGradient);
        
    }

    public void updateDraw() {
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

//        canvas.clipRect(new Rect(0, 0, (int) (50 + mDurianTextWidth), 50 + 5));

//        canvas.drawText(mDurianText, 50, 50, mPaint);

//        canvas.drawLine(10.0f, 50.0f, 100.0f, 50.0f, mPaint);

//        canvas.drawBitmap(mBitmap, 0, 55, mPaint);

        // canvas.drawCircle(250f, 250f, 100f, mPaint);
//        canvas.drawCircle(250f, 250f, 100f, mRadialPaint);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // TODO Auto-generated method stub
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int minw = getPaddingLeft() + getPaddingRight()
                + getSuggestedMinimumWidth();
        int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

        int minh = MeasureSpec.getSize(w) - (int) mDurianTextWidth
                + getPaddingBottom() + getPaddingTop();
        int h = resolveSizeAndState(MeasureSpec.getSize(w)
                - (int) mDurianTextWidth, heightMeasureSpec, 0);

        setMeasuredDimension(w, h);

    }

}

下面是一个工具类:

DurianUtils.java

/**  
 * @Title: DurianUtils.java
 * @Package com.durian.utils
 * @Description: TODO
 * @author zhibao.liu from durian organization
 * @date 2015-12-29 上午10:07:53
 * @version V1.0  
 */
package com.durian.utils;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;

import com.durian.durianperformanceview.R;

/**
 * @ClassName: DurianUtils
 * @Description: TODO
 * @author zhibao.liu Freelancer
 * @email [email protected]
 * @date 2015-12-29 上午10:07:53
 * 
 */
public class DurianUtils {

    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }

    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    public static Bitmap drawableToBitmap(Drawable drawable) {

        Bitmap bitmap = Bitmap.createBitmap(

        drawable.getIntrinsicWidth(),

        drawable.getIntrinsicHeight(),

        drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888

        : Bitmap.Config.RGB_565);

        Canvas canvas = new Canvas(bitmap);

        // canvas.setBitmap(bitmap);

        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight());

        drawable.draw(canvas);

        return bitmap;

    }

    public static Bitmap getResBitmap(Context context) {

        Resources res = context.getResources();

        Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.debug);

        return bmp;
    }

}

布局文件:durian.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:durian_view="http://schemas.android.com/apk/res/com.durian.durianperformanceview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    
    <com.durian.view.DurianView
		android:layout_width="fill_parent"
		android:layout_height="60dp"
		android:id="@+id/durianview"
		durian_view:durian_text="zhibao.liu"
		durian_view:durian_textsize="64"
		durian_view:durian_src="@drawable/debug"
		/>
    
    
    <Button 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        android:id="@+id/button"/>

</LinearLayout>

属性文件:attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="durian_view">
        <attr name="durian_text" format="string"/>
        <attr name="durian_textsize" format="integer"/>
        <attr name="durian_leftpadding" format="integer"/>
        <attr name="durian_rightpadding" format="integer"/>
        <attr name="durian_src" format="reference"/>
        <attr name="durian_labelpos" format="enum">
        	<enum name="left" value="0"></enum>
        	<enum name="right" value="1"></enum>
        </attr>
    </declare-styleable>
</resources>

<3> : 下面是个人通过修改上面,得出的一些结论,仅仅共参考用.

a> : 先修改durian.java中DurianView这个自定View的布局大小:

<com.durian.view.DurianView
		android:layout_width="fill_parent"
		android:layout_height="60dp"
		android:id="@+id/durianview"
		durian_view:durian_text="zhibao.liu"
		durian_view:durian_textsize="64"
		durian_view:durian_src="@drawable/debug"
		/>

启动程序后,打开DDMS窗口:

选中测试的工程,如上.这里面暂时看耗时, 只需要学会下面的参考.

运行APP有一个按钮,点击APP界面按钮之前,先点击上面右边带红点的图标,如下:

然后点击APP界面的按钮,然后在点击上面右边的图标,图标由灰色转变带红色点了,同时:

Android 自定义View 性能分析学习 <1>_第2张图片

上面的eclipse会自动弹出来的.主要看第一行,即:

第一次看上面一堆的东西,什么Incl Cpu Time等参数可能不知道说什么,我参看了下面的 一片文章非常的好:

http://www.oschina.net/news/56500/traceview-android

上面的文章非常的好,对于一般的分析,主要参看下面的就好了:


在耗时上面,看Incl Real Time 参数,Calls+RecurC...参数,Cpu Time/Call参数,还有Real Time/Call参数

Incl Real Time 参数 : 程序运行需要的事件;

Calls+RecruC...参数 : 这段程序/或者程序中某个具体的方法被运行的次数;

Cpu Time/Call参数 : 这段程序/程序中某个具体的方法被运行所需要的时间,特指CPU中;

Real Time/Call参数: 这段程序/程序中某个具体的方法被运行所需要的时间,一般具体程序运行的时间,一般这个时间大于Cpu Time/Call参数的时间.

所以上面总耗时185.765ms

例如:177

这个updateDraw方法是我们点击APP里面的按钮被调用刷新View的:

从面的参数可以看出,点击一次按钮后,执行一次,所需时间0.305s,CPU时间和实际执行程序时间差不多相等.

但是上面发送更新后,执行onDraw方法需要的时间是多少呢,这里可以看一下294:

虽然上面的程序还没有添加任何东西,是直接放空的,耗时如下:

从上面参数可以看出onDraw被执行一次,所需耗时0.122.

可以这样想一下,如果这个onDraw方法好了大量的时间,APP在显示的时候是什么样的呢?

下面我们修改自定义View的暂用尺寸大小:

<com.durian.view.DurianView
		android:layout_width="fill_parent"
		android:layout_height="600dp"
		android:id="@+id/durianview"
		durian_view:durian_text="zhibao.liu"
		durian_view:durian_textsize="64"
		durian_view:durian_src="@drawable/debug"
		/>

高度从60dp调整到600dp ,执行程序,操作刷新一次:

总耗时变成219.674,注意前面只有100多.

但是看onDraw方法是看不出的,因为太小了,所以下面的在onDraw操作量不大的时候,数字上无法做出判断.


结论一 : 从上面看,布局空间大小的设置对APP刷新所需的时间是有影响的.


b> : 自定View初始化变量:

public DurianView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub

        initView(context,attrs);

    }

将程序中的initView注释去掉,同样运行APP,会有以下结论.


结论二 : 构造中初始化过多,对View的创建过程会有影响,但是也有一个好处,对后面的刷新,因为不需要创建对象,所以不会增加耗时和内存,很多时候开发会在onDraw方法中再次创建一些对象,会增加内存泄露,但是由于每次都创建,耗时不会增加.但是内存泄露也是需要避免的.


c> : onDraw方法中修改如下:

    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

//        canvas.clipRect(new Rect(0, 0, (int) (50 + mDurianTextWidth), 50 + 5));

        canvas.drawText(mDurianText, 50, 50, mPaint);

        canvas.drawLine(10.0f, 50.0f, 100.0f, 50.0f, mPaint);

//        canvas.drawBitmap(mBitmap, 0, 55, mPaint);

        // canvas.drawCircle(250f, 250f, 100f, mPaint);
//        canvas.drawCircle(250f, 250f, 100f, mRadialPaint);

    }

运行总耗时: 245.313

去掉下面注释:

canvas.clipRect(new Rect(0, 0, (int) (50 + mDurianTextWidth), 50 + 5));


重新运行:

运行总耗时: 164.254

从下面可以得出结论:


结论三 : 在画布上面再划出一个区域来绘制,刷新会更快.


d> : 画一个圆,用不同的东西对圆进行填充.

一种是填充红色:

canvas.drawCircle(250f, 250f, 100f, mPaint);

一种是渲染式填充:

canvas.drawCircle(250f, 250f, 100f, mRadialPaint);

运行后同样测试,发现第二种渲染式填充更省时,直接用单色填充更耗时,有点心理影响.


结论四 : 这个可能图像在重画时,不同颜色填充耗时有些差别---这个不一定啊,但是APP颜色对耗电不同,这是确定的


|-----------------------------------------------------------------------------------------------------------------------------------------------------|

下面是一些基本的总结,仅仅共参考意见:

View的一个标准 : 为了避免UI显得卡顿,你必须确保动画能够保持在60fps以上.

这个东西以前的公司设定的下线就是60fps,但现在看来原来差不多是一个标准了,用来衡量View显示是否存在卡顿.


<1> : View的尺寸大小,以及自身绘制区域的大小需要严格控制.

精确定位View的大小,可以在View中自己实现:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width ends up being, ask for a height that would let the pie
   // get as big as it can
   int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

最终通过setMeasuredDimension(w,h);设置View的大小.


<2> : 当View需要一些动画效果时,能够使用Android ***Animator产生动画效果的,尽量不适用onDraw重新绘制,因为***Animator调用时不会执行onDraw重新绘制,推荐ValueAnimator,ObjectAnimator,更复杂一点的ViewPropertyAnimator组合属性动画.

如果实在迫不得已,一般相应touch事件,Scroller设置产生的移动动画,但是从View事件传递的手稿中,我们知道start这个动画效果,是需要强制执行重绘的,所以这种相对上面的效果可能低一些.


<3> : View嵌套布局水平布局和纵向布局:

水平方向布局:

Android 自定义View 性能分析学习 <1>_第3张图片

纵向方向布局:

Android 自定义View 性能分析学习 <1>_第4张图片


由于经常刷新,会导致View经常重绘,如果是纵向方向布局,就会导致一个View的子View也会出现重绘的现象,从而导致产生一连锁反应,本想只绘制一个View,结果所有的子View也重新绘制了.

所以一般情况,水平(平级)布局要优于纵向布局,有必要都需要使用ViewGroup容器去搞定View,不要在View中反复嵌套子View.

同时有必要提一下,反复的include包含布局其实也很恶心,没有必要的话,建议不要经常用include,天下没有白送的午餐,不要图一时爽.

另外一个非常耗时的操作是请求layout。任何时候执行requestLayout(),会使得Android UI系统去遍历整个View的层级来计算出每一个view的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持View的层级是扁平化的,这样对提高效率很有帮助。

如果你有一个复杂的UI,你应该考虑写一个自定义的ViewGroup来执行他的layout操作。与内置的view不同,自定义的view可以使得程序仅仅测量这一部分,这避免了遍历整个view的层级结构来计算大小

<4> : 在一个View在屏幕翻转的时候,尺寸调整须知,屏幕在翻转时,调用:

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // TODO Auto-generated method stub
        super.onSizeChanged(w, h, oldw, oldh);
    }
onSizeChanged(),当你的view第一次被赋予一个大小时,或者你的view大小被更改时会被执行。在onSizeChanged方法里面计算位置,间距等其他与你的view大小值


<5> : 在不得不调用重绘通知时,即调用invalidate方法时,也需要注意,这个方法3种,参见的是invalidate()不带任何参数的,按照上面的,画布大小对刷新也是有影响的,那么专家(不是我)建议使用带四个参数的invalidate(l, t, r, b)方法,对指定的区域进行刷新就好了,如果使用不带参数的invalidate(),那默认就是整个区域都进行刷新---这是强制性的.


<6> : 硬件加速 :

硬件加速虽然有很多好处,如在翻转,平移等动画方面不错,但是并不是所有的都很好,甚至效果不理想.

在View中,在你需要的时候添加:

if(!isInEditMode()){
            setLayerType(View.LAYER_TYPE_HARDWARE, null);
        }

但不需要的时候:

setLayerType(View.LAYER_TYPE_NONE, null);

添加硬件加速的设置,View将在执行onDraw之后,自动把缓存保存为一张图片,并通过GPU来重绘不同的角度,但是由于硬件加速是需要付出代价的,它会消耗video memory资源的,所以仅仅在用户触发scrolling的时候使用LAYER_TYPE_HARDWARE,在其他时候,使用LAYER_TYPE_NONE,这话是专家的.

硬件加速可能遭遇的问题,来自官网翻译整理:

开启硬件加速之后的异常反应:

      1.某些UI元素没有显示:可能是没有调用invalidate

      2.某些UI元素没有更新:可能是没有调用invalidate

      3.绘制不正确:可能使用了不支持硬件加速的操作, 需要关闭硬件加速或者绕过该操作

      4.抛出异常:可能使用了不支持硬件加速的操作, 需要关闭硬件加速或者绕过该操作




上面的工程程序:

http://pan.baidu.com/s/1bq8D0U











你可能感兴趣的:(Android 自定义View 性能分析学习 <1>)