Android View绘制原理 - RenderNodeDrawable

上一篇文章介绍了SkiaOpenGLPipeline.draw主流程,其中renderFrame是一个主要的流程之一,本文将继续去分析这个renderFrame方法。这个方法是定义在SkiaOpenGLPipeline的父类SkiaPipeline上
frameworks/base/libs/hwui/pipeline/skia/SkiaPipeline.cpp

java 复制代码
void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip,
                               const std::vector<sp<RenderNode>>& nodes, bool opaque,
                               const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
                               const SkMatrix& preTransform) {
    ...
    SkCanvas* canvas = tryCapture(surface.get(), nodes[0].get(), layers);
    ...
    renderLayersImpl(layers, opaque);
    ...
    renderFrameImpl(clip, nodes, opaque, contentDrawBounds, canvas, preTransform);
    ...
}

它的流程还是非常清晰的,先在SkSurface上创建一个canvas,然后先渲染Layer,后渲染nodes。 先看一下几个参数的来源

  • layers。这个是在前面是遍历RenderNode 树形结构的时候,如果发现一些节点的layertype == RENDER_LAYER, 则为这些RenderNode生成一个Layer,每个Layer都有一个SkSurface.然后将这个layer加入到这个layers。如果没有手动设置过layertype的话,layers是empty的。
  • nodes, 是CanvasContext的mRenderNodes,正常情况下只有一个元素,类型是RootRenderNode。多元素的情况目前我还没有发现。
  • surface 前面构建的基于SkGpuDevice的SkSurface对象

有了这些背景知识,我们看看上面方法内部的几个方法

1 tryCapture

java 复制代码
SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface, RenderNode* root,
    const LayerUpdateQueue& dirtyLayers) {
    if (CC_LIKELY(!Properties::skpCaptureEnabled)) {
        return surface->getCanvas(); // Bail out early when capture is not turned on.
    }
    ...
}

capture是一种debug的场景,正常情况下,直接就进入这个分支
external/skia/src/image/SkSurface.cpp

java 复制代码
SkCanvas* SkSurface::getCanvas() {
    return asSB(this)->getCachedCanvas();
}

继续调用asSB方法,希望这个名字不要给河蟹哈

**

java 复制代码
static SkSurface_Base* asSB(SkSurface* surface) {
    return static_cast<SkSurface_Base*>(surface);
}

external/skia/src/image/SkSurface_Base.h

java 复制代码
SkCanvas* SkSurface_Base::getCachedCanvas() {
    if (nullptr == fCachedCanvas) {
        fCachedCanvas = std::unique_ptr<SkCanvas>(this->onNewCanvas());
        if (fCachedCanvas) {
            fCachedCanvas->setSurfaceBase(this);
        }
    }
    return fCachedCanvas.get();
}

这里继续调用onNewCanvas,因此这个SkSuface实际类型是SkSurface_Gpu, 因为我们看看它的onNewCanvas方法
external/skia/src/image/SkSurface_Gpu.cpp

java 复制代码
SkCanvas* SkSurface_Gpu::onNewCanvas() { return new SkCanvas(fDevice); }

这里直接以fDevice为参数创建一个新的SkCanvas。这在之前分析SkCanvas时说过,创建一个依赖SkGpuDevice的SkCanvas来会绘制才能真正的去做像素渲染。这里的fDevice就真是一个SkGpuDevice。因此这里生成的SkCanvas会真正的去调用GPU渲染像素。

所以tryCapture方法就是准备一个真正的可以渲染像素的Canvas

2 renderLayersImpl

这个方法先去处理layers。因此我们先来分析一下layer是如何处理的,它将涉及到本人的主角RenderNodeDrawable,它继承自SkDrawable

frameworks/base/libs/hwui/pipeline/skia/RenderNodeDrawable.h

java 复制代码
class RenderNodeDrawable : public SkDrawable {}
java 复制代码
void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
    sk_sp<GrDirectContext> cachedContext;

    for (size_t i = 0; i < layers.entries().size(); i++) {
        RenderNode* layerNode = layers.entries()[i].renderNode.get();
        ...
        SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();
        ...
        RenderNodeDrawable root(layerNode, layerCanvas, false);
        root.forceDraw(layerCanvas);
        layerCanvas->restoreToCount(saveCount);
        ...
        GrDirectContext* currentContext =
            GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext());
        if (cachedContext.get() != currentContext) {
            if (cachedContext.get()) {
                ATRACE_NAME("flush layers (context changed)");
                cachedContext->flushAndSubmit();
            }
            cachedContext.reset(SkSafeRef(currentContext));
        }
    }

    if (cachedContext.get()) {
        ATRACE_NAME("flush layers");
        cachedContext->flushAndSubmit();
    }
}

renderLayersImpl方法会遍历所有的layers,然后针对每个layer,进行一些列的判断,满足某些条件的layer才会执行渲染。这里的条件包括,如对一个的RenderNode的SkSurface()不为空,layer对于的RenderNode有绘制指令等。**layerNode->getLayerSurface()->getCanvas()**这里返回的 SkCanvas也即使SkGpuDevice的canvas。然后构造一个RenderNodeDrawable对象,然后调用forceDraw,就进入到RenderNodeDrawable的逻辑

java 复制代码
RenderNodeDrawable::RenderNodeDrawable(RenderNode* node, SkCanvas* canvas, bool composeLayer,
                                       bool inReorderingSection)
        : mRenderNode(node)
        , mRecordedTransform(canvas->getTotalMatrix())
        , mComposeLayer(composeLayer)
        , mInReorderingSection(inReorderingSection) {}

这里mComposeLayer将给赋值为传入的是false

java 复制代码
void RenderNodeDrawable::forceDraw(SkCanvas* canvas) const {
    RenderNode* renderNode = mRenderNode.get();
    MarkDraw _marker{*canvas, *renderNode};

    if ((mProjectedDisplayList == nullptr && !renderNode->isRenderable()) ||
        (renderNode->nothingToDraw() && mComposeLayer)) {
        return;
    }

    SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl();

    SkAutoCanvasRestore acr(canvas, true);
    const RenderProperties& properties = this->getNodeProperties();
    // pass this outline to the children that may clip backward projected nodes
    displayList->mProjectedOutline =
            displayList->containsProjectionReceiver() ? &properties.getOutline() : nullptr;
    if (!properties.getProjectBackwards()) {
        drawContent(canvas);
        if (mProjectedDisplayList) {
            acr.restore();  // draw projected children using parent matrix
            LOG_ALWAYS_FATAL_IF(!mProjectedDisplayList->mProjectedOutline);
            const bool shouldClip = mProjectedDisplayList->mProjectedOutline->getPath();
            SkAutoCanvasRestore acr2(canvas, shouldClip);
            canvas->setMatrix(mProjectedDisplayList->mParentMatrix);
            if (shouldClip) {
                canvas->clipPath(*mProjectedDisplayList->mProjectedOutline->getPath());
            }
            drawBackwardsProjectedNodes(canvas, *mProjectedDisplayList);
        }
    }
    displayList->mProjectedOutline = nullptr;
}

在绘制layer的时候,会判断是否需要绘制,如果不可绘制或者没有绘制内容且composeLayer = true则不需要绘制,之后会取出RenderNode 的properties,如果不是getProjectBackwards的话,才进行绘制,因为设置为ProjectBackwards的节点会被绘制到它的锚点的节点里。进入之后会先调用drawContent(canvas);绘制内容,然后在判断mProjectedDisplayList是否为空,如果不为空的话,表示它就是一个投影锚点,需要去绘制被投影的那些节点,那些节点的绘制指令就保存在mProjectedDisplayList里面。

java 复制代码
void RenderNodeDrawable::drawContent(SkCanvas* canvas) const {
    RenderNode* renderNode = mRenderNode.get();
    
    SkiaDisplayList* displayList = mRenderNode->getDisplayList().asSkiaDl();
    displayList->mParentMatrix = canvas->getTotalMatrix();
        SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl();
       
        if (renderNode->getLayerSurface() && mComposeLayer) {
             sk_sp<SkImage> snapshotImage  = renderNode->getLayerSurface()->makeImageSnapshot();
            if (stretch.isEmpty() ||
                Properties::getStretchEffectBehavior() == StretchEffectBehavior::UniformScale) {
                ...
                if (renderNode->hasHolePunches()) {
                    TransformCanvas transformCanvas(canvas, SkBlendMode::kClear);
                    displayList->draw(&transformCanvas);
                }
                canvas->drawImageRect(snapshotImage, SkRect::Make(srcBounds),
                                      SkRect::Make(dstBounds), sampling, &paint,
                                      SkCanvas::kStrict_SrcRectConstraint);
            } 
            ...
        } else {
            if (alphaMultiplier < 1.0f) {
                // Non-layer draw for a view with getHasOverlappingRendering=false, will apply
                // the alpha to the paint of each nested draw.
                AlphaFilterCanvas alphaCanvas(canvas, alphaMultiplier);
                displayList->draw(&alphaCanvas);
            } else {
                displayList->draw(canvas);
            }
        }
    }
}

