Android View绘制原理 - RecordingCanvas

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

1 获取对象

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

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;
    }
java 复制代码
 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

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

java 复制代码
static JNINativeMethod gMethods[] = {
        // ------------ @CriticalNative --------------
        {"nCreateDisplayListCanvas", "(JII)J",
         (void*)android_view_DisplayListCanvas_createDisplayListCanvas},
         ...
}
java 复制代码
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

java 复制代码
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

java 复制代码
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重置一下属性

java 复制代码
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

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

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

2 initDisplayList

java 复制代码
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将为空

java 复制代码
    
     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

java 复制代码
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

java 复制代码
DisplayListData mDisplayList;

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

java 复制代码
canvas: SkiaRecordingCanvas
       - mDisplayList: SkiaDisplayList
             - mDisplayList: DisplayListData

frameworks/base/libs/hwui/RecordingCanvas.cpp

java 复制代码
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对象。 java

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

frameworks/base/libs/hwui/SkiaCanvas.h

java 复制代码
SkCanvas* mCanvas;     

3 RecordingCanvas

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

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

RecordingCanvas -> BaseRecordingCanvas -> Canvas.

drawRect的方法是定义在BaseRecordingCanvas

java 复制代码
 @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

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

映射到CanvasJNI::drawRect函数

java 复制代码
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);
}
java 复制代码
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

java 复制代码
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

java 复制代码
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

java 复制代码
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

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

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

java 复制代码
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的定义

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

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

java 复制代码
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字节数组中。

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

相关推荐
三少爷的鞋8 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)12 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码14 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程17 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓19 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer