View和ViewGroup的绘制过程简单总结

需要了解的

  • 先来张图说明一下它们的关系 
    view和viewgroup 
    你还要知道ViewGroup之间是可以嵌套的.

View的绘制流程

  • 不知道大家有没有这种疑惑, 为什么我们在写布局文件的时候, 一定要写layout_width和layout_height呢, 难道就没有默认值吗? 颜色, 背景, 等等其他的都有默认值, 为什么宽高就一定要我们手动写呢? 接下来就让我们一起来解答这个疑惑吧.
  • 绘制流程的源码就不贴出来了, 有兴趣的可以打开View的源码对照着来看, 印象会更深刻, 当然, 不看源码, 理解以下的实例代码, 也不会影响你对整个流程的理解.
  • 首先看一下View的绘制流程示例:
public class MyView extends View {

    private static final String TAG = "MyView";


    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * measure - > onMeasure ,view的源码中,measure会调用onMeasure
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  //测量, 表示这个view的大小, 在View的源码中, Measure是final修饰的, 我们只能重写onMeasure方法
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "measure");
    }

    /**
     * layout - > setFrame 和 onLayout
     */
    @Override
    public void layout(int l, int t, int r, int b) {  //布局,决定了摆放在父容器中的哪个位置
        // TODO Auto-generated method stub
        super.layout(l, t, r, b);
        Log.d(TAG, "layout");
    }

    /**
     * draw - > onDraw 
     */
    @Override
    public void draw(Canvas canvas) {  //绘制
        // TODO Auto-generated method stub
        super.draw(canvas);
        Log.d(TAG, "draw");
    }
}
  • 以上就是View绘制显示在屏幕上必定会调用的三个方法, 可能你还不太理解, 不过没关系, 先混个脸熟, 有个印象先, 下面我们一个个分析.

View的onMeasure

  • 来看一段Demo
public class MyView extends View {

    private static final String TAG = "MyView";

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * measure -> onMeasure
     * 
     * 1.父容器拿到孩子的申请的宽高layout_width, layout_height封装成宽高的期望widthMeasureSpec和heightMeasureSpec
     * 父容器Relativelayout(或者其他Linearlyout)
     * 调用MyView的 measure(int widthMeasureSpec, int heightMeasureSpec)传入对孩子宽高的期望
     * measure -> onMeasure(widthMeasureSpec, heightMeasureSpec)
     * 
     * @param widthMeasureSpec 父容器(RelativeLaoyut)对孩子MyView的宽度期望, 跟layout_width相关
     * @param heightMeasureSpec 父容器(RelativeLaoyut)对孩子MyView的高度期望, 跟layout_height相关
     * 这是我们为什么一定要指定layout_width和layout_height的原因.
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /**
        * int widthMeasureSpec
        * 32位二进制
        * 前两位 是测量模式 mode
        * public static final int UNSPECIFIED = 0 << MODE_SHIFT; 父容器对孩子没有任何的限制,孩子想多大多大
        * public static final int EXACTLY     = 1 << MODE_SHIFT; 父容器对孩子有确切的大小要求,大小就会后30位
        * public static final int AT_MOST     = 2 << MODE_SHIFT; 父容器对孩子的最大值有要求,大小就会后30位
        * 后30位表示大小
        */
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        Log.d(TAG, "onMeasure mode " + (mode>>30) + " " + size);

        //super方法默认使用父容器对我的期望的宽高
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //我们也可以不调用super直接调用setMeasuredDimension(50, 50)来指定宽高
    }

}
  • 注释里的setMeasuredDimension(int measuredWidth, int measuredHeight)是一个重要的方法, 它是整个测量结束的标志, 只有这个方法调用了, 我们才能调用getMeasuredWidth()或者getMeasuredHeight()方法获得测量宽高(注意, 不是实际宽高, 实际宽高要在布局完成之后)。
  • 自定义View如果要使用wrap_content属性的话,则需重写onMeasure方法。

