SKP Capture 如何保存图片?

在 Android 上,我们可以抓取 hwui SKP 分析 View 的绘制情况。本文将对 SKP capture 保存图片的流程进行分析,并与读者探讨如何参考 SKP capture 开发一个将客制化渲染结果保存为图片的 debug 功能。

一、SKP Capture 流程简介

先简单介绍一下 hwui SKP capture 的整体流程。SKP capture 的主要流程在 renderFrame 中,首先会调用 tryCapture 初始化一个 SkCanvas* 指针,然后调用 renderLayersImplrenderFrameImpl 进行绘制,最后调用 endCapture 结束抓取。

C++ 复制代码
void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip,
                               const std::vector<sp<RenderNode>>& nodes, bool opaque,
                               const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
                               const SkMatrix& preTransform) {
    // ...

    // Initialize the canvas for the current frame, that might be a recording canvas if SKP
    // capture is enabled.
    SkCanvas* canvas = tryCapture(surface.get(), nodes[0].get(), layers);

    // draw all layers up front
    renderLayersImpl(layers, opaque);

    renderFrameImpl(clip, nodes, opaque, contentDrawBounds, canvas, preTransform);

    endCapture(surface.get());

    // ...
}

二、切入点:保存图片文件的函数

上一节初步介绍了 SKP capture 的整体流程,但 SkiaPipeline 的绘制是一个非常复杂的流程,直接分析起来相当困难(后续文章可能会展开分析,先挖个坑)。我们不妨选一个点由浅入深进行分析,这里笔者选取的是 SKP capture 中直接负责保存图片文件的函数------savePictureAsync

C++ 复制代码
static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) {
    CommonPool::post([data, filename] {
        if (0 == access(filename.c_str(), F_OK)) {
            return;
        }

        SkFILEWStream stream(filename.c_str());
        if (stream.isValid()) {
            stream.write(data->data(), data->size());
            stream.flush();
            ALOGD("SKP Captured Drawing Output (%zu bytes) for frame. %s", stream.bytesWritten(),
                     filename.c_str());
        }
    });
}

savePictureAsync 函数的流程很简单,核心是利用 SkFILEWStream::write 函数将绘制出来的数据写入到文件中。

三、SKP Capture 如何追踪绘制

3.1 savePictureAsync 的数据源

顺着 savePictureAsync 函数的线索向上追溯,它在 endCapture 中被调用:

C++ 复制代码
void SkiaPipeline::endCapture(SkSurface* surface) {
    // ...
        sk_sp<SkPicture> picture = mRecorder->finishRecordingAsPicture();
        if (picture->approximateOpCount() > 0) {
            if (mPictureCapturedCallback) {
                std::invoke(mPictureCapturedCallback, std::move(picture));
            } else {
                // single frame skp to file
                SkSerialProcs procs;
                procs.fTypefaceProc = [](SkTypeface* tf, void* ctx){
                    return tf->serialize(SkTypeface::SerializeBehavior::kDoIncludeData);
                };
                procs.fImageProc = [](SkImage* img, void* ctx) -> sk_sp<SkData> {
                    GrDirectContext* dCtx = static_cast<GrDirectContext*>(ctx);
                    return SkPngEncoder::Encode(dCtx,
                                                img,
                                                SkPngEncoder::Options{});
                };
                procs.fImageCtx = mRenderThread.getGrContext();
                auto data = picture->serialize(&procs);
                savePictureAsync(data, mCapturedFile);
                mCaptureSequence = 0;
                mCaptureMode = CaptureMode::None;
            }
        }
    // ...
}

可以看到 savePictureAsync 函数有两个入参:data 来自于 mRecorder->finishRecordingAsPicture(),而 filename 来自于成员变量 mCapturedFile

3.2 mRecorder 的调用逻辑

上一节中出现了 SkiaPipeline 中一个重要的成员变量------mRecorder,它正是 SKP capture 追踪绘制的核心,先看它的定义:

C++ 复制代码
class SkiaPipeline : public renderthread::IRenderPipeline {
    /**
     * mRecorder holds the current picture recorder when serializing in either SingleFrameSKP or
     * CallbackAPI modes.
     */
    std::unique_ptr<SkPictureRecorder> mRecorder;
};

mRecorder 是一个 std::unique_ptr<SkPictureRecorder> 类型的指针,在开启 debug 的情况下才会实际持有对象。SkPictureRecorder 是 skia 中用于记录绘制指令的类,本文的主要关注点在 SKP capture 保存图片的流程上,不对 SkPictureRecorder 的实现细节做分析。

继续看 mRecorder 的调用逻辑。mRecorder 仅在 tryCaptureendCapture 两个函数中被调用。在 tryCapture 函数中调用 reset 释放旧对象,接受新对象,之后再调用 beginRecording 开始追踪。在 endCapture 函数中调用 finishRecordingAsPicture 结束追踪并返回 SkPicture 指针,最后调用 reset 释放对象。

C++ 复制代码
SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface, RenderNode* root,
    const LayerUpdateQueue& dirtyLayers) {
    // ...
    // Create a canvas pointer, fill it depending on what kind of capture is requested (if any)
    SkCanvas* pictureCanvas = nullptr;
    switch (mCaptureMode) {
        case CaptureMode::CallbackAPI:
        case CaptureMode::SingleFrameSKP:
            mRecorder.reset(new SkPictureRecorder());
            pictureCanvas = mRecorder->beginRecording(surface->width(), surface->height());
            break;
        case CaptureMode::MultiFrameSKP:
            // If a multi frame recording is active, initialize recording for a single frame of a
            // multi frame file.
            pictureCanvas = mMultiPic->beginPage(surface->width(), surface->height());
            break;
        case CaptureMode::None:
            // Returning here in the non-capture case means we can count on pictureCanvas being
            // non-null below.
            return surface->getCanvas();
    }
    // ...
}

