Android系统如何实现高效图像显示处理?

2026-05-27 13:461阅读0评论SEO问题
  • 内容介绍
  • 相关推荐

本文共计3440个文字,预计阅读时间需要14分钟。

Android系统如何实现高效图像显示处理?

一、前言在项目的开发中,为定位Android显示异常的原因,我们常关注以下因素:GPU渲染、GPU合成或HWC合成送显异常的问题。我们通常会先将图层的原始数据写入文件,然后通过RGB或YUV的软件工具查看这些数据。

一、前言

在项目的开发中,为了定位Android显示异常的原因:GPU渲染 or GPU合成 or HWC合成送显异常的问题。我们通常会把图层的原始数据写到文件,然后通过RGB或YUV的软件工具来查看这些原始的图像数据,从而确定问题发生的大体阶段。

本文就将介绍如何dump Android渲染和合成图层GraphicBuffer 或 buffer_handle_t/native_handle_t的原始数据到文件:

  • 如何 dump Android 渲染图层的原始数据;
  • 如何 dump Android GPU合成图层的原始数据;
  • 如何 dump Android HWC端的图层的原始数据;

注意:本篇的介绍是基于Android 12平台进行的,涉及源码请查看12的Source code。


二、Android 内置的截屏命令 screencap

Android系统已经内置了一个非常方便好用的截屏命令 screencap,执行命令后可以通过GPU合成的方式,把所有图层合成到一个 GraphicBuffer中,并最终处理保存为一张PNG图片。

先看看基本用法:执行screencap -h得到基本的使用说明

console:/ # screencap -h usage: screencap [-hp] [-d display-id] [FILENAME] -h: this message -p: save the file as a png. -d: specify the physical display ID to capture (default: 4629995328241972480) see "dumpsys SurfaceFlinger --display-id" for valid display IDs. If FILENAME ends with .png it will be saved as a png. If FILENAME is not given, the results will be printed to stdout. 通常我个人的使用方式,分两步:

第一步:截屏

adb shell screencap -p /data/test.png

第二步:下载到电脑端查看

adb pull /data/test.png

接下来就可以直接使用电脑上的图片查看工具打开这张图片了

再强调一点,重要的事情说三遍:无论layer实际显示时合成方式是CLIENT还是DEVICE

screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

本文作者@二的次方 2022-05-04 发布于自由互联


screencap的实现机制,有兴趣的同学可以去看源码,位置在:frameworks/base/cmds/screencap/screencap.cpp

最终会走到SurfaceFlinger::renderScreenImplLocked,然后进入到SkiaGLRenderEngine::drawLayers,把所有图层layers都画到目的GraphicBuffer中。screencap拿到这个GraphicBuffer后再压缩转码存为png图片。


三、Android GPU渲染图层数据的导出/dump保存

首先看导出数据到文件的核心方法dumpRawDataOfLayers2file,代码如下,很简单,不解释