View的layout

  1. layout方法里面调用setFrame(),给View的上下左右四个位置mLeft, mTop,mRight, mBottom赋值,完成布局工作.
  2. onLayout()是一个空的方法,说明具体的布局不应该由view来决定
  3. 我们的view并不需要关心layout方法, 布局的事应该交由父容器去处理, 让它决定它的孩子应该摆放在哪个地方.
  4. 在布局完成后, 我们才能调用getWidth()和getHeight获得实际的宽高.
  5. getWidth()和getMeasuredWidth()的区别: getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

View的draw

  • view的绘制分为6步:

    1. 对视图的背景进行绘制
    2. If necessary, save the canvas’ layers to prepare for fading (暂时忽略它)
    3. 对视图的内容进行绘制, 在onDraw(canvas)方法中完成
    4. 对当前视图的所有子视图进行绘制 ,调用dispatchDraw。
    5. If necessary, draw the fading edges and restore layers (暂时忽略它)
    6. 绘制装饰品(如滚动条)任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已.
  • 即我们关心四个步骤:

    1. 绘制背景
    2. 绘制内容
    3. 绘制孩子
    4. 绘制装饰
  • 绘制需要两个类, 画布(Canvas)和画笔(Paint), 通过以下Demo通过onDraw方法利用画笔在画布上绘制我们的图案吧.

public class MyView extends View {

    private Paint mPaint;
    private Bitmap mBitmap;
    private Path mPath;
    private RectF mOval;


    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        //设置去锯齿
        mPaint.setAntiAlias(true);
        //配置画笔,画空心圆
        mPaint.setStyle(Style.STROKE);
        //设置画笔宽度
        mPaint.setStrokeWidth(3);

        //设置画笔颜色
        mPaint.setColor(Color.BLUE);

        //画图片时需要设置图片
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.haha);

        //设置扇形的大小
        mOval = new RectF(5, 5, 195, 195);

        initPath();
    }

    private void initPath() {
        mPath = new Path();
        //确定三个点
        int x1 = 100, y1 = 5;
        int x2 = 195, y2 = 195;
        int x3 = 5, y3 = 195;
        //移动到第一个点
        mPath.moveTo(x1, y1);
        //链接第一个点和第二个点
        mPath.lineTo(x2, y2);
        //链接第二个点和第三个点
        mPath.lineTo(x3, y3);
        mPath.lineTo(x1, y1);

    }

    /**
     * 不要在onDraw方法里面创建新的对象,因为onDraw方法可能会频繁调用
     */
    @Override
    protected void onDraw(Canvas canvas) {
//      6. 裁剪
//      canvas.clipPath(mPath);


//      1. 画直线
//      int startX =5, startY = 100;
//      int stopX = 195, stopY = 100;
//      canvas.drawLine(startX, startY, stopX, stopY, mPaint);
//      2. 画圆
//      int cx = 100,  cy = 100;
//      int radius = 80;
//      canvas.drawCircle(cx, cy, radius, mPaint);
//      3. 画空心圆

//      4. 画图片
//      canvas.drawBitmap(mBitmap, 0, 0, mPaint);
//      5. 画三角形
//      canvas.drawPath(mPath, mPaint);

        //7.画扇形
        int startAngle = -90; //开始的角度
        int sweepAngle = 45;  //扫过的角度
        boolean useCenter = false;//是否画出扇形的两边
        canvas.drawArc(mOval, startAngle, sweepAngle, useCenter, mPaint);
    }
}

View的重新绘制

  • invalidate(); //触发View的重新绘制 onDraw
  • postInvalidate(); //请求在主线程重新绘制控件 onDraw

ViewGroup的绘制流程

  • ViewGroup继承View,绘制流程跟View是一致

ViewGroup的测量

  • 相同点:measure -> onMeasure
  • 不同点:ViewGroup需要在onMeasure去测量孩子
  • 自定义ViewGroup一定要重写onMeasure方法,如果不重写则子View获取不到宽和高。重写是在onMeasure方法中调用measureChildern()方法,遍历出所有子View并对其进行测量。

ViewGroup的布局

  • 相同点:layout (父容器调用) -》 onLayout
  • 不同点:ViewGroup需要实现onLayout方法去布局孩子,调用孩子的layout方法,指定孩子上下左右的位置
  • requestLayout();//请求重新布局 onLayout

ViewGroup的绘制

  • 相同点:draw -> onDraw
  • 不同点:ViewGroup一般不绘制自己,ViewGroup默认实现dispatchDraw去绘制孩子
