Android-View绘制原理(09)-RecordingCanvas

前面文章介绍RenderNode, 它承包了View的绘制业务,提供了绘制的Canvas,今天这篇文章就来分析一下这个Canvas, 并看看一个基本的绘制功能是如何完成的。

1 获取对象

前文中分析过,在RenderNode.beginRecording的时候,会通过RecordingCanas.obtain方法获取一个还缓存,我们就从这里接着分析
frameworks/base/graphics/java/android/graphics/RenderNode.java

public @NonNull RecordingCanvas beginRecording(int width, int height) {
        if (mCurrentRecordingCanvas != null) {
            throw new IllegalStateException(
                    "Recording currently in progress - missing #endRecording() call?");
        }
        mCurrentRecordingCanvas = RecordingCanvas.obtain(this, width, height);
        return mCurrentRecordingCanvas;
    }
 static RecordingCanvas obtain(@NonNull RenderNode node, int width, int height) {
        if (node == null) throw new IllegalArgumentException("node cannot be null");
        RecordingCanvas canvas = sPool.acquire();
        if (canvas == null) {
            canvas = new RecordingCanvas(node, width, height);
        } else {
            nResetDisplayListCanvas(canvas.mNativeCanvasWrapper, node.mNativeRenderNode,
                    width, height);
        }
        canvas.mNode = node;
        canvas.mWidth = width;
        canvas.mHeight = height;
        return canvas;
    }

RecordingCanvas准备了25个缓存,如果缓存都在使用的话,sPool就获取不到可用的缓存,此时需要重新创建一个新的对象。否则调用
nResetDisplayListCanvas 来重新设置宽高等参数。先分析一下新创建的情况。

1.1 构造新对象

frameworks/base/graphics/java/android/graphics/RecordingCanvas.java

private RecordingCanvas(@NonNull RenderNode node, int width, int height) {
        super(nCreateDisplayListCanvas(node.mNativeRenderNode, width, height));
        mDensity = 0; // disable bitmap density scaling
    }

nCreateDisplayListCanvas函数将会被映射到C层的android_view_DisplayListCanvas_createDisplayListCanvas函数
frameworks/base/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp

static JNINativeMethod gMethods[] = {
        // ------------ @CriticalNative --------------
        {"nCreateDisplayListCanvas", "(JII)J",
         (void*)android_view_DisplayListCanvas_createDisplayListCanvas},
         ...
}
static jlong android_view_DisplayListCanvas_createDisplayListCanvas(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr,
        jint width, jint height) {
    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
    return reinterpret_cast<jlong>(Canvas::create_recording_canvas(width, height, renderNode));
}

调用Canvas::create_recording_canvas函数来生成对象

frameworks/base/libs/hwui/hwui/Canvas.cpp

Canvas* Canvas::create_recording_canvas(int width, int height, uirenderer::RenderNode* renderNode) {
    return new uirenderer::skiapipeline::SkiaRecordingCanvas(renderNode, width, height);
}

最终生成的是一个SkiaRecordingCanvas对象
frameworks/base/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h

class SkiaRecordingCanvas : public SkiaCanvas {
 explicit SkiaRecordingCanvas(uirenderer::RenderNode* renderNode, int width, int height) {
        initDisplayList(renderNode, width, height);
    }
    
    ...
    RecordingCanvas mRecorder;
    std::unique_ptr<SkiaDisplayList> mDisplayList;
    ...
}

它有两个重要的成员变量,mDisplayListmRecorder

mDisplayList 的类型是SkiaDisplayList,在这个canvas上绘制的内容,会转换成绘制命令,保存到这个mDisplayList里面。在构造方法内部调用initDisplayList,初始化mDisplayList

mRecorder的类型是RecordingCanvas,这里看起来是比较容易混淆的,RecordingCanvas 和SkiaRecordingCanvas 并没有直接继承关系。

SkiaRecordingCanvas - > SkiaCanvas -> Canvas

RecordingCanvas -> SkNoDrawCanvas -> SkCanvas

而SkCanvas是图形库skia里的api,因此我们可以得出这样的结论,SkiaRecordingCanvas 是android层Canvas实际对应的类,在android与skia之间,定义了一个适配层RecordingCanvas(虽然它的名字和Java层的类名是相同,但是它与RecordingCanvas没有直接的关系.它只是SkiaRecordingCanvas的一个成员变量mRecorder),android层的绘制命令通过调用RecordingCanvas对应的方法,进入到skia层完成的绘制命令的记录。

1.2 重用已有Canvas

如果有可用的缓存,则通过nResetDisplayListCanvas重置一下属性

static void android_view_DisplayListCanvas_resetDisplayListCanvas(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr,
        jlong renderNodePtr, jint width, jint height) {
    Canvas* canvas = reinterpret_cast<Canvas*>(canvasPtr);
    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
    canvas->resetRecording(width, height, renderNode);
}

