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。

相关推荐
江上清风山间明月2 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
子非衣6 小时前
MySQL修改JSON格式数据示例
android·mysql·json
openinstall全渠道统计9 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫9 小时前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫9 小时前
一句话说透Android里面的查找服务
android
双鱼大猫9 小时前
一句话说透Android里面的SystemServer进程的作用
android
双鱼大猫9 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫10 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫11 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫11 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android