在 Android 上,我们可以抓取 hwui SKP 分析 View 的绘制情况。本文将对 SKP capture 保存图片的流程进行分析,并与读者探讨如何参考 SKP capture 开发一个将客制化渲染结果保存为图片的 debug 功能。
一、SKP Capture 流程简介
先简单介绍一下 hwui SKP capture 的整体流程。SKP capture 的主要流程在 renderFrame
中,首先会调用 tryCapture
初始化一个 SkCanvas*
指针,然后调用 renderLayersImpl
和 renderFrameImpl
进行绘制,最后调用 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
仅在 tryCapture
和 endCapture
两个函数中被调用。在 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());
需要传入两个参数 data
和 filename
,filename
不成问题,难点在如何拿到 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。