前面文章介绍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;
...
}
它有两个重要的成员变量,mDisplayList 和 mRecorder
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>(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字节数组中。