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>(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字节数组中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容