android 技术梳理

Android 基本功-View 的工作流程(四)

2021-04-09  本文已影响0人  jkwen

这是 View 工作流程的最后一部分了,依然从 ViewRootImpl 说起,这次是 performDraw() 方法,方法里代码比较多,我就挑重点的 draw() 方法继续看了,不过再继续之前,有个概念要先做个了解,

Surface

Surface 对象用来将图像渲染到设备屏幕上。(这里仅做简单了解,深了我也不会)还有个相关的接口 SurfaceHolder,通过它可以实现对 Surface 的配置,类似于 Config 操作。

绘制流程

在 View 绘制的时候,Canvas 对象是不可或缺的,但 Canvas 对象的来源又是哪里?

//为了找 Canvas 对象来源,先看下 ViewRootImpl 的 draw
private boolean draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;
    if (!surface.isValid()) {
        return false;
    }
    //这个对象没太理解作用,虽然出现的频率比较高
    final Rect dirty = mDirty;
    boolean accessibilityFocusDirty = false;
    //这中间也省略了部分代码
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        //重点看这块判断
        //如果走 if,那将用 GPU 进行渲染(也就是硬件渲染),
        //简单看了下会涉及到 native 层,这里做大致了解
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
        } else {
            // else 这里分两种情况,一种是因为硬件渲染条件不足,所以在这里做了初始化后重新执行 View 的工作流程去了
            //另外一种就是用软件渲染,也就是 CPU 渲染
            if (mAttachInfo.mThreadedRenderer != null &&
                !mAttachInfo.mThreadedRenderer.isEnabled() &&
                mAttachInfo.mThreadedRenderer.isRequested() &&
                mSurface.isValid()) {
                mAttachInfo.mThreadedRenderer.initializeIfNeeded(
                    mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
                mFullRedrawNeeded = true;
                //这里开始重走 View 的工作流程
                scheduleTraversals();
                return false;
            }
            //这里去进行软件渲染
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                              scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
        }
    }
}
//ViewRootImpl
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    //省略了一些代码,挑重点的看
    //这个 Canvas 就是我们平常接触到的 Canvas 
    final Canvas canvas;
    try {
        //在这里通过 Surface 对象赋值,
        canvas = mSurface.lockCanvas(dirty);
        canvas.setDensity(mDensity);
    } 
    try {
        dirty.setEmpty();
        try {
            //mView 很熟悉了,就是 DecorView,调了它的 draw 方法,并传入 canvas 对象
            //这样看,这个 canvas 可能就是我们平常接触的那个
            mView.draw(canvas);
        }
    } finally {
        try {
            //这个方法和前面的 lockCanvas 是匹配的
            surface.unlockCanvasAndPost(canvas);
        }
    }
    return true;
}

官方文档对 lockCanvas()unlockCanvasAndPost() 的说明是为了资源不被同时占用,就要做一个锁的操作,这类似线程共享的资源。

往下再继续看 View 的 draw() 方法,就会发现入参的 canvas 对象正是平常我们接触到的那个 canvas,因此它的来源就是通过 Surface 对象创建的,可见绘制的背后是 Surface 对象以及一些 native 方法在主导。解决了 Canvas 对象来源问题,再继续看绘制过程的剩余部分,

//View 的 draw 方法
public void draw(Canvas canvas) {
    /*
     * 绘制需要有个合理的步骤
     * 1.画背景
     * 2.如果需要,就暂存画布准备进行渐变处理
     * 3.画 View 的内容
     * 4.画子 View
     * 5.如果需要,再画渐变的边界,并恢复画布
     * 6.画一些附加件,例如滚动条
     */
    //源码里的注释是这样的步骤,所以 draw 方法的整体思路估计就是这样了
    if (!dirtyOpaque) {
        //不透明的话就画背景
        drawBackground(canvas);
    }
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    //这是不考虑第二,第五步的情况
    if (!verticalEdges && !horizontalEdges) {
        //不透明的话继续画 View 的内容
        if (!dirtyOpaque) onDraw(canvas);
        //画子 View 们
        dispatchDraw(canvas);
        //画一些附加件,也就是画前景
        onDrawForeground(canvas);
        //还有第七步,应该是画光标
        drawDefaultFocusHighlight(canvas);
        return;
    }
    //下面代码是考虑第二,第五步的情况
    //简单看了下绘制渐变区域其实用到的,和我们平时绘制用到的差不多,也是涉及到区域,旋转,缩放,平移,画笔,矩阵等这些东西
    //除了多出的第二,第五步骤,其他和上面一样,代码就不放了。
}

Canvas

这里简单说下 Canvas 的 save 和 restore 相关的概念。画布有个图层的概念,如果不想后续的操作影响到当前图层的效果,就要保存图层,然后在新的图层上画,但是图层与图层之间是叠加的,所以看起来就像在一层上操作,这个概念可以参考下 Photoshop 里图层的概念。restore 的话就是恢复某图层状态(我在想这么一来原先的操作效果不就没了么,这块还有待实践验证)。除了不同图层,画布还有针对状态的一个保存恢复操作,这个也有待实践验证。建议大家可以再看下这个 Canvas类的最全面详解 - 自定义View应用系列 说不定更好理解,之前也看过这个作者写的关于事件分发的分析,也很到位。这里他也分析了 View 的工作流程,值得推荐。

绘制的通用步骤

再说回来,依次看下通用的绘制步骤,

//绘制背景
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    setBackgroundBounds();
    //这里还是尝试看看能否用硬件渲染,如果可以,下面的 draw 就不会执行了
    if (canvas.isHardwareAccelerated() && mAttachInfo != null && mAttachInfo.mThreadedRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //不考虑滚动值的话接下去就是 Drawable 去执行绘制的操作了
    //Drawable 是个抽象类,子类有 BitmapDrawable,ColorDrawable, NinePatchDrawable 等等
    //对应到的也就是我们常用的一些资源的绘制转换,例如 NinePatchDrawable 应该就是与 .9 图对应
    //这些子类都有各自的 draw 方法实现,最终才能展示到屏幕上
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

View 的 onDraw() 方法是空实现,去看看 DecorView 有没有重写该方法,

//DecorView 的 onDraw
public void onDraw(Canvas c) {
    super.onDraw(c);
    //这个东西简单看了下, BackgroundFallback 类的解释是专门给 DecorView 绘制 fallback background 的,我理解的是就像是我们弹起一个对话框,周围暗一点的透明层,那个估计就叫 fallback background.
    mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
                mStatusColorViewState.view, mNavigationColorViewState.view);
}

照这么看,DecorView 就画了个 fallback 背景,FrameLayout 也没做啥相关的。我想,容器类控件估计也不会做什么,更多的是具体控件会做一些相关绘制。

//步骤 dispatchDraw 的实现要追溯到 DecorView 的父类的父类 ViewGroup
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    //mChildren 明确说了是在这个 ViewGroup 下的子 View
    final View[] children = mChildren;
    //接下去的一段代码是和动画相关的,这里省略
    final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList();
    //对子View 的绘制分发肯定会涉及到遍历,所以我的思路是找循环
    for (int i = 0; i < childrenCount; i++) {
        //这里面这个循环好像也是和动画相关的,不做深入整理了,不过其核心部分
        //和普通绘制调的方法一样
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        //这里会去找到具体的子View,这个方法的入参有 3 个,前面 2 个,一个是列表,一个是数组
        //列表表示用硬件渲染,数组就是软件渲染了,
        //方法的逻辑其实就是如果列表存在就到列表里找,不然就到数组里找
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        //可见只有 Visible 的 view 才会绘制
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            //这个 drawChild 我是通过网页看源码找的,Android Studio 貌似找不到
            //里面还是有点工作的,大致就是是否能用硬件渲染,再进行画布的实际绘制操作还有保存恢复等
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    //后面又做了一些断后处理,这里省略
}

最后的绘制前景过程和绘制背景差不多,最终也会落到 Drawable 的绘制。

以上就是整个的绘制流程了,我们实际项目中可能接触的比较多的在于偏视觉展示类的自定义 View,或者动画,当然更多的是对一些绘制工具的掌握,对动画本质(数学函数等)的理解,还有绘制上对性能的考究。

上一篇下一篇

猜你喜欢

热点阅读