Android View绘制原理 - SkCanvas

上一篇文章介绍了在Android框架中的各种Canvas,其中C层的RecordingCanas承上启下,在SkiaRecordingCanvas的绘制方法会通过调用它的mRecorder来记录,而这个mRecorder的类型正好就是SkCanvas,准确的说是它的子类RecordingCanas。而各种绘制方法会对应生成一个Op对象来描述这个绘制操作,RecordingCanvas将这个op对象分配到它持有的DisplayListData的fBytes上,从而完成记录。 到这里也仅仅是完成记录,离真正使用GPU按照Op描述数据渲染素还很远。这篇我们进入skia库来分析SKCanvas对绘制操作的处理。

SKCanvas有好几个构造方法,根据不同的场景可以生成基于不同绘制目标的对象,绘制的目标被抽象为SkBaseDevice,它有很多的子类,代表不同的绘制目标,比如SkBitmapDevice,绘制到一个SKBitmap上; SkNoPixelsDevice是一个虚拟的不会绘制成像素点的设备,比如前面介绍的SKCanvas的子类RecordingCanvas就是使用的SkNoPixelsDevice,它就只是将绘制命令记录到一个二进制数组,并不渲染像素;还有SkGpuDevice,这才是代表使用GPU进行像素渲染的目标设备。所以要完成像素渲染,必须要要在某个地方生成一个使用SkGpuDevice的SkCanvas的对象。

1 SkCanvas

下面是几个构造函数的定义:

这是外部传入Device的构造方法,会将device传入init方法进行初始化

java 复制代码
SkCanvas::SkCanvas(sk_sp<SkBaseDevice> device)
    : fMCStack(sizeof(MCRec), fMCRecStorage, sizeof(fMCRecStorage))
    , fProps(device->surfaceProps())
{
    inc_canvas();

    this->init(device);
}

这是RecordingCanvas继承的构造方法,init传入null进行初始化,内部会生成一个SkNoPixelsDevice

java 复制代码
SkCanvas::SkCanvas()
    : fMCStack(sizeof(MCRec), fMCRecStorage, sizeof(fMCRecStorage))
    , fProps()
{
    inc_canvas();
    this->init(nullptr);
}

这是使用SkBitmap作为绘制目标的构造方法,它会生成一个SkBitmapDevice,用于绘制

java 复制代码
SkCanvas::SkCanvas(const SkBitmap& bitmap, const SkSurfaceProps& props)
    : fMCStack(sizeof(MCRec), fMCRecStorage, sizeof(fMCRecStorage))
    , fProps(props)
{
    inc_canvas();

    sk_sp<SkBaseDevice> device(new SkBitmapDevice(bitmap, fProps, nullptr, nullptr));
    this->init(device);
}

所以看到,构造函数内部都会调用init方法来初始化,下面分析一下这个init方法

java 复制代码
void SkCanvas::init(sk_sp<SkBaseDevice> device) {
     ...
    if (!device) {
        device = sk_make_sp<SkNoPixelsDevice>(SkIRect::MakeEmpty(), fProps);
    }

    // From this point on, SkCanvas will always have a device
    SkASSERT(device);

    fSaveCount = 1;
    fMCRec = new (fMCStack.push_back()) MCRec(device.get());
    fMarkerStack = sk_make_sp<SkMarkerStack>();

    // The root device and the canvas should always have the same pixel geometry
    SkASSERT(fProps.pixelGeometry() == device->surfaceProps().pixelGeometry());
    device->androidFramework_setDeviceClipRestriction(&fClipRestrictionRect);
    device->setMarkerStack(fMarkerStack.get());

    fSurfaceBase = nullptr;
    fBaseDevice = std::move(device);
    fScratchGlyphRunBuilder = std::make_unique<SkGlyphRunBuilder>();
    fQuickRejectBounds = this->computeDeviceClipBounds();
}

如果device为空,则生成一个SkNoPixelsDevice对象以确保每个SKCanvas都有绘制目标设备,然后将device保存到fBaseDevice

然后以device初始化一个MCRec,然后保存到fMCStack. MCRec的定义如下:看起来时记录一个Layer的绘制,每个layer将对应一个fBackImage。而SkCanvas中fMCStack是一个栈,所以layer将以栈的方式来保存。每个layer都持有一个SkBaseDevice 和 一个BackImage。初始化时即默认包含一个layer,即便没有调用过saveLayer方法。所以fSaveCount也被设置为1。