通过指针找到C层的canvas对象,从上面的分析结论可知,它是一个SkiaRecordingCanvas对象,然后调用resetRecording 重新设置宽高和新的renderNode

virtual void resetRecording(int width, int height,
                                uirenderer::RenderNode* renderNode = nullptr) override {
        initDisplayList(renderNode, width, height);
    }

这和构造方法一样,调用的是initDisplayList方法。下面我们来分析一下这个方法。

2 initDisplayList

void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, int width,
                                          int height) {
    mCurrentBarrier = nullptr;
    SkASSERT(mDisplayList.get() == nullptr);

    if (renderNode) {
        mDisplayList = renderNode->detachAvailableList();
    }
    if (!mDisplayList) {
        mDisplayList.reset(new SkiaDisplayList());
    }

    mDisplayList->attachRecorder(&mRecorder, SkIRect::MakeWH(width, height));
    SkiaCanvas::reset(&mRecorder);
    mDisplayList->setHasHolePunches(false);
}

这里总共有四个步骤,下面分别介绍一下:

2.1 renderNode->detachAvailableList()

首先分离renderNode和它的mAvailableDisplayList, 之后renderNode中的mAvailableDisplayList将为空

	
	 std::unique_ptr<skiapipeline::SkiaDisplayList> detachAvailableList() {
        return std::move(mAvailableDisplayList);
    }
	

2.2 mDisplayList.reset(new SkiaDisplayList());

重置取出来的displayList,设置为新创建的SkiaDisplayList对象,因为在这种情况下,意味需要完全重新绘制。它是一个智能指针,reset后指向新的这个对象, 然后将mRecorder附着到新的displayList,上面分析过,它是一个c层RecordingCanvas。

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

void attachRecorder(RecordingCanvas* recorder, const SkIRect& bounds) {
        recorder->reset(&mDisplayList, bounds);
    }

这里继续调用RecordingCanvas的reset方法,传入当前SkiaDisplayList的成员变量mDisplayList的地址,这个mDisplayList的类型是DisplayListData
frameworks/base/libs/hwui/pipeline/skia/SkiaDisplayList.h

DisplayListData mDisplayList;

因为在SkiaRecordingCanvas中也又一个mDisplayList成员,而它的类型却是SkiaDisplayList,二者容易混淆起来,其实他们的关系如下:

canvas: SkiaRecordingCanvas
       - mDisplayList: SkiaDisplayList
             - mDisplayList: DisplayListData

frameworks/base/libs/hwui/RecordingCanvas.cpp

void RecordingCanvas::reset(DisplayListData* dl, const SkIRect& bounds) {
    this->resetCanvas(bounds.right(), bounds.bottom());
    fDL = dl;
    mClipMayBeComplex = false;
    mSaveCount = mComplexSaveCount = 0;
}

这里完成底层skia库里canvas的重置方法后,将新的DisplayListData赋值给RecordingCanvas 的fDL字段。实质上真正保存绘制指令的地方就是这个fDL

2.3 SkiaCanvas::reset(&mRecorder)

最后再调用了SkiaCanvas::reset(&mRecorder);。 mRecorder的类型是RecordingCanvas。因为SkiaRecordingCanvas 是SkiaCanvas的子类,因此这相当于是将mReorder赋值给当前的SkiaRecordingCanvas的mCanvas字段,也就是说SkiaRecordingCanvas.mCanvas 这个SkCanvas的值是一个RecordingCanvas对象。

void SkiaCanvas::reset(SkCanvas* skiaCanvas) {
    if (mCanvas != skiaCanvas) {
        mCanvas = skiaCanvas;
        mCanvasOwned.reset();
    }
    mSaveStack.reset(nullptr);
}

frameworks/base/libs/hwui/SkiaCanvas.h

SkCanvas* mCanvas;     

3 RecordingCanvas

这里的RecordingCanvas是指的C层的RecordingCanvas,如上所述,它是一个适配层,上层应用的绘制方法会通过这个类转发到skia的canvas上进行绘制,或者说记录。我们接着分析一下一个典型的流程。我们以drawRect为例。

在java层,我们获取到的是个对象也是RecordingCanvas, 它继承自BaseRecordingCanvas,并最终继承自Canvas,当然对应于C层的对象类型是SkiaRecordingCanvas

RecordingCanvas -> BaseRecordingCanvas -> Canvas.

drawRect的方法是定义在BaseRecordingCanvas

 @Override
   public final void drawRect(float left, float top, float right, float bottom,
           @NonNull Paint paint) {
       nDrawRect(mNativeCanvasWrapper, left, top, right, bottom, paint.getNativeInstance());
   }

mNativeCanvasWrapper这个字段记录的是C层的SkiaRecordingCanvas指针,因此进入到C层的nDrawRect
frameworks/base/libs/hwui/jni/android_graphics_Canvas.cpp

static const JNINativeMethod gDrawMethods[] = {
     ...
    {"nDrawRect","(JFFFFJ)V", (void*) CanvasJNI::drawRect},
}

映射到CanvasJNI::drawRect函数

static void drawRect(JNIEnv* env, jobject, jlong canvasHandle, jfloat left, jfloat top,
                     jfloat right, jfloat bottom, jlong paintHandle) {
    const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
    get_canvas(canvasHandle)->drawRect(left, top, right, bottom, *paint);
}


static Canvas* get_canvas(jlong canvasHandle) {
    return reinterpret_cast<Canvas*>(canvasHandle);
}

get_canvas函数将指针转换成Canvas对象,这个实质就是前面分析的SkiaRecordingCanvas,因此进入到SkiaRecordingCanvas的drawRect方法,这个方法是在SkiaRecordingCanvas的父类SkiaCanvas中定义的
frameworks/base/libs/hwui/SkiaCanvas.cpp

void SkiaCanvas::drawRect(float left, float top, float right, float bottom, const Paint& paint) {
    if (CC_UNLIKELY(paint.nothingToDraw())) return;
    applyLooper(&paint, [&](const SkPaint& p) {
        mCanvas->drawRect({left, top, right, bottom}, p);
    });
}

applyLooper函数将lamba表达是放到looper中去执行,定在在SkiaCanvas.h,不详细去分析这个函数,最后会执行到这个lamda,于是会执行到mCanvas->drawRect({left, top, right, bottom}, p);, 而这个mCanvas是SkiaRecordingCanvas的成员变量, 它是C层的RecordingCanvas。 drawRect方法是定义在它的父类SkCanvas中,属于skia库的内容
external/skia/src/core/SkCanvas.cpp

void SkCanvas::drawRect(const SkRect& r, const SkPaint& paint) {
    TRACE_EVENT0("skia", TRACE_FUNC);
    // To avoid redundant logic in our culling code and various backends, we always sort rects
    // before passing them along.
    this->onDrawRect(r.makeSorted(), paint);
}

回调onDrawRect,从而又回调到子类RecordingCanvas的实现

frameworks/base/libs/hwui/RecordingCanvas.cpp

void RecordingCanvas::onDrawRect(const SkRect& rect, const SkPaint& paint) {
    fDL->drawRect(rect, paint);
}

内部调用fDL的drawRect方法,这个** fDL**是一个DisplayListData类型的对象,正是上面initDisplayList的时候设置到RecordingCanvas的。看一下它的drawRect方法
frameworks/base/libs/hwui/RecordingCanvas.cpp

void DisplayListData::drawRect(const SkRect& rect, const SkPaint& paint) {
    this->push<DrawRect>(0, rect, paint);
}

进一步调push函数,类型是的DrawRect

template <typename T, typename... Args>
void* DisplayListData::push(size_t pod, Args&&... args) {
    size_t skip = SkAlignPtr(sizeof(T) + pod);
    SkASSERT(skip < (1 << 24));
    if (fUsed + skip > fReserved) {
        static_assert(SkIsPow2(SKLITEDL_PAGE), "This math needs updating for non-pow2.");
        // Next greater multiple of SKLITEDL_PAGE.
        fReserved = (fUsed + skip + SKLITEDL_PAGE) & ~(SKLITEDL_PAGE - 1);
        fBytes.realloc(fReserved);
        LOG_ALWAYS_FATAL_IF(fBytes.get() == nullptr, "realloc(%zd) failed", fReserved);
    }
    SkASSERT(fUsed + skip <= fReserved);
    auto op = (T*)(fBytes.get() + fUsed);
    fUsed += skip;
    new (op) T{std::forward<Args>(args)...};
    op->type = (uint32_t)T::kType;
    op->skip = skip;
    return op + 1;
}

这里先计算出新的操作(这里是DrawRect)的size,并分配储存空间,使用op指向这块内存,然后使用 new (op) T{std::forward(args)…};在指定的内存创建一个操作对象,这是C++的placement new的语法,于是就push完成新的操作。因为我们是调用的drawRect函数,我们分析对应的操作DrawRect的定义

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); }
};

它包含一个rect 和paint属性。因此我们看到drawRect函数其实就是将DrawRect这个对象push到fBytes里面, fBytes里保存的即所谓的绘制命令。Op是对绘制操作的抽象,它有很多子类,这里就不一一介绍了。

struct Op {
    uint32_t type : 8;
    uint32_t skip : 24;
};

4 总结

我们从Java获取Canvas的调用,逐步深入分析了整个创建和初始化的流程,同时也分析了几个重要的容易混淆的Canvas类,包括SkiaRecordingCanvas,RecordingCanvas,SkiaCanvas,SkCanvas,并介绍了他们的关系, 以及相关的SkiaDisplayList 和 DisplayListData,最后以drawRect为例,详细分析了记录的流程,它生成了一个DrawRect操作对象,用这个对象记录下了它的rect和paint属性,最后将这个对象保存到DisplayListData的fBytes字节数组中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值