版权声明:本文为博主原创文章,随意转载 https://blog.youkuaiyun.com/u011155781/article/details/52584044

<think>我们是在Android开发中,想要查看ViewViewGroup绘制时间。在Android系统中,我们可以通过多种方式来监控绘制的性能,包括绘制时间。以下是一些方法: 1. **使用Systrace工具**:Systrace是一个性能分析工具,它可以收集系统应用的执行时间信息,并生成报告。在报告中,我们可以看到每个View绘制时间。 2. **使用Layout Inspector**:在Android Studio中,Layout Inspector可以检查正在运行的应用程序的视图层次结构。它提供了一些性能数据,包括渲染时间。 3. **在代码中使用`ViewTreeObserver`**:我们可以通过注册`ViewTreeObserver.OnDrawListener`来监听绘制事件,并记录时间。注意:这个监听器在每次绘制视图时都会被调用,因此我们可以记录开始结束时间。 4. **使用`FrameMetrics`API(API 24及以上)**:从Android 7.0(API 24)开始,我们可以通过`FrameMetrics`来获取详细的帧性能数据,包括绘制时间。 5. **使用`Choreographer`**:通过`Choreographer`我们可以注册`FrameCallback`,从而在每一帧绘制时得到回调,我们可以在这里记录时间。 下面,我将重点介绍两种在代码中实现的方法(适用于Java): ### 方法一:使用`ViewTreeObserver.OnDrawListener`(适用于任何API) 我们可以在View被添加到窗口后,设置一个`OnDrawListener`,然后在绘制时记录时间戳: ```java // 在Activity或Fragment中 View view = findViewById(R.id.my_view); // 替换为你的View view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() { private long previousTime = 0; @Override public void onDraw() { long currentTime = System.currentTimeMillis(); if (previousTime != 0) { long elapsed = currentTime - previousTime; Log.d("DrawTime", "View绘制耗时: " + elapsed + "ms"); } previousTime = currentTime; } }); ``` 注意:这个方法会监听每一次的绘制,因此可能会被频繁调用。另外,它记录的是两次绘制之间的间隔,而不是单次绘制的确切时间。而且,它只能记录整个View树的绘制时间(对于单个View,实际上整个View树都会重绘)。 ### 方法二:使用`FrameMetrics`(API 24+) 从Android 7.0开始,我们可以通过`FrameMetrics`来监控应用渲染性能。我们可以给一个窗口添加`FrameMetrics`监听器: ```java // 在Activity的onCreate方法中 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() { @Override public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) { // 获取绘制时间(纳秒) long drawTime = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION); Log.d("FrameMetrics", "绘制时间: " + drawTime / 1000000 + "ms"); } }, new Handler()); } ``` `FrameMetrics`提供了一帧渲染过程中各个阶段的耗时,其中`DRAW_DURATION`表示执行绘制的时间(即`View.draw()`方法执行的时间)。注意,这个时间包括了所有视图的绘制时间,而不仅仅是某个特定的View。 ### 方法三:使用`Choreographer`(适用于监控帧时间) 如果我们想监控每一帧的绘制时间(包括界面上的所有View),我们可以使用`Choreographer`: ```java Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { private long lastFrameTime = 0; @Override public void doFrame(long frameTimeNanos) { if (lastFrameTime != 0) { long elapsed = (frameTimeNanos - lastFrameTime) / 1000000; // 转换为毫秒 Log.d("FrameTime", "两帧间隔时间: " + elapsed + "ms"); } lastFrameTime = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); } }); ``` 注意:这个方法会持续回调,所以需要的时候要记得移除回调。 ### 总结 - 如果你想监控单个View绘制时间,可以考虑使用`ViewTreeObserver.OnDrawListener`,但注意它提供的是两次绘制之间的间隔。 - 如果你想获得更详细的绘制性能数据,并且应用运行在API 24+,可以使用`FrameMetrics`。 - 如果你想监控每一帧的绘制时间,可以使用`Choreographer`。 另外,我们还可以使用性能分析工具(如Systrace、Layout Inspector、Profiler等)来查看绘制时间,这些工具可以提供更直观的图表更详细的数据。 ### 相关问题 1. 如何优化View绘制时间? 2. 在Android中,有哪些常见的导致绘制性能下降的问题? 3. 除了绘制时间,还有哪些指标可以衡量UI性能? [^1]: View/ViewGroup 绘制流程疑惑(一) [^2]: Android基础——ViewViewGroup </think>### Android中监控View/ViewGroup绘制时间的方法 在 Android 开发中,可以通过以下三种方式监控 View/ViewGroup绘制时间: --- #### **方法一:使用 Choreographer.FrameCallback(精确到纳秒)** ```java // 在 Activity 中 @Override protected void onResume() { super.onResume(); // 开始监听帧绘制 Choreographer.getInstance().postFrameCallback(frameCallback); } private final Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() { private long lastFrameTimeNanos = 0; @Override public void doFrame(long frameTimeNanos) { if (lastFrameTimeNanos != 0) { // 计算帧耗时(纳秒→毫秒) float frameTimeMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000f; Log.d("DrawTime", "帧耗时: " + frameTimeMs + "ms"); // 监控特定 View View targetView = findViewById(R.id.target_view); if (targetView != null) { monitorViewDrawTime(targetView); } } lastFrameTimeNanos = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); // 持续监听 } }; // 手动监控特定 View绘制耗时 private void monitorViewDrawTime(View view) { view.getViewTreeObserver().addOnDrawListener(() -> { long startTime = System.nanoTime(); view.post(() -> { long duration = (System.nanoTime() - startTime) / 1_000_000; Log.d("ViewDraw", view.getClass().getSimpleName() + " 绘制耗时: " + duration + "ms"); }); }); } ``` --- #### **方法二:使用 Debug 类(开启 GPU 渲染分析)** 1. **开启开发者选项** 在设备设置中启用: ``` 开发者选项 → GPU 渲染模式分析 → 在屏幕上显示条形图 ``` ![GPU渲染条形图示例](https://developer.android.com/images/tools/performance/inspect-gpu-rendering.png)[^1] 2. **解读条形图** - 蓝色:测量/布局时间 - 红色:执行时间 - 黄色:提交时间 - 绿色阈值:16ms(60FPS标准) --- #### **方法三:使用 FrameMetrics(API 24+)** ```java // 在 Activity 中(仅支持 Android 7.0+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { getWindow().addOnFrameMetricsAvailableListener((window, metrics, dropCount) -> { // 获取绘制阶段耗时 long layoutTime = metrics.getMetric(FrameMetrics.LAYOUT_DURATION); long drawTime = metrics.getMetric(FrameMetrics.DRAW_DURATION); Log.d("FrameMetrics", "布局耗时: " + layoutTime/1e6 + "ms"); Log.d("FrameMetrics", "绘制耗时: " + drawTime/1e6 + "ms"); }, new Handler()); } ``` --- ### 关键参数说明 | **监控方式** | **精度** | **适用场景** | **API要求** | |-----------------------|------------|---------------------------|-------------| | `Choreographer` | 纳秒级 | 代码级精确监控 | API 16+ | | GPU 渲染条形图 | 毫秒级 | 实时可视化 | 所有版本 | | `FrameMetrics` | 纳秒级 | 系统级详细分析 | API 24+ | --- ### 性能优化建议 1. **减少布局层级** 使用 ConstraintLayout 替代多层嵌套的 LinearLayout,层级每增加1层,绘制时间增加约2ms[^2]。 2. **避免过度绘制** 在开发者选项中开启 **调试GPU过度绘制**,现大面积红色区域需优化: ```xml <!-- 在主题中添加 --> <item name="android:windowBackground">@null</item> ``` 3. **使用 ViewStub 延迟加载** 对非立即显示的视图: ```xml <ViewStub android:id="@+id/stub_advanced" android:layout="@layout/advanced_view" android:inflatedId="@+id/advanced_root" /> ``` --- ### 数学原理:绘制时间阈值 为保证流畅的 60 FPS,每帧绘制时间需满足: $$ t_{\text{draw}} \leq \frac{1000\text{ms}}{60} \approx 16.67\text{ms} $$ 当超过该阈值时,系统会丢弃中间帧,导致卡顿现象[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值