如果是mComposeLayer的layer且存在SkSurface,如果不是打孔屏幕的话,会获取SKSurface中的缓存SkImage,然后将这个SkImage画到SkCavas中,从而不会再执行它的DisplayList的指令;否则则执行displayList中的指令,将displayList画到canvas。但是如果是打孔屏幕的画,还是要重新绘制一遍displayList,似乎打孔屏幕不能利用到到Layer缓存带来的性能由优化,只是是因为使用的是TransformCanvas包装了canvas,它会过滤掉一些指令,因此不会执行所有的指令。

displayList->draw(canvas);

diaplayList的类型是SkisDisplayList,它里面保存的是录制的绘制指令
frameworks/base/libs/hwui/pipeline/skia/SkiaDisplayList.h

java 复制代码
void draw(SkCanvas* canvas) { mDisplayList.draw(canvas); }

mDisplayList的类型是DisplayListData,定义RecordingCanvas

java 复制代码
void DisplayListData::draw(SkCanvas* canvas) const {
    SkAutoCanvasRestore acr(canvas, false);
    this->map(draw_fns, canvas, canvas->getTotalMatrix());
}

关于draw_fns的定义如下:

java 复制代码
#define X(T)                                                    \
    [](const void* op, SkCanvas* c, const SkMatrix& original) { \
        ((const T*)op)->draw(c, original);                      \
    },
static const draw_fn draw_fns[] = {
#include "DisplayListOps.in"
};
#undef X

DisplayListOps.in的内容如下:
frameworks/base/libs/hwui/DisplayListOps.in

java 复制代码
X(Flush)
X(Save)
....
X(DrawRect)
...

这里是通过宏定义了一些lamda用于的draw方法。以此Flush,Save,DrawRect,为例子,将draw_fns展开为如下的内容:

java 复制代码
static const draw_fn draw_fns[] = {
    [](const void* op, SkCanvas* c, const SkMatrix& original) { 
    ((const Flush*)op)->draw(c, original); 
    },      
     [](const void* op, SkCanvas* c, const SkMatrix& original) { 
    ((const Save*)op)->draw(c, original); 
    },      
   [](const void* op, SkCanvas* c, const SkMatrix& original) { 
    ((const DrawRect*)op)->draw(c, original); 
    },      
}

map方法如下:

java 复制代码
template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
    auto end = fBytes.get() + fUsed;
    for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
        auto op = (const Op*)ptr;
        auto type = op->type;
        auto skip = op->skip;
        if (auto fn = fns[type]) {  // We replace no-op functions with nullptrs
            fn(op, args...);        // to avoid the overhead of a pointless call.
        }
        ptr += skip;
    }
}

前面介绍过fBytes就是存储绘制Op的数据块,map函数遍历取出这个op之后调用对应的lamda进行处理

每一个op的有他自己的type和draw方法。比如DrawRect

java 复制代码
    struct DrawRect final : Op {
         static const auto kType = Type::DrawRect;
         DrawRect(const SkRect& rect, const SkPaint& paint) : rect(rect), paint(paint) {}
         SkRect rect;
         SkPaint paint;
         void draw(SkCanvas* c, const SkMatrix&) const { c->drawRect(rect, paint); }
};
    

Type::DrawRect也是有一个宏定义,它也使用相同的"DisplayListOps.in",所以每个绘制Op的都能以它的type作为下标找到对应lamda处理函数

java 复制代码
   #define X(T) T,
   enum class Type : uint8_t {
           #include "DisplayListOps.in"
   };
   #undef X

以DrawRect为例fn(op, args...); 即调了DrawRect的draw方法。最后即调用到SkCanvas的drawRect方法,这个方法再介绍SkCanvas的时候已经介绍了,因此这里就不再介绍了。

遍历完整个fBytes之后,所有的之前录制(绘制)到DisplayList内容就保存到了SkCpuDevice的GrSurfaceDrawContextget的GrOpsTask里面了。但仍还没有提交到GPU。

当所有的Layer的渲染完了之后,会调flushAndSubmit来提交GPU,于是完成渲染。

java 复制代码
 if (cachedContext.get()) {
        ATRACE_NAME("flush layers");
        cachedContext->flushAndSubmit();
    }

3 renderFrameImpl

这个逻辑和renderLayer差不多

