Android组件View绘制流程原理分析

Android组件View绘制流程原理分析

android视图构成

这里写图片描述

如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是DecorView,这个DecorView就是存放布局文件的,里面有TitleActionBar和我们setContentView传入进去的layout布局文件

  • Window类时一个抽象类,提供绘制窗口的API
  • PhoneWindow是继承Window的一个具体的类,该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View
  • DecorView继承FrameLayout,里面id=content的就是我们传入的布局视图

依据面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置

setContentView流程

setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,重点概括为:

  1. 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。

  2. 依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。

  3. 将Activity的布局文件添加至id为content的FrameLayout内。

  4. 当setContentView设置显示OK以后会回调Activity的onContentChanged方法。Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。

android的View绘制

view绘制主要包括三个方面:

  • measure 测量组件本身的大小
  • layout 确定组件在视图中的位置
  • draw 根据位置和大小,将组件画出来

视图绘制的起点在ViewRootImpl类的performTraversals()方法,该方法完成的工作主要是: 根据之前的状态,判定是否重新计算测试视图大小(measure)、是佛重新放置视图位置(layout)和是否重新重绘视图(draw) ,部分源码如下:

private void performTraversals() {
        ......
        //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
        //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }

measure计算视图大小

几乎所有的组件都是继承View类的,而关于view的测量工作,日常开发用得多的方法就是measure和onMeasure两个方法,measure不可重写,当我们自定义时主要重写onMeasure方法即可,在方法内部我们必须完成组件的mMeasuredWidth和mMeasuredHeight实际尺寸测量,而这个尺寸是需要父视图和子视图共同决定的

measure流程从根视图measure遍历整个view树结构,如下:

这里写图片描述

还要注意视图尺寸MeasureSpec是一个组合尺寸,它是一个32位bit值,高两位是尺寸模式specMode,低30位是尺寸大小值,我们可以利用提供的原声库方法很方便的进行尺寸组合和拆解:
specMode有三种: MeasureSpec.EXACTLY表示确定大小, MeasureSpec.AT_MOST表示最大大小, MeasureSpec.UNSPECIFIED不确定

int measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);    //合成
int specMode = MeasureSpec.getMode(measureSpec);								   //拆解
int specSize = MeasureSpec.getSize(measureSpec);

而在视图测量meause中,父组件传给子组件的一般都是一个组合尺寸,我们可以拿出具体尺寸然后根据其他条件产生一个新的尺寸值,将这个值用setMeasuredDimension设置mMeasuredWidth和mMeasuredHeight具体尺寸,完成测量;

measure原理总结
  • MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定; 
  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  • View的布局大小由父View和子View共同决定。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

layout视图位置确定

layout的流程主要也是遍历整个view树结构,调用view.layout(int l, int t, int r, int b)确定好view的具体坐标位置,流程图如下

这里写图片描述

当我们自定义一个组件时,通常时重写onLayout方法,里面实现好自己的逻辑,最后在调用layout方法完成视图位置确定,如果自定义组件时一个ViewGroup的话,还需要我们去遍历每一个child确定尺寸

layout原理总结
  • 整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。

  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

draw绘制

完成measure和Layout后,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程
先来看下View树的递归draw流程图,如下:
这里写图片描述

draw原理总结

可以看见,绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

  • View的绘制是借助onDraw方法传入的Canvas类来进行的。

  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

View的刷新机制

当我们调用view.invalidate或者setText()时,其背后view是如何绘制的,是立即绘制,还是有一个声明样的执行流程

这里就要引出另一个东西FPS,Android FPS为60,在这个帧率上我们不会查看到屏幕有撕裂画面出现,这个60如何而来?
由硬件触发每个16ms产生一次VSYNC信号,从而触发view绘制任务(添加了绘制任务的前提下才会触发),这样一秒就绘制60次

view结构树

如本文最顶上view树图,其实最上层还有一个ViewRootImp对象,它才是整个view树的最顶层;当我们调用view.invalidate()要求重绘时,他会依次调用:
在这里插入图片描述
当view执行invalidate时,会调用它的parent.invalidateChild,层层调用调用,直到顶层ViewRootImp执行invalidateChildInParent绘制,这也好理解,因为紧靠自身view无法测量、布局和绘制自己,需要整颗view到自己的引用树

最终顶层的invalidateChildInParent方法会执行到scheduleTraversals遍历函数:

void scheduleTraversals() {
     1. 防止重复调用
     if (!mTraversalScheduled) {
        mTraversalScheduled = true;
    2. 发送同步屏障,保证优先处理异步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    3. 提交mTraversalRunnable绘制任务到Choreographer回调中去,这个mTraversalRunnable
    任务会执行doTraversal方法
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ......
    }
}

以下这个函数就是mTraversalRunnable任务
void doTraversal() {
    if (mTraversalScheduled) {
         1. mTraversalScheduled 置为 false
        mTraversalScheduled = false;
  	2. 移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

	  3. 开始布局,测量,绘制流程,这步就会执行上面讲到的那些layout、measure和draw了
        performTraversals();
        ......
    }

但是现在这个任务mTraversalRunnable是如何提交到Choreographer任务中,什么时候又执行呢?
我们进入Choreographer编舞者这个类进去看看,首先看看任务添加时机:

添加绘制任务

public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}