void SkiaPipeline::endCapture(SkSurface* surface) {
    // ...
    if (mCaptureSequence > 0 && mCaptureMode == CaptureMode::MultiFrameSKP) {
        // ...
    } else {
        sk_sp<SkPicture> picture = mRecorder->finishRecordingAsPicture();
        // ...
        mRecorder.reset();
    }
}

四、思考题:如何在客制化功能里保存绘制结果?

假设读者你现在负责开发一个绘制相关的客制化功能,需要增加一个 debug 功能保存绘制结果,请问你应该编写代码?

参考 SKP capture 源码,笔者认为至少可以从以下几点入手。

4.1 通过 SkFILEWStream 保存图片

最后一步保存图片文件自然是通过 SkFILEWStream::write 函数:

C++ 复制代码
// sk_sp<SkData> data = ...;
// std::string filename = ...;
SkFILEWStream stream(filename.c_str());
stream.write(data->data(), data->size());

需要传入两个参数 datafilenamefilename 不成问题,难点在如何拿到 data

4.2 获取 SkData

这是开发 debug 功能的核心,需要我们在客制化功能链路中找到正确的渲染对象,并获取 SkData。下面提供几种常见的 skia 类型转换成 SkData 的方法。

4.2.1 SkPicture

在 SKP capture 源码中,Google 正是将 SkPicture 转换为 SkData,调用的函数为 serialize,详细代码可以参阅 3.1 节中的 endCapture 函数代码。

4.2.2 SkImage

通过 encodeToData 函数转换。

C++ 复制代码
// sk_sp<SkImage> image = ...;
sk_sp<SkData> data = image->encodeToData(SkEncodeImageFormat::kPNG, 100);

4.2.3 SkBitmap

先转换成 SkImage,再通过 encodeToData 函数转换(同 4.2.2 节)。

C++ 复制代码
// sk_sp<SkBitmap> bitmap = ...;
sk_sp<SkImage> image = SkImage::MakeFromBitmap(bitmap);
sk_sp<SkData> data = image->encodeToData(SkEncodeImageFormat::kPNG, 100);

4.2.4 SkSurface

对于 SkSurface,需要先获取其内容。比如调用 makeImageSnapshot 函数,创建一个 SkImage 指针,后续流程同 4.2.2 节。

C++ 复制代码
// sk_sp<SkSurface> surface = ...;
sk_sp<SkImage> snapshot = surface->makeImageSnapshot();
sk_sp<SkData> data = snapshot->encodeToData(SkEncodeImageFormat::kPNG, 100);

4.3 通过 GraphicBuffer 转换

如果在你的功能中没有直接使用 skia 库里的类型,而是使用了 GraphicBuffer,应该如何将其转换为 SkData

在较低版本的 NDK 中,只能将 GraphicBuffer 转换成 ANativeWindowBuffer(笔者目前也不清楚后续该如何转换,欢迎大佬留言反馈):

C++ 复制代码
// sp<GraphicBuffer> graphicBuffer = ...;
ANativeWindowBuffer* nativeBuffer = graphicBuffer->getNativeBuffer();

在较新的支持 AHardwareBuffer 的 NDK 版本上,可以通过 toAHardwareBuffer 函数将 GraphicBuffer 直接转换为 AHardwareBuffer

C++ 复制代码
// sp<GraphicBuffer> graphicBuffer = ...;
AHardwareBuffer* hardwareBuffer = graphicBuffer->toAHardwareBuffer();

拿到 AHardwareBuffer 后先创建 GrBackendTexture,再转换成 SkImage,最后同 4.2.2 节。

C++ 复制代码
std::unique_ptr<SkiaBackendTexture> backendTexture = gpuContext->makeBackendTexture(graphicBuffer->toAHardwareBuffer(), false);
sk_sp<SkImage> image = backendTexture->makeImage(kPremul_SkAlphaType, dataspace, nullptr, nullptr);
sk_sp<SkData> data = image->encodeToData(SkEncodeImageFormat::kPNG, 100);

本文中所有 AOSP 代码的版本均为 android15-qpr1-release。

相关推荐
雨白3 小时前
Android 多线程:理解 Handler 与 Looper 机制
android
sweetying6 小时前
30了,人生按部就班
android·程序员
用户2018792831676 小时前
Binder驱动缓冲区的工作机制答疑
android
真夜6 小时前
关于rngh手势与Slider组件手势与事件冲突解决问题记录
android·javascript·app
用户2018792831676 小时前
浅析Binder通信的三种调用方式
android
用户097 小时前
深入了解 Android 16KB内存页面
android·kotlin
火车叼位8 小时前
Android Studio与命令行Gradle表现不一致问题分析
android
前行的小黑炭10 小时前
【Android】 Context使用不当,存在内存泄漏,语言不生效等等
android·kotlin·app
前行的小黑炭11 小时前
【Android】CoordinatorLayout详解;实现一个交互动画的效果(上滑隐藏,下滑出现);附例子
android·kotlin·app
用户2018792831671 天前
Android黑夜白天模式切换原理分析
android