java 复制代码
void SkiaPipeline::renderFrameImpl(const SkRect& clip,
                                   const std::vector<sp<RenderNode>>& nodes, bool opaque,
                                   const Rect& contentDrawBounds, SkCanvas* canvas,
                                   const SkMatrix& preTransform) {
    ...
    if (1 == nodes.size()) {
        if (!nodes[0]->nothingToDraw()) {
            RenderNodeDrawable root(nodes[0].get(), canvas);
            root.draw(canvas);
        }
    } else if (0 == nodes.size()) {
        // nothing to draw
    } else {
       ...
    }

因为i大部分情况下nodes的元素为1个,因此直接就将他转换成一个RenderNodeDrawable,但是只调用的是draw方法,而不是forceDraw方法。RenderNodeDrawable构造方法默认的composeLayer是true。

frameworks/base/libs/hwui/pipeline/skia/RenderNodeDrawable.h

java 复制代码
class RenderNodeDrawable : public SkDrawable {
      explicit RenderNodeDrawable(RenderNode* node, SkCanvas* canvas, bool composeLayer = true,
                                bool inReorderingSection = false);
      ...
}                                

它的draw方法定义再父类SkDrawable中

external/skia/include/core/SkDrawable.h

java 复制代码
void draw(SkCanvas*, const SkMatrix* = nullptr);

external/skia/src/core/SkDrawable.cpp

java 复制代码
void SkDrawable::draw(SkCanvas* canvas, const SkMatrix* matrix) {
    SkAutoCanvasRestore acr(canvas, true);
    if (matrix) {
        canvas->concat(*matrix);
    }
    this->onDraw(canvas);

    if (false) {
        draw_bbox(canvas, this->getBounds());
    }
}

于是回调子类实现的onDraw方法

java 复制代码
void RenderNodeDrawable::onDraw(SkCanvas* canvas) {
    // negative and positive Z order are drawn out of order, if this render node drawable is in
    // a reordering section
    if ((!mInReorderingSection) || MathUtils::isZero(mRenderNode->properties().getZ())) {
        this->forceDraw(canvas);
    }
}

最后还是进入到forceDraw方法,只是mComposeLayer = true,但是它的laysurface为null,还是直接进入到这段逻辑

java 复制代码
 if (alphaMultiplier < 1.0f) {
                // Non-layer draw for a view with getHasOverlappingRendering=false, will apply
                // the alpha to the paint of each nested draw.
                AlphaFilterCanvas alphaCanvas(canvas, alphaMultiplier);
                displayList->draw(&alphaCanvas);
            } else {
                displayList->draw(canvas);
            }

最后仍然走到displayList->draw(canvas);

这里需要注意的是,再ViewGroup中,绘制子控件的时候,会调用一个drawRenderNode,将子控件的RenderNode转换成一个RendeNodeDrawable,然后使用DrawDrawble指令写入到父控件的fBytes,因此在循环从fBytes中读取出来的Op中可能包含DrawDrawable,这样的话,就会进行递归的调用RendeNodeDrawable.draw方法了。

4 总结

本文主要分析了renderFrame函数的原理,包括了对Layer的处理和RootRenderNode的处理,他们最后都是通过RenderNodeDrawable来进行渲染的。然后将RootRenderNode中的displayList画到SkSurface中完成像素渲染。其中对于Layer的处理逻辑比较难理解。我总结一下设置成Layer与不设置成Layer的差别

  1. 设置成LAYER_TYPE_HARDWARE的View,在prepareTree的时候会为dirty的RenderNode创建一个SkSurface,并且保存到layers中去
  2. 渲染的时候,会先去渲染这些layer,因此传入mComposeLayer为false,因此会执行RenderNode的displayList绘制,并绘制到layer自己的的SkSurface中去
  3. 渲染帧的时候,是使用的RootRenderNode,它的displayList的fBytes中DrawDrawable类型的Op仍然持有上面那些设置成layer的RenderNode,但是因为displayList中的RendeNodeDrawable都是设置mComposeLayer = true,因此在RendeNodeDrawable绘制的时候,如果遇到layer类型的RendeNode则利用第二步中画好的SkSurface生成一个SkImage,再将SkImage画到 最终的canvas中去。
  4. Layer创建好后,如果没有发生变化,则不会设置成layer的RenderNode创建新的layer,也不会出现再layers里面,但它持有的原来的layer,因此再绘制帧的时候直接进入第3步,从而得到优化。
  5. Layer除了能内容没有发生变化的时候,可以重用之前的绘制的SkImage外,也可以作为一个整体应用某些属性。

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

相关推荐
烬奇小云2 小时前
认识一下Unicorn
android·python·安全·系统安全
顾北川_野14 小时前
Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
android
CYRUS STUDIO15 小时前
Android 下内联汇编,Android Studio 汇编开发
android·汇编·arm开发·android studio·arm
右手吉他15 小时前
Android ANR分析总结
android
PenguinLetsGo16 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
杨武博19 小时前
音频格式转换
android·音视频
音视频牛哥21 小时前
Android音视频直播低延迟探究之:WLAN低延迟模式
android·音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·android rtmp
ChangYan.21 小时前
CondaError: Run ‘conda init‘ before ‘conda activate‘解决办法
android·conda
二流小码农21 小时前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
夏非夏1 天前
Android 生成并加载PDF文件
android