static void dumpRawDataOfLayers2file(const sp<GraphicBuffer>& buffer) { ALOGE("%s [%d]", __FUNCTION__, __LINE__); static int sDumpCount = 0; if(buffer.get() == nullptr) return; /** 获取GraphicBuffer信息 */ uint32_t width = buffer->getWidth(); uint32_t height = buffer->getHeight(); uint32_t stride = buffer->getStride(); int32_t format = buffer->getPixelFormat(); uint32_t buffer_size = stride * height * bytesPerPixel(format); ALOGE("buffer info: width:%u, height:%u, stride:%u, format:%d, size:%u", width, height, stride, format, buffer_size); /** 打开要保存的文件 */ char layerName[100] = {0}; sprintf(layerName, "/data/buffer_layer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format)); ALOGD("save buffer's raw data to file : %s", layerName); FILE * pfile = nullptr; pfile = fopen(layerName,"w+"); if(pfile) { /** 获取GraphicBuffer的数据地址 */ void *vaddr = nullptr; status_t err = buffer->lock(GraphicBuffer::USAGE_SW_READ_OFTEN, &vaddr); if(err == NO_ERROR && vaddr != nullptr){ /** 写数据到文件 */ size_t result = 0; result = fwrite( (const void *)vaddr, (size_t)(buffer_size), 1, pfile); if(result > 0) { ALOGD("fwrite success!"); } else{ ALOGE("fwrite failed error %d", result); } } else{ ALOGE("lock buffer error!"); } fclose(pfile); buffer->unlock(); } }


那上面这个方法如何使用呢?答案是把代码放到需要导出数据的位置并调用dumpRawDataOfLayers2file就可以了,so easy !


要导出GPU渲染的每个图层layer的原始数据,可以加到SkiaGLRenderEngine::drawLayers的合适位置,如下所示:

注:Android 12之前的版本应该是放到GLESRenderEngine::drawLayers

[ /frameworks/native/libs/renderengine/skia/SkiaGLRenderEngine.cpp ] status_t SkiaGLRenderEngine::drawLayers(const DisplaySettings& display, const std::vector<const LayerSettings*>& layers, const std::shared_ptr<ExternalTexture>& buffer, const bool /*useFramebufferCache*/, base::unique_fd&& bufferFence, base::unique_fd* drawFence) { ATRACE_NAME("SkiaGL::drawLayers"); std::lock_guard<std::mutex> lock(mRenderingMutex); if (layers.empty()) { ALOGV("Drawing empty layer stack"); return NO_ERROR; } if (buffer == nullptr) { ALOGE("No output buffer provided. Aborting GPU composition."); return BAD_VALUE; } =================================================================================== /** dump每一个layer的图像数据 */ char dump_layers[PROPERTY_VALUE_MAX]; property_get("dump.layers.debug", dump_layers, "false"); if(!strcmp(dump_layers, "true")) { for (auto const layer : layers) { if (layer->source.buffer.buffer != nullptr) { dumpRawDataOfLayers2file(layer->source.buffer.buffer->getBuffer()); } } property_set("dump.layers.debug", "false"); // 根据需要设置是连续dump还是一次 } =================================================================================== ...... }

注:编译不过时,可能需要补必要的头文件,比如 #include <cutils/properties.h>

上面给出的这段代码示例,重新编译surfaceflinger并更新到测试板中,然后设置属性值:setprop dump.layers.debug true在UI发生变化时就会导出数据到文件了。

本文作者@二的次方 2022-05-04 发布于自由互联

如下是我导出的一次结果

图层一:buffer_layer_0_frame_1920_1080_4.bin,使用7yuv这个工具查看

图层二:buffer_layer_1_frame_1184_976_4.bin,使用7yuv这个工具查看

图层三:buffer_layer_2_frame_540_161_4.bin,使用7yuv这个工具查看

到这里你已经掌握了如何导出每一个采用GPU渲染的图层的数据的方式,那如何导出GPU合成后的图像数据呢?下面就告诉你方法

四、Android GPU合层图层数据的导出/dump保存

要导出GPU合成后的图层数据,可以在RenderSurface::queueBufferFramebufferSurface::nextBuffer中添加导出数据的逻辑。

导出数据到文件可以采用前面一节提供的dumpRawDataOfLayers2file方法,不过这个方法是通过GraphicBuffer::lock方式来获取数据地址的,有时候会发现lock可能不被允许导致数据无法读取。

这里我们再提供另一种方式,直接根据native_handle_t中的shared fd,通过mmap的方式获取数据地址,给出通用方法dumpGraphicRawData2file

static void dumpGraphicRawData2file(const native_handle_t* bufferHandle, uint32_t width, uint32_t height, uint32_t stride, int32_t format) { ALOGE("%s [%d]", __FUNCTION__, __LINE__); static int sDumpCount = 0; if(bufferHandle != nullptr) { int shareFd = bufferHandle->data[0]; unsigned char *srcAddr = NULL; uint32_t buffer_size = stride * height * bytesPerPixel(format); srcAddr = (unsigned char *)mmap(NULL, buffer_size, PROT_READ, MAP_SHARED, shareFd, 0);// 获取数据地址 char dumpPath[100] = ""; snprintf(dumpPath, sizeof(dumpPath), "/data/buffer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format)); int dumpFd = open(dumpPath, O_WRONLY|O_CREAT|O_TRUNC, 0644); if(dumpFd >= 0 && srcAddr != NULL) { write(dumpFd, srcAddr, buffer_size);// 写数据到文件 close(dumpFd); } munmap((void*)srcAddr, buffer_size); } }

要导出GPU合成后的图层数据,我下面给出的示例是在RenderSurface::queueBuffer中添加逻辑的,如下:

[/frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp] void RenderSurface::queueBuffer(base::unique_fd readyFence) { auto& state = mDisplay.getState(); if (state.usesClientComposition || state.flipClientTarget) { // hasFlipClientTargetRequest could return true even if we haven't // dequeued a buffer before. Try dequeueing one if we don't have a // buffer ready. if (mTexture == nullptr) { ALOGI("Attempting to queue a client composited buffer without one " "previously dequeued for display [%s]. Attempting to dequeue " "a scratch buffer now", mDisplay.getName().c_str()); // We shouldn't deadlock here, since mTexture == nullptr only // after a successful call to queueBuffer, or if dequeueBuffer has // never been called. base::unique_fd unused; dequeueBuffer(&unused); } if (mTexture == nullptr) { ALOGE("No buffer is ready for display [%s]", mDisplay.getName().c_str()); } else { status_t result = mNativeWindow->queueBuffer(mNativeWindow.get(), mTexture->getBuffer()->getNativeBuffer(), dup(readyFence)); /** 导出GPU合成后的数据 */ char dump_layers[PROPERTY_VALUE_MAX]; property_get("dump.layers.debug", dump_layers, "false"); if(!strcmp(dump_layers, "true")) { const sp<GraphicBuffer> buffer = mTexture->getBuffer(); uint32_t width = buffer->getWidth(); uint32_t height = buffer->getHeight(); uint32_t stride = buffer->getStride(); int32_t format = buffer->getPixelFormat(); dumpGraphicRawData2file(mTexture->getBuffer()->getNativeBuffer()->handle, width, height, stride, format); //dumpRawDataOfLayers2file(mTexture->getBuffer()); property_set("dump.layers.debug", "false"); } ...... } }

注:编译不过时,可能需要补必要的头文件,比如 #include <cutils/properties.h>

上面给出的这段代码示例,重新编译surfaceflinger并更新到测试板中,然后设置属性值:setprop dump.layers.debug true在UI发生变化时就会导出数据到文件了。

如下是我导出的一次结果,GPU合成后的结果


和前面一节,我们导出来的每一个图层对比,三个图层合成后的结果,有木有!


五、不走GPU合成的图层数据如何导出/dump保存呢?

如果一个图层不是通过GPU合成,那前面3、4节的方法是不能把它的数据导出的。那应该怎样处理呢?

我们先来看一个例子:我打开腾讯TV,小窗口播放视频,然后采用第4节的方法导出GPU合成后的图层数据,如下

Android系统如何实现高效图像显示处理?

我们对比下screencap的截屏的效果,因为screencap会使用GPU把所有图层都绘制到一张图片上,也就是我屏幕上看到什么,截屏就得到什么

可以通过dumpsys SurfaceFlinger看到信息:

SurfaceView[com.ktcp.video/com.ktcp.[...]ImmerseDetailCoverActivity](BLAST)#0 这个图层Layer合成方式是DEVICE,单纯导出GPU合成的图层是没有它的。

Display 4629995328241972480 HWC layers: --------------------------------------------------------------------------------------------------------------------------------------------------------------- Layer name Z | Window Type | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB) | Frame Rate (Explicit) (Seamlessness) [Focused] --------------------------------------------------------------------------------------------------------------------------------------------------------------- SurfaceView[com.ktcp.video/com.ktcp.[...]ImmerseDetailCoverActivity](BLAST)#0 rel 0 | 0 | DEVICE | 0 | 440 2 1920 834 | 0.0 0.0 1280.0 720.0 | [*] --------------------------------------------------------------------------------------------------------------------------------------------------------------- com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity#0 rel 0 | 1 | CLIENT | 0 | 0 0 1920 1080 | 0.0 0.0 1920.0 1080.0 | [*] -------------------------------------------------------------------------------------------------------------------------------------------------------------

那问题来了,如果我想要导出小窗口视频的数据那应该如何做的?

方法有很多,可以在SurfaceFlinger端,也可以在HWC端,

因为视频解码出来一般是YUV格式,所以需要对前面的导出数据的方法做点修改,如下dumpYUVRawData2file提供了一个参考可以导出YUV格式的图层数据

[ /frameworks/native/services/surfaceflinger/BufferStateLayer.cpp ] static void dumpYUVRawData2file(const sp<GraphicBuffer>& buffer) { ALOGE("%s [%d]", __FUNCTION__, __LINE__); static int sDumpCount = 0; if(buffer.get() == nullptr) return; /** 获取GraphicBuffer信息 */ uint32_t width = buffer->getWidth(); uint32_t height = buffer->getHeight(); uint32_t stride = buffer->getStride(); int32_t format = buffer->getPixelFormat(); ALOGE("buffer info: width:%u, height:%u, stride:%u, format:%d", width, height, stride, format); /** 打开要保存的文件 */ char layerName[100] = {0}; sprintf(layerName, "/data/buffer_layer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format)); ALOGD("save buffer's raw data to file : %s", layerName); FILE * pfile = nullptr; pfile = fopen(layerName, "w+"); if(pfile) { /** 获取GraphicBuffer的数据地址 */ android_ycbcr ycbcr = {0}; /** For HAL_PIXEL_FORMAT_YCbCr_420_888 */ status_t err = buffer->lockYCbCr(GraphicBuffer::USAGE_SW_READ_OFTEN, &ycbcr); ALOGD("y=%p, cb=%p, cr=%p, ystride=%u, cstride=%u", ycbcr.y, ycbcr.cb, ycbcr.cr, ycbcr.ystride, ycbcr.cstride); if(err == NO_ERROR) { /** 写数据到文件 */ size_t result = 0; result = fwrite( (const void *)ycbcr.y, (size_t)(ycbcr.ystride*height), 1, pfile); result = fwrite( (const void *)ycbcr.cb, (size_t)(ycbcr.cstride*height/2), 1, pfile); if(result > 0) { ALOGD("fwrite success !"); } else{ ALOGE("fwrite failed error %u", result); } } else{ ALOGE("lock buffer error!"); } fclose(pfile); buffer->unlock(); } }

如果要导出某一个视频图层的数据,dumpYUVRawData2file加到合适的位置,我们这里的示例是加到BufferStateLayer::setBuffer,修改如下:

[ /frameworks/native/services/surfaceflinger/BufferStateLayer.cpp] bool BufferStateLayer::setBuffer(const std::shared_ptr<renderengine::ExternalTexture>& buffer, const sp<Fence>& acquireFence, nsecs_t postTime, nsecs_t desiredPresentTime, bool isAutoTimestamp, const client_cache_t& clientCacheId, uint64_t frameNumber, std::optional<nsecs_t> dequeueTime, const FrameTimelineInfo& info, const sp<ITransactionCompletedListener>& releaseBufferListener) { ATRACE_CALL(); // 判断是否是我们感兴趣的图层,名字中含有关键字 if(getName().find("SurfaceView")!=std::string::npos && getName().find("com.ktcp.video")!=std::string::npos && getName().find("BLAST")!=std::string::npos) { /** 导出图层数据 */ char dump_layers[PROPERTY_VALUE_MAX]; property_get("dump.layers.debug", dump_layers, "false"); if(!strcmp(dump_layers, "true")) { dumpYUVRawData2file(buffer->getBuffer()); // 我们已经确定是YUV的视频了,所以调用dumpYUVRawData2file property_set("dump.layers.debug", "false"); } } ...... }

这样重编并替换surfaceflinger,再次进到腾讯TV播放视频,设置属性setprop dump.layers.debug true就可以导出数据了,如下我导出的


以上,只是提供了一些导出dump图层数据的参考,要导出特定的图层或特定阶段合成的结果,可以在不同的位置,添加dump逻辑,具体问题,具体分析。


本文作者@二的次方 2022-05-04 发布于自由互联

还有一点非常重要,如何获取图层的一些信息,以便我们正确的导出和查看数据,关于dump GraphicBuffer获取的信息大小,格式,以及存储计算规则是否正确可以通过dumpsys SurfaceFlinger进行查看

比如下面这段信息,定位到GraphicBufferAllocator buffers:的位置,可以看到 我在播放腾讯TV视频时的buffer的信息, 

planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000
Cb/Cr: w/h:500x168, stride:500 bytes, size:78000

GraphicBufferAllocator buffers: Handle | Size | W (Stride) x H | Layers | Format | Usage | Requestor 0xed7801e0 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface 0xed7817a0 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface 0xed783840 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface Total allocated by GraphicBufferAllocator (estimate): 24300.00 KB Imported gralloc buffers: + name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:983547510872, size:1.4e+03KiB, w/h:1280x720, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000 Cb/Cr: w/h:500x168, stride:500 bytes, size:78000 + name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:e500000057, size:1.4e+03KiB, w/h:500x2d0, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000 Cb/Cr: w/h:500x168, stride:500 bytes, size:78000 + name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:e500000054, size:1.4e+03KiB, w/h:500x2d0, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000 Cb/Cr: w/h:500x168, stride:500 bytes, size:78000

六、Android HWC端图层数据的导出/dump保存

HWC中也可以去导出图层的数据用于debug,基本方法类似我们前面讲到的

比如

SetClientTarget方法中可以导出GPU合成的结果

SetLayerBuffer方法中可以导出某一图层的数据

这些方法中都持有一个buffer_handle_t,它指向一块GraphicBuffer,可以使用我们前面讲到的dumpGraphicRawData2file来导出数据。具体的就不详细展开了。

七、总结

至此Android dump渲染和合成图层GraphicBuffer阶段整个就完成了,以上讲到的方法仅作参考,实际工作中还要具体问题,具体分析。灵活运用各种技巧


今日五四青年节,吾辈青年当立鸿鹄之志,抱璞守正,坚定理念信仰!


推荐阅读:

Android 12(S) 图像显示系统 - 开篇

心有猛虎,细嗅蔷薇,生活就该无惧无悔

本文共计3440个文字,预计阅读时间需要14分钟。

Android系统如何实现高效图像显示处理?

一、前言在项目的开发中,为定位Android显示异常的原因,我们常关注以下因素:GPU渲染、GPU合成或HWC合成送显异常的问题。我们通常会先将图层的原始数据写入文件,然后通过RGB或YUV的软件工具查看这些数据。

一、前言

在项目的开发中,为了定位Android显示异常的原因:GPU渲染 or GPU合成 or HWC合成送显异常的问题。我们通常会把图层的原始数据写到文件,然后通过RGB或YUV的软件工具来查看这些原始的图像数据,从而确定问题发生的大体阶段。

本文就将介绍如何dump Android渲染和合成图层GraphicBuffer 或 buffer_handle_t/native_handle_t的原始数据到文件:

  • 如何 dump Android 渲染图层的原始数据;
  • 如何 dump Android GPU合成图层的原始数据;
  • 如何 dump Android HWC端的图层的原始数据;

注意:本篇的介绍是基于Android 12平台进行的,涉及源码请查看12的Source code。


二、Android 内置的截屏命令 screencap

Android系统已经内置了一个非常方便好用的截屏命令 screencap,执行命令后可以通过GPU合成的方式,把所有图层合成到一个 GraphicBuffer中,并最终处理保存为一张PNG图片。

先看看基本用法:执行screencap -h得到基本的使用说明

console:/ # screencap -h usage: screencap [-hp] [-d display-id] [FILENAME] -h: this message -p: save the file as a png. -d: specify the physical display ID to capture (default: 4629995328241972480) see "dumpsys SurfaceFlinger --display-id" for valid display IDs. If FILENAME ends with .png it will be saved as a png. If FILENAME is not given, the results will be printed to stdout. 通常我个人的使用方式,分两步:

第一步:截屏

adb shell screencap -p /data/test.png

第二步:下载到电脑端查看

adb pull /data/test.png

接下来就可以直接使用电脑上的图片查看工具打开这张图片了

再强调一点,重要的事情说三遍:无论layer实际显示时合成方式是CLIENT还是DEVICE

screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。

本文作者@二的次方 2022-05-04 发布于自由互联


screencap的实现机制,有兴趣的同学可以去看源码,位置在:frameworks/base/cmds/screencap/screencap.cpp

最终会走到SurfaceFlinger::renderScreenImplLocked,然后进入到SkiaGLRenderEngine::drawLayers,把所有图层layers都画到目的GraphicBuffer中。screencap拿到这个GraphicBuffer后再压缩转码存为png图片。


三、Android GPU渲染图层数据的导出/dump保存

首先看导出数据到文件的核心方法dumpRawDataOfLayers2file,代码如下,很简单,不解释

static void dumpRawDataOfLayers2file(const sp<GraphicBuffer>& buffer) { ALOGE("%s [%d]", __FUNCTION__, __LINE__); static int sDumpCount = 0; if(buffer.get() == nullptr) return; /** 获取GraphicBuffer信息 */ uint32_t width = buffer->getWidth(); uint32_t height = buffer->getHeight(); uint32_t stride = buffer->getStride(); int32_t format = buffer->getPixelFormat(); uint32_t buffer_size = stride * height * bytesPerPixel(format); ALOGE("buffer info: width:%u, height:%u, stride:%u, format:%d, size:%u", width, height, stride, format, buffer_size); /** 打开要保存的文件 */ char layerName[100] = {0}; sprintf(layerName, "/data/buffer_layer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format)); ALOGD("save buffer's raw data to file : %s", layerName); FILE * pfile = nullptr; pfile = fopen(layerName,"w+"); if(pfile) { /** 获取GraphicBuffer的数据地址 */ void *vaddr = nullptr; status_t err = buffer->lock(GraphicBuffer::USAGE_SW_READ_OFTEN, &vaddr); if(err == NO_ERROR && vaddr != nullptr){ /** 写数据到文件 */ size_t result = 0; result = fwrite( (const void *)vaddr, (size_t)(buffer_size), 1, pfile); if(result > 0) { ALOGD("fwrite success!"); } else{ ALOGE("fwrite failed error %d", result); } } else{ ALOGE("lock buffer error!"); } fclose(pfile); buffer->unlock(); } }


那上面这个方法如何使用呢?答案是把代码放到需要导出数据的位置并调用dumpRawDataOfLayers2file就可以了,so easy !


要导出GPU渲染的每个图层layer的原始数据,可以加到SkiaGLRenderEngine::drawLayers的合适位置,如下所示:

注:Android 12之前的版本应该是放到GLESRenderEngine::drawLayers

[ /frameworks/native/libs/renderengine/skia/SkiaGLRenderEngine.cpp ] status_t SkiaGLRenderEngine::drawLayers(const DisplaySettings& display, const std::vector<const LayerSettings*>& layers, const std::shared_ptr<ExternalTexture>& buffer, const bool /*useFramebufferCache*/, base::unique_fd&& bufferFence, base::unique_fd* drawFence) { ATRACE_NAME("SkiaGL::drawLayers"); std::lock_guard<std::mutex> lock(mRenderingMutex); if (layers.empty()) { ALOGV("Drawing empty layer stack"); return NO_ERROR; } if (buffer == nullptr) { ALOGE("No output buffer provided. Aborting GPU composition."); return BAD_VALUE; } =================================================================================== /** dump每一个layer的图像数据 */ char dump_layers[PROPERTY_VALUE_MAX]; property_get("dump.layers.debug", dump_layers, "false"); if(!strcmp(dump_layers, "true")) { for (auto const layer : layers) { if (layer->source.buffer.buffer != nullptr) { dumpRawDataOfLayers2file(layer->source.buffer.buffer->getBuffer()); } } property_set("dump.layers.debug", "false"); // 根据需要设置是连续dump还是一次 } =================================================================================== ...... }

注:编译不过时,可能需要补必要的头文件,比如 #include <cutils/properties.h>

上面给出的这段代码示例,重新编译surfaceflinger并更新到测试板中,然后设置属性值:setprop dump.layers.debug true在UI发生变化时就会导出数据到文件了。

本文作者@二的次方 2022-05-04 发布于自由互联

如下是我导出的一次结果

图层一:buffer_layer_0_frame_1920_1080_4.bin,使用7yuv这个工具查看

图层二:buffer_layer_1_frame_1184_976_4.bin,使用7yuv这个工具查看

图层三:buffer_layer_2_frame_540_161_4.bin,使用7yuv这个工具查看

到这里你已经掌握了如何导出每一个采用GPU渲染的图层的数据的方式,那如何导出GPU合成后的图像数据呢?下面就告诉你方法

四、Android GPU合层图层数据的导出/dump保存

要导出GPU合成后的图层数据,可以在RenderSurface::queueBufferFramebufferSurface::nextBuffer中添加导出数据的逻辑。

导出数据到文件可以采用前面一节提供的dumpRawDataOfLayers2file方法,不过这个方法是通过GraphicBuffer::lock方式来获取数据地址的,有时候会发现lock可能不被允许导致数据无法读取。

这里我们再提供另一种方式,直接根据native_handle_t中的shared fd,通过mmap的方式获取数据地址,给出通用方法dumpGraphicRawData2file

static void dumpGraphicRawData2file(const native_handle_t* bufferHandle, uint32_t width, uint32_t height, uint32_t stride, int32_t format) { ALOGE("%s [%d]", __FUNCTION__, __LINE__); static int sDumpCount = 0; if(bufferHandle != nullptr) { int shareFd = bufferHandle->data[0]; unsigned char *srcAddr = NULL; uint32_t buffer_size = stride * height * bytesPerPixel(format); srcAddr = (unsigned char *)mmap(NULL, buffer_size, PROT_READ, MAP_SHARED, shareFd, 0);// 获取数据地址 char dumpPath[100] = ""; snprintf(dumpPath, sizeof(dumpPath), "/data/buffer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format)); int dumpFd = open(dumpPath, O_WRONLY|O_CREAT|O_TRUNC, 0644); if(dumpFd >= 0 && srcAddr != NULL) { write(dumpFd, srcAddr, buffer_size);// 写数据到文件 close(dumpFd); } munmap((void*)srcAddr, buffer_size); } }

要导出GPU合成后的图层数据,我下面给出的示例是在RenderSurface::queueBuffer中添加逻辑的,如下:

[/frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp] void RenderSurface::queueBuffer(base::unique_fd readyFence) { auto& state = mDisplay.getState(); if (state.usesClientComposition || state.flipClientTarget) { // hasFlipClientTargetRequest could return true even if we haven't // dequeued a buffer before. Try dequeueing one if we don't have a // buffer ready. if (mTexture == nullptr) { ALOGI("Attempting to queue a client composited buffer without one " "previously dequeued for display [%s]. Attempting to dequeue " "a scratch buffer now", mDisplay.getName().c_str()); // We shouldn't deadlock here, since mTexture == nullptr only // after a successful call to queueBuffer, or if dequeueBuffer has // never been called. base::unique_fd unused; dequeueBuffer(&unused); } if (mTexture == nullptr) { ALOGE("No buffer is ready for display [%s]", mDisplay.getName().c_str()); } else { status_t result = mNativeWindow->queueBuffer(mNativeWindow.get(), mTexture->getBuffer()->getNativeBuffer(), dup(readyFence)); /** 导出GPU合成后的数据 */ char dump_layers[PROPERTY_VALUE_MAX]; property_get("dump.layers.debug", dump_layers, "false"); if(!strcmp(dump_layers, "true")) { const sp<GraphicBuffer> buffer = mTexture->getBuffer(); uint32_t width = buffer->getWidth(); uint32_t height = buffer->getHeight(); uint32_t stride = buffer->getStride(); int32_t format = buffer->getPixelFormat(); dumpGraphicRawData2file(mTexture->getBuffer()->getNativeBuffer()->handle, width, height, stride, format); //dumpRawDataOfLayers2file(mTexture->getBuffer()); property_set("dump.layers.debug", "false"); } ...... } }

注:编译不过时,可能需要补必要的头文件,比如 #include <cutils/properties.h>

上面给出的这段代码示例,重新编译surfaceflinger并更新到测试板中,然后设置属性值:setprop dump.layers.debug true在UI发生变化时就会导出数据到文件了。

如下是我导出的一次结果,GPU合成后的结果


和前面一节,我们导出来的每一个图层对比,三个图层合成后的结果,有木有!


五、不走GPU合成的图层数据如何导出/dump保存呢?

如果一个图层不是通过GPU合成,那前面3、4节的方法是不能把它的数据导出的。那应该怎样处理呢?

我们先来看一个例子:我打开腾讯TV,小窗口播放视频,然后采用第4节的方法导出GPU合成后的图层数据,如下

Android系统如何实现高效图像显示处理?

我们对比下screencap的截屏的效果,因为screencap会使用GPU把所有图层都绘制到一张图片上,也就是我屏幕上看到什么,截屏就得到什么

可以通过dumpsys SurfaceFlinger看到信息:

SurfaceView[com.ktcp.video/com.ktcp.[...]ImmerseDetailCoverActivity](BLAST)#0 这个图层Layer合成方式是DEVICE,单纯导出GPU合成的图层是没有它的。

Display 4629995328241972480 HWC layers: --------------------------------------------------------------------------------------------------------------------------------------------------------------- Layer name Z | Window Type | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB) | Frame Rate (Explicit) (Seamlessness) [Focused] --------------------------------------------------------------------------------------------------------------------------------------------------------------- SurfaceView[com.ktcp.video/com.ktcp.[...]ImmerseDetailCoverActivity](BLAST)#0 rel 0 | 0 | DEVICE | 0 | 440 2 1920 834 | 0.0 0.0 1280.0 720.0 | [*] --------------------------------------------------------------------------------------------------------------------------------------------------------------- com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity#0 rel 0 | 1 | CLIENT | 0 | 0 0 1920 1080 | 0.0 0.0 1920.0 1080.0 | [*] -------------------------------------------------------------------------------------------------------------------------------------------------------------

那问题来了,如果我想要导出小窗口视频的数据那应该如何做的?

方法有很多,可以在SurfaceFlinger端,也可以在HWC端,

因为视频解码出来一般是YUV格式,所以需要对前面的导出数据的方法做点修改,如下dumpYUVRawData2file提供了一个参考可以导出YUV格式的图层数据

[ /frameworks/native/services/surfaceflinger/BufferStateLayer.cpp ] static void dumpYUVRawData2file(const sp<GraphicBuffer>& buffer) { ALOGE("%s [%d]", __FUNCTION__, __LINE__); static int sDumpCount = 0; if(buffer.get() == nullptr) return; /** 获取GraphicBuffer信息 */ uint32_t width = buffer->getWidth(); uint32_t height = buffer->getHeight(); uint32_t stride = buffer->getStride(); int32_t format = buffer->getPixelFormat(); ALOGE("buffer info: width:%u, height:%u, stride:%u, format:%d", width, height, stride, format); /** 打开要保存的文件 */ char layerName[100] = {0}; sprintf(layerName, "/data/buffer_layer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format)); ALOGD("save buffer's raw data to file : %s", layerName); FILE * pfile = nullptr; pfile = fopen(layerName, "w+"); if(pfile) { /** 获取GraphicBuffer的数据地址 */ android_ycbcr ycbcr = {0}; /** For HAL_PIXEL_FORMAT_YCbCr_420_888 */ status_t err = buffer->lockYCbCr(GraphicBuffer::USAGE_SW_READ_OFTEN, &ycbcr); ALOGD("y=%p, cb=%p, cr=%p, ystride=%u, cstride=%u", ycbcr.y, ycbcr.cb, ycbcr.cr, ycbcr.ystride, ycbcr.cstride); if(err == NO_ERROR) { /** 写数据到文件 */ size_t result = 0; result = fwrite( (const void *)ycbcr.y, (size_t)(ycbcr.ystride*height), 1, pfile); result = fwrite( (const void *)ycbcr.cb, (size_t)(ycbcr.cstride*height/2), 1, pfile); if(result > 0) { ALOGD("fwrite success !"); } else{ ALOGE("fwrite failed error %u", result); } } else{ ALOGE("lock buffer error!"); } fclose(pfile); buffer->unlock(); } }

如果要导出某一个视频图层的数据,dumpYUVRawData2file加到合适的位置,我们这里的示例是加到BufferStateLayer::setBuffer,修改如下:

[ /frameworks/native/services/surfaceflinger/BufferStateLayer.cpp] bool BufferStateLayer::setBuffer(const std::shared_ptr<renderengine::ExternalTexture>& buffer, const sp<Fence>& acquireFence, nsecs_t postTime, nsecs_t desiredPresentTime, bool isAutoTimestamp, const client_cache_t& clientCacheId, uint64_t frameNumber, std::optional<nsecs_t> dequeueTime, const FrameTimelineInfo& info, const sp<ITransactionCompletedListener>& releaseBufferListener) { ATRACE_CALL(); // 判断是否是我们感兴趣的图层,名字中含有关键字 if(getName().find("SurfaceView")!=std::string::npos && getName().find("com.ktcp.video")!=std::string::npos && getName().find("BLAST")!=std::string::npos) { /** 导出图层数据 */ char dump_layers[PROPERTY_VALUE_MAX]; property_get("dump.layers.debug", dump_layers, "false"); if(!strcmp(dump_layers, "true")) { dumpYUVRawData2file(buffer->getBuffer()); // 我们已经确定是YUV的视频了,所以调用dumpYUVRawData2file property_set("dump.layers.debug", "false"); } } ...... }

这样重编并替换surfaceflinger,再次进到腾讯TV播放视频,设置属性setprop dump.layers.debug true就可以导出数据了,如下我导出的


以上,只是提供了一些导出dump图层数据的参考,要导出特定的图层或特定阶段合成的结果,可以在不同的位置,添加dump逻辑,具体问题,具体分析。


本文作者@二的次方 2022-05-04 发布于自由互联

还有一点非常重要,如何获取图层的一些信息,以便我们正确的导出和查看数据,关于dump GraphicBuffer获取的信息大小,格式,以及存储计算规则是否正确可以通过dumpsys SurfaceFlinger进行查看

比如下面这段信息,定位到GraphicBufferAllocator buffers:的位置,可以看到 我在播放腾讯TV视频时的buffer的信息, 

planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000
Cb/Cr: w/h:500x168, stride:500 bytes, size:78000

GraphicBufferAllocator buffers: Handle | Size | W (Stride) x H | Layers | Format | Usage | Requestor 0xed7801e0 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface 0xed7817a0 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface 0xed783840 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface Total allocated by GraphicBufferAllocator (estimate): 24300.00 KB Imported gralloc buffers: + name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:983547510872, size:1.4e+03KiB, w/h:1280x720, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000 Cb/Cr: w/h:500x168, stride:500 bytes, size:78000 + name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:e500000057, size:1.4e+03KiB, w/h:500x2d0, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000 Cb/Cr: w/h:500x168, stride:500 bytes, size:78000 + name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:e500000054, size:1.4e+03KiB, w/h:500x2d0, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000 Cb/Cr: w/h:500x168, stride:500 bytes, size:78000

六、Android HWC端图层数据的导出/dump保存

HWC中也可以去导出图层的数据用于debug,基本方法类似我们前面讲到的

比如

SetClientTarget方法中可以导出GPU合成的结果

SetLayerBuffer方法中可以导出某一图层的数据

这些方法中都持有一个buffer_handle_t,它指向一块GraphicBuffer,可以使用我们前面讲到的dumpGraphicRawData2file来导出数据。具体的就不详细展开了。

七、总结

至此Android dump渲染和合成图层GraphicBuffer阶段整个就完成了,以上讲到的方法仅作参考,实际工作中还要具体问题,具体分析。灵活运用各种技巧


今日五四青年节,吾辈青年当立鸿鹄之志,抱璞守正,坚定理念信仰!


推荐阅读:

Android 12(S) 图像显示系统 - 开篇

心有猛虎,细嗅蔷薇,生活就该无惧无悔