java 复制代码
class SkCanvas::MCRec {
public:
    // If not null, this MCRec corresponds with the saveLayer() record that made the layer.
    // The base "layer" is not stored here, since it is stored inline in SkCanvas and has no
    // restoration behavior.
    std::unique_ptr<Layer> fLayer;

    // This points to the device of the top-most layer (which may be lower in the stack), or
    // to the canvas's fBaseDevice. The MCRec does not own the device.
    SkBaseDevice* fDevice;

    std::unique_ptr<BackImage> fBackImage;
    SkM44 fMatrix;
    int fDeferredSaveCount;
    MCRec(SkBaseDevice* device)
            : fLayer(nullptr)
            , fDevice(device)
            , fBackImage(nullptr)
            , fDeferredSaveCount(0) {
        SkASSERT(fDevice);
        fMatrix.setIdentity();
        inc_rec();
    ...
    }
    

2 drawRect

SkCanvas也有很多的对应绘制方法,流程也差不多,最后,我们也来看看SkCanvas的绘制矩形的方法drawRect

java 复制代码
void SkCanvas::drawRect(const SkRect& r, const SkPaint& paint) {
    ...
    this->onDrawRect(r.makeSorted(), paint);
}

继续调用onDrawRect方法

java 复制代码
void SkCanvas::onDrawRect(const SkRect& r, const SkPaint& paint) {
    SkASSERT(r.isSorted());
    if (this->internalQuickReject(r, paint)) {
        return;
    }

    AutoLayerForImageFilter layer(this, paint, &r, CheckForOverwrite::kYes);
    this->topDevice()->drawRect(r, layer.paint());
}
java 复制代码
SkBaseDevice* SkCanvas::topDevice() const {
    SkASSERT(fMCRec->fDevice);
    return fMCRec->fDevice;
}

它使用的时topDevice,就是最上面一个layer对应的device。因为我们的想要看一下如何进行像素渲染的,因此看一下SkGpuDevice的情况

external/skia/src/gpu/SkGpuDevice.cpp

java 复制代码
void SkGpuDevice::drawRect(const SkRect& rect, const SkPaint& paint) {
    ...
    fSurfaceDrawContext->drawRect(this->clip(), std::move(grPaint),
                                  fSurfaceDrawContext->chooseAA(paint), this->localToDevice(), rect,
                                  &style);
}

在SkGpuDevice中由会去调用fSurfaceDrawContext的drawRect方法。它的类型是GrSurfaceDrawContext

external/skia/src/gpu/SkGpuDevice.h

java 复制代码
private:
    std::unique_ptr<GrSurfaceDrawContext> fSurfaceDrawContext;

它是在构造的时候从外部传入的
external/skia/src/gpu/SkGpuDevice.cpp

java 复制代码
SkGpuDevice::SkGpuDevice(std::unique_ptr<GrSurfaceDrawContext> surfaceDrawContext, unsigned flags)
        : INHERITED(make_info(surfaceDrawContext.get(), SkToBool(flags & kIsOpaque_Flag)), surfaceDrawContext->surfaceProps())
        , fContext(sk_ref_sp(surfaceDrawContext->recordingContext()))
        , fSurfaceDrawContext(std::move(surfaceDrawContext))
      ...
}

我们直接去看一下GrSurfaceDrawContext的drawRect方法, 且仅仅看看Fill的情况

java 复制代码
void GrSurfaceDrawContext::drawRect(const GrClip* clip,
                                    GrPaint&& paint,
                                    GrAA aa,
                                    const SkMatrix& viewMatrix,
                                    const SkRect& rect,
                                    const GrStyle* style) {
   ...
    AutoCheckFlush acf(this->drawingManager());

    const SkStrokeRec& stroke = style->strokeRec();
    if (stroke.getStyle() == SkStrokeRec::kFill_Style) {
        // Fills the rect, using rect as its own local coordinates
        this->fillRectToRect(clip, std::move(paint), aa, viewMatrix, rect, rect);
        return;
    } 
    ...
}

继续调用fillRectToRect

java 复制代码
void GrSurfaceDrawContext::fillRectToRect(const GrClip* clip,
                                          GrPaint&& paint,
                                          GrAA aa,
                                          const SkMatrix& viewMatrix,
                                          const SkRect& rectToDraw,
                                          const SkRect& localRect) {
    DrawQuad quad{GrQuad::MakeFromRect(rectToDraw, viewMatrix), GrQuad(localRect),
                  aa == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone};
     ...
    this->drawFilledQuad(clip, std::move(paint), aa, &quad);
}