public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    ......
    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

 传入的参数依次是 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null0
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    ......
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
         1. 将 mTraversalRunnable 塞入队列
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {  立即执行
             2. 由于 delayMillis 是 0,所以会执行到这里
            scheduleFrameLocked(now);
        } else {  延迟执行
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

在第一个步骤,将我们的绘制任务添加到了mCallbackQueues任务列表中去,然后执行schudleFrameLocked函数,在这个函数中会注册一次监听(nativeScheduleVsync)底层VYSNC信号,这个VYSNC信号是每个16ms由硬件触发,触发后就会执行onVsync方法,那这个onVsync是干什么的呢?
看看就知道了:

 vsync 信号监听回调
    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        ......
        long now = System.nanoTime();
         timestampNanos 是 vsync 回调的时间,不能比 now 大
        if (timestampNanos > now) {
            Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                    + " ms in the future!  Check that graphics HAL is generating vsync "
                    + "timestamps using the correct timebase.");
            timestampNanos = now;
        }
        ......
        mTimestampNanos = timestampNanos;
        mFrame = frame;
         这里传入的是 this,会回调本身的 run() 方法
        Message msg = Message.obtain(mHandler, this);
         这是一个异步消息,保证优先执行
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        doFrame(mTimestampNanos, mFrame);
    }

vsync信号来临后,会发送一个handler异步消息,由于传入时间小于当前时间,而且messageQueue中还有一个同步栏,所以会立即执行这个消息,这个消息是一个run的消息,也就是会执行doFrame方法

相信你也才到了,doFrame中肯定会执行我们之间添加的重绘任务,看看吧!

frameTimeNanos传入的是VYSNC的产生信号时间
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            return; // no work to do
        }
        ......

        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
   计算超时时间
   frameTimeNanos 是 vsync 信号回调的时间,startNanos 是当前时间戳
   相减得到主线程的耗时时间
        final long jitterNanos = startNanos - frameTimeNanos;
   mFrameIntervalNanos 是一帧的时间,16ms
  if (jitterNanos >= mFrameIntervalNanos) {
  			做除法,得到16ms的倍数,也就是有多少帧
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
    		掉帧超过 30 帧,打印 log
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            frameTimeNanos = startNanos - lastFrameOffset;
        }
        ......
    }

    try {
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

   doCallBacks() 开始执行回调
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
		我们的回调就在CALLBACK_TRAVERSAL这个type中
        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
    }
    ......
}

还要注意的是,在doCallbacks方法中,执行任务后,会将任务清除掉,也就是说你要再次执行重绘任务的话,在下一个vsync信号来临之前要再次添加任务,使用Choreographer.postCallback将新的任务添加到回调中,也就是你需要在使用一次invalidate方法

来个图片小结:
在这里插入图片描述


view提供的API控制视图的方法

invalidate和postInvalidate方法源码分析

请求重新绘制视图,调用draw

  • invalidate在主线程调用
  • postInvalidate是在非主线程调用

View的requestLayout方法

requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

本文参考于:这里

view计算与渲染

view渲染

View绘制会用到CPU和GPU,CPU主要是完成上面的measure/layout测量每个view,最后使用onDraw绘制到canvas上去,而这个canvas来源于phoneWindow的Surface,所以最终绘制的view视图都会转移到surface里面去;

我们知道,我们视图最终都会转递给surfaceflinger去,而我们的window里面的surface就是由surfaceflinger创建的,并且对应一个Graphic Buffer,这块内存主要就是传递图像数据的;
插入一个 displaylist 概念,displaylist缓存了view视图的绘制指令,包含它的位置、大小、透明度等绘制信息,对应view的drawCanvas绘制,每个view视图对应一个displayList,并且父view包含子view的displaylist,多以最终提交到GPU的Render thread时,只需要绘制根节点的displaylist即可;

所以CPU需要计算、测量view树中每个view的大小、位置以及draw到canvas,构建绘制指令序列到displayList,最终提交到GPU的Render Thread渲染线程,完成渲染;

Android有软件和硬件渲染:

  • 软件渲染,CPU会将所有的displayList绘制到一个bitmap中,提交到GPU作为纹理,贴图上去
  • 硬件渲染,就是上述讲到的过程
硬件渲染的优劣

当第二帧页面渲染时,如果视图view树没有变化或是简单变化(透明度、大小),就无需重新执行onDraw方法,直接复用上次提交displayList,简单修改其属性后渲染即可;
若是view树有更新,其对应的displayList也会发生更新并且通过Graphic Buffer提交给GPU,但是硬件渲染有个好处是只绘制有更新的脏区,软件绘制则会全部都会更新
android 4.0以后默认会开启硬件加速!也有缺点,更耗电和内存。

对不可见的UI组件进行绘制更新会导致Overdraw。例如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。

但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视

每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。

在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示比较复杂,需要先经过CPU换算成纹理,然后交给GPU进行渲染,返回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则存在一个更加复杂的操作流程。

为了能够使得App流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值