继续调用drawFilledQuad

java 复制代码
void GrSurfaceDrawContext::drawFilledQuad(const GrClip* clip,
                                          GrPaint&& paint,
                                          GrAA aa,
                                          DrawQuad* quad,
                                          const GrUserStencilSettings* ss) {
        ...
        this->addDrawOp(finalClip, GrFillRectOp::Make(fContext, std::move(paint), aaType,
                                                      quad, ss));
    }
}

继续调用addDrawOp

java 复制代码
void GrSurfaceDrawContext::addDrawOp(const GrClip* clip,
                                     GrOp::Owner op,
                                     const std::function<WillAddOpFn>& willAddFn) {
 
    GrDrawOp* drawOp = (GrDrawOp*)op.get();
    
   ...
    auto opsTask = this->getOpsTask();
   ...
    opsTask->addDrawOp(this->drawingManager(), std::move(op), fixedFunctionFlags, analysis,
                       std::move(appliedClip), dstProxyView,
                       GrTextureResolveManager(this->drawingManager()), *this->caps());
  ...
}

这里的GrDrawOp 是一个GrFillRectOp对象,表示绘制填充矩形,这和Android中的Op是差不多的概念,仅仅是描述对象。最后将这个描述对象添加到了opsTask。 getOpsTask是定义在GrSurfaceDrawContext的父类GrSurfaceFillContext中的方法

java 复制代码
GrOpsTask* GrSurfaceFillContext::getOpsTask() {
    if (!fOpsTask || fOpsTask->isClosed()) {
        sk_sp<GrOpsTask> newOpsTask = this->drawingManager()->newOpsTask(
                this->writeSurfaceView(), this->arenas(), fFlushTimeOpsTask);
        this->willReplaceOpsTask(fOpsTask.get(), newOpsTask.get());
        fOpsTask = std::move(newOpsTask);
    }
    SkASSERT(!fOpsTask->isClosed());
    return fOpsTask.get();
}

获得了一个GrOpsTask之后,调用addDrawOp方法
external/skia/src/gpu/GrOpsTask.cpp

java 复制代码
void GrOpsTask::addDrawOp(GrDrawingManager* drawingMgr, GrOp::Owner op,
                          GrDrawOp::FixedFunctionFlags fixedFunctionFlags,
                          const GrProcessorSet::Analysis& processorAnalysis, GrAppliedClip&& clip,
                          const DstProxyView& dstProxyView,
                          GrTextureResolveManager textureResolveManager, const GrCaps& caps) {
     ...
    this->recordOp(std::move(op), processorAnalysis, clip.doesClip() ? &clip : nullptr,
                   &dstProxyView, caps);
}

继续调用recordOp方法

java 复制代码
void GrOpsTask::recordOp(
        GrOp::Owner op, GrProcessorSet::Analysis processorAnalysis, GrAppliedClip* clip,
        const DstProxyView* dstProxyView, const GrCaps& caps) {
     ...
     GrSurfaceProxy* proxy = this->target(0);
     ...
     fOpChains.emplace_back(std::move(op), processorAnalysis, clip, dstProxyView);
}

fOpChains是一个OpChain的集合,因此最后recordOp是生成了一个OpChain对象,并放入到fOpChains中。

external/skia/src/gpu/GrOpsTask.h

java 复制代码
SkSTArray<25, OpChain> fOpChains;

3 总结

本文接着上一篇文章,继续分析了skia层的SkCanvas, 它可以接受多种绘制目标设备,比如它的子类RecordingCanvas使用的是SkNoPixelsDevice,因此只能记录而不能渲染成像素;需要渲染成像素需要使用比如SkGpuDevice。SkCanvas除了device这个重要属性外,还有一个fMCStack用于保存绘制Layer,并且默认会创建一个Layer,绘制时,绘制方法都是作用于栈顶的Layer。接着分析了典型的绘制方法drawRect,它穿越了多个类,最后生成一个OpChain对象保存GrOpsTask的fOpChains集合。因此到目前位置,SkCanvas仍然只是起到一个记录的作用,并未发生像素渲染。

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

相关推荐
Kapaseker1 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton4 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
ji_shuke5 小时前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday04267 小时前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理8 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台8 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐8 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极9 小时前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan9 小时前
setHintTextColor不生效
android
德育处主任9 小时前
在小程序做海报的话,Painter就很给力
前端·微信小程序·canvas