1 前言

为了更深刻的理解Android图形系统抽象的概念和BufferQueue的工作机制,这篇文章我们将从Native Level入手,基于Android图形系统API写作一个简单的图形处理小程序。透过这个小程序我们将学习如何使用Native API创建Surface,如何请求图形缓冲区,如何向图形缓冲区中写入数据等知识。Talk is cheap, show me the code。让我们马上开始吧!

注:本系列文章的分析及代码均基于Android 12(S) Source Code,可参考:http://aospxref.com/  或 http://aosp.opersys.com/

2 程序源码简介

  • 源码下载

地址:https://github.com/yrzroger/NativeSFDemo

注:可以git命令下载(比如git clone git@github.com:yrzroger/NativeSFDemo.git)或直接Download ZIP解压后使用

  • 源码编译

本demo程序是基于Android源码编译环境开发的,所以需要放到Android源码目录下编译。

将上一步中下载的的源码放到Android源码的合适目录下,然后执行mm进行编译,得到可执行档 NativeSFDemo

  • 源码运行

将可执行档NativeSFDemo放到目标测试平台/system/bin下(比如:adb push NativeSFDemo /system/bin/)

然后执行 adb shell NativeSFDemo

  • 效果展示

程序中去绘制单色背景: 红色->绿色->蓝色背景交替展示,如下图所示:

至此你已经收获一个可以供后续学习研究的demo小程序了 !!!


Tips:

Android源码是一个宝藏,即提供了丰富多彩的APIs供开发者使用,又可以在其中搜索到很多有价值的APIs使用实例。本文中提供的演示Demo亦是基于源码中的参考来完成的。

我把参考位置列于此:

参考1:/frameworks/av/media/libstagefright/SurfaceUtils.cpp

参考2:/frameworks/native/libs/gui/tests/CpuConsumer_test.cpp


3 程序源码分析

在显示子系统中,Surface 是一个接口,供生产者与消费者交换缓冲区。通过Surface我们就能向BufferQueue请求Buffer,并和Android Native窗口系统建立连接。本文展示的demo就是基于Surface建立起来的。
 
  • 封装类NativeSurfaceWrapper

NativeSurfaceWrapper是对Surface的一层封装,用于获取屏幕参数并创建与配置Surface属性。

首先看到头文件中该类的定义:

class NativeSurfaceWrapper : public RefBase {
public:
NativeSurfaceWrapper(const String8& name);
virtual ~NativeSurfaceWrapper() {} virtual void onFirstRef(); // Retrieves a handle to the window.
sp<ANativeWindow> getSurface() const; int width() { return mWidth; }
int height() { return mHeight; } private:
DISALLOW_COPY_AND_ASSIGN(NativeSurfaceWrapper);
ui::Size limitSurfaceSize(int width, int height) const; sp<SurfaceControl> mSurfaceControl;
int mWidth;
int mHeight;
String8 mName;
};
 
NativeSurfaceWrapper继承自Refbase,这样就可以使用智能指针sp,wp来管理其对象,避免内存泄漏。

同时可以重写onFirstRef方法,在创建NativeSurfaceWrapper对象第一次被引用时调用onFirstRef做一些初始化操作。

下面是onFirstRef的定义:

void NativeSurfaceWrapper::onFirstRef() {
sp<SurfaceComposerClient> surfaceComposerClient = new SurfaceComposerClient;
status_t err = surfaceComposerClient->initCheck();
if (err != NO_ERROR) {
ALOGD("SurfaceComposerClient::initCheck error: %#x\n", err);
return;
} // Get main display parameters.
sp<IBinder> displayToken = SurfaceComposerClient::getInternalDisplayToken();
if (displayToken == nullptr)
return; ui::DisplayMode displayMode;
const status_t error =
SurfaceComposerClient::getActiveDisplayMode(displayToken, &displayMode);
if (error != NO_ERROR)
return; ui::Size resolution = displayMode.resolution;
resolution = limitSurfaceSize(resolution.width, resolution.height);
// create the native surface
sp<SurfaceControl> surfaceControl = surfaceComposerClient->createSurface(mName, resolution.getWidth(),
resolution.getHeight(), PIXEL_FORMAT_RGBA_8888,
ISurfaceComposerClient::eFXSurfaceBufferState,
/*parent*/ nullptr); SurfaceComposerClient::Transaction{}
.setLayer(surfaceControl, std::numeric_limits<int32_t>::max())
.show(surfaceControl)
.apply(); mSurfaceControl = surfaceControl;
mWidth = resolution.getWidth();
mHeight = resolution.getHeight();
}
 

onFirstRef中完成主要工作:

1. 创建一个SurfaceComposerClient对象,这是SurfaceFlinger的Client端,它将建立和SurfaceFlinger服务的通信;

2. 获取屏幕参数,SurfaceComposerClient::getActiveDisplayMode获取当前的DisplayMode,其中可以得到resolution信息;

3. 创建Surface & SurfaceControl,createSurface方法会通过Binder通信机制一直呼叫到SurfaceFlinger,SurfaceFlinger会进行创建Layer等操作;

4. createSurface时会设置width,height,format等信息;

5. setLayer,设置窗口的z-order,SurfaceFlinger根据z-Oder决定窗口的可见性及可见大小;

6. show,让当前窗口可见;

7. apply,使透过Transaction进行的设置生效,属性信息传给SurfaceFlinger;
 

Tips:

创建Surface的过程会涉及到与SurfaceFlinger的互动,SurfaceFlinger是一个系统级的服务,负责创建/管理/合成Surface对应的Layer,这部分我们本文暂不展开,之后文章中会陆续讲解。


 
limitSurfaceSize方法
该方法的作用是将width和height限制在设备GPU支持的范围内。
 
ui::Size NativeSurfaceWrapper::limitSurfaceSize(int width, int height) const {
ui::Size limited(width, height);
bool wasLimited = false;
const float aspectRatio = float(width) / float(height); int maxWidth = android::base::GetIntProperty("ro.surface_flinger.max_graphics_width", 0);
int maxHeight = android::base::GetIntProperty("ro.surface_flinger.max_graphics_height", 0); if (maxWidth != 0 && width > maxWidth) {
limited.height = maxWidth / aspectRatio;
limited.width = maxWidth;
wasLimited = true;
}
if (maxHeight != 0 && limited.height > maxHeight) {
limited.height = maxHeight;
limited.width = maxHeight * aspectRatio;
wasLimited = true;
}
SLOGV_IF(wasLimited, "Surface size has been limited to [%dx%d] from [%dx%d]",
limited.width, limited.height, width, height);
return limited;
}

该方法会将屏幕的width/height和max_graphics_width/max_graphics_height进行比较,取较小者作为创建Surface的参数。

这一点也是Android 12引入的一个新特性。getActiveDisplayMode获取到的是屏幕的真实分辨率(real display resolution),但GPU可能不支持高分辨率的UI合成,所以必须对framebuffer size做出限制。

比如设备可以4K分辨率进行video的解码和渲染,但由于硬件限制application UI只能以1080P进行合成。

  • NativeSFDemo的main方法

main方法比较简单

1. signal函数注册监听SIGINT信号的handler,也就是保证Ctrl+C退出程序的完整性;

2. 创建NativeSurfaceWrapper对象,并调用drawNativeSurface进行图片的绘制;

int main() {
signal(SIGINT, sighandler); sp<NativeSurfaceWrapper> nativeSurface(new NativeSurfaceWrapper(String8("NativeSFDemo")));
drawNativeSurface(nativeSurface);
return 0;
}

按下Ctrl+C退出程序时,呼叫到sighandler将mQuit这个标志设为true,这样会使图片的while循环就可以正常流程退出了

void sighandler(int num) {
if(num == SIGINT) {
printf("\nSIGINT: Force to stop !\n");
mQuit = true;
}
}
  • drawNativeSurface方法

绘制图片的核心逻辑都在这个方法中,我们先看一下代码:

int drawNativeSurface(sp<NativeSurfaceWrapper> nativeSurface) {
status_t err = NO_ERROR;
int countFrame = 0;
ANativeWindowBuffer *nativeBuffer = nullptr;
ANativeWindow* nativeWindow = nativeSurface->getSurface().get(); // 1. connect the ANativeWindow as a CPU client. Buffers will be queued after being filled using the CPU
err = native_window_api_connect(nativeWindow, NATIVE_WINDOW_API_CPU);
if (err != NO_ERROR) {
ALOGE("ERROR: unable to native_window_api_connect\n");
return EXIT_FAILURE;
} // 2. set the ANativeWindow dimensions
err = native_window_set_buffers_user_dimensions(nativeWindow, nativeSurface->width(), nativeSurface->height());
if (err != NO_ERROR) {
ALOGE("ERROR: unable to native_window_set_buffers_user_dimensions\n");
return EXIT_FAILURE;
} // 3. set the ANativeWindow format
err = native_window_set_buffers_format(nativeWindow, PIXEL_FORMAT_RGBX_8888);
if (err != NO_ERROR) {
ALOGE("ERROR: unable to native_window_set_buffers_format\n");
return EXIT_FAILURE;
} // 4. set the ANativeWindow usage
err = native_window_set_usage(nativeWindow, GRALLOC_USAGE_SW_WRITE_OFTEN);
if (err != NO_ERROR) {
ALOGE("native_window_set_usage failed: %s (%d)", strerror(-err), -err);
return err;
} // 5. set the ANativeWindow scale mode
err = native_window_set_scaling_mode(nativeWindow, NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW);
if (err != NO_ERROR) {
ALOGE("native_window_set_scaling_mode failed: %s (%d)", strerror(-err), -err);
return err;
} // 6. set the ANativeWindow permission to allocte new buffer, default is true
static_cast<Surface*>(nativeWindow)->getIGraphicBufferProducer()->allowAllocation(true); // 7. set the ANativeWindow buffer count
int numBufs = 0;
int minUndequeuedBufs = 0; err = nativeWindow->query(nativeWindow,
NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS, &minUndequeuedBufs);
if (err != NO_ERROR) {
ALOGE("error: MIN_UNDEQUEUED_BUFFERS query "
"failed: %s (%d)", strerror(-err), -err);
goto handle_error;
} numBufs = minUndequeuedBufs + 1;
err = native_window_set_buffer_count(nativeWindow, numBufs);
if (err != NO_ERROR) {
ALOGE("error: set_buffer_count failed: %s (%d)", strerror(-err), -err);
goto handle_error;
} // 8. draw the ANativeWindow
while(!mQuit) {
// 9. dequeue a buffer
int hwcFD = -1;
err = nativeWindow->dequeueBuffer(nativeWindow, &nativeBuffer, &hwcFD);
if (err != NO_ERROR) {
ALOGE("error: dequeueBuffer failed: %s (%d)",
strerror(-err), -err);
break;
} // 10. make sure really control the dequeued buffer
sp<Fence> hwcFence(new Fence(hwcFD));
int waitResult = hwcFence->waitForever("dequeueBuffer_EmptyNative");
if (waitResult != OK) {
ALOGE("dequeueBuffer_EmptyNative: Fence::wait returned an error: %d", waitResult);
break;
} sp<GraphicBuffer> buf(GraphicBuffer::from(nativeBuffer)); // 11. Fill the buffer with black
uint8_t* img = nullptr;
err = buf->lock(GRALLOC_USAGE_SW_WRITE_OFTEN, (void**)(&img));
if (err != NO_ERROR) {
ALOGE("error: lock failed: %s (%d)", strerror(-err), -err);
break;
} //12. Draw the window
countFrame = (countFrame+1)%3;
fillRGBA8Buffer(img, nativeSurface->width(), nativeSurface->height(), buf->getStride(),
countFrame == 0 ? 255 : 0,
countFrame == 1 ? 255 : 0,
countFrame == 2 ? 255 : 0); err = buf->unlock();
if (err != NO_ERROR) {
ALOGE("error: unlock failed: %s (%d)", strerror(-err), -err);
break;
} // 13. queue the buffer to display
int gpuFD = -1;
err = nativeWindow->queueBuffer(nativeWindow, buf->getNativeBuffer(), gpuFD);
if (err != NO_ERROR) {
ALOGE("error: queueBuffer failed: %s (%d)", strerror(-err), -err);
break;
} nativeBuffer = nullptr;
sleep(1);
} handle_error:
// 14. cancel buffer
if (nativeBuffer != nullptr) {
nativeWindow->cancelBuffer(nativeWindow, nativeBuffer, -1);
nativeBuffer = nullptr;
} // 15. Clean up after success or error.
err = native_window_api_disconnect(nativeWindow, NATIVE_WINDOW_API_CPU);
if (err != NO_ERROR) {
ALOGE("error: api_disconnect failed: %s (%d)", strerror(-err), -err);
} return err;
}

处理的大概过程

1. 获取我们已经创建Surface的窗口ANativeWindow,作为CPU客户端来连接ANativeWindow,CPU填充buffer数据后入队列进行后续处理;

2. 设置Buffer的大小尺寸native_window_set_buffers_user_dimensions;

3. 设置Buffer格式,可选,之前创建Layer的时候已经设置了;

4. 设置Buffer的usage,可能涉及protected的内容,这里我们简单设为GRALLOC_USAGE_SW_WRITE_OFTEN;

5. 设置scale模式,如果上层给的数据,比如Video,超出Buffer的大小后,怎么处理,是截取一部分还是,缩小;

6. 设置permission允许分配新buffer,默认true;

7. 设置Buffer数量,即BufferQueue中有多少个buffer可以用;

8. 下面的流程就是请求buffer并进行绘制图像的过程

9. dequeueBuffer先请求一块可用的Buffer,也就是FREE的Buffer;

10. Buffer虽然是Free的,但是在异步模式下,Buffer可能还在使用中,需要等到Fence才能确保buffer没有在被使用;

11. lock方法可以获取这块GraphicBuffer的数据地址;

12. 绘制图像,即把图像颜色数据写入Buffer里面,我们这里使用fillRGBA8Buffer来填充纯色图片;

13. 将绘制好的Buffer,queue到Buffer队列中,入队列后的buffer就可以被消费者处理或显示了;

14. 错误处理,取消掉Buffer,cancelBuffer;

15. 断开BufferQueue和窗口的连接,native_window_api_disconnect。

  • fillRGBA8Buffer
void fillRGBA8Buffer(uint8_t* img, int width, int height, int stride, int r, int g, int b) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t* pixel = img + (4 * (y*stride + x));
pixel[0] = r;
pixel[1] = g;
pixel[2] = b;
pixel[3] = 0;
}
}
}

fillRGBA8Buffer用指定的RGBA填充buffer数据,我们设置的颜色格式为PIXEL_FORMAT_RGBX_8888,所以每个像素点均由4个字节组成,前3个字节分别为R/G/B颜色分量。


 
我们可以通过执行 dumpsys SurfaceFlinger 来查看图层的信息
Display 4629995328241972480 HWC layers:
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Layer name
Z | Window Type | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB) | Frame Rate (Explicit) (Seamlessness) [Focused]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
bbq-wrapper#0
rel 0 | 0 | CLIENT | 0 | 0 0 1920 1080 | 0.0 0.0 1920.0 1080.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
 
看到了没,一个名字为“bbq-wrapper#0”的Layer显示在最上层,也就是我们应用显示的图层,看到这里你一定有个疑问,我们设置的Surface Name不是“NativeSFDemo”吗 ?
 
dumpsys SurfaceFlinger信息中,我们还可以看到如下内容:
+ BufferStateLayer (NativeSFDemo#0) uid=0
Region TransparentRegion (this=0 count=0)
Region VisibleRegion (this=0 count=0)
Region SurfaceDamageRegion (this=0 count=0)
layerStack= 0, z=2147483647, pos=(0,0), size=( -1, -1), crop=[ 0, 0, -1, -1], cornerRadius=0.000000, isProtected=0, isTrustedOverlay=0, isOpaque=0, invalidate=0, dataspace=Default, defaultPixelFormat=Unknown/None, backgroundBlurRadius=0, color=(0.000,0.000,0.000,1.000), flags=0x00000000, tr=[0.00, 0.00][0.00, 0.00]
parent=none
zOrderRelativeOf=none
activeBuffer=[ 0x 0: 0,Unknown/None], tr=[0.00, 0.00][0.00, 0.00] queued-frames=0, mRefreshPending=0, metadata={}, cornerRadiusCrop=[0.00, 0.00, 0.00, 0.00], shadowRadius=0.000,
+ BufferStateLayer (bbq-wrapper#0) uid=0
Region TransparentRegion (this=0 count=0)
Region VisibleRegion (this=0 count=1)
[ 0, 0, 1920, 1080]
Region SurfaceDamageRegion (this=0 count=0)
layerStack= 0, z= 0, pos=(0,0), size=(1920,1080), crop=[ 0, 0, -1, -1], cornerRadius=0.000000, isProtected=0, isTrustedOverlay=0, isOpaque=1, invalidate=0, dataspace=Default, defaultPixelFormat=RGBx_8888, backgroundBlurRadius=0, color=(0.000,0.000,0.000,1.000), flags=0x00000100, tr=[0.00, 0.00][0.00, 0.00]
parent=NativeSFDemo#0
zOrderRelativeOf=none
activeBuffer=[1920x1080:1920,RGBx_8888], tr=[0.00, 0.00][0.00, 0.00] queued-frames=0, mRefreshPending=0, metadata={dequeueTime:700243748286}, cornerRadiusCrop=[0.00, 0.00, 0.00, 0.00], shadowRadius=0.000,
 
两个BufferStateLayer:BufferStateLayer (NativeSFDemo#0)  和  BufferStateLayer (bbq-wrapper#0),其中bbq-wrapper#0的parent就是NativeSFDemo#0,这其中的关系我们之后的文章中会陆续分析。

4 小结

至此,我们已经建立起来了一个简单的图形图像处理的简单Demo,当让我们目前还是只从应用的较多介绍了基本图形APIs的使用逻辑,接下来的我们就基于此demo,深入底层逻辑探究其中的奥秘。



必读:

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



Android 12(S) 图形显示系统 - 示例应用(二)的更多相关文章

  1. Android 12(S) 图形显示系统 - createSurface的流程(五)

    题外话 刚刚开始着笔写作这篇文章时,正好看电视在采访一位92岁的考古学家,在他的日记中有这样一句话,写在这里与君共勉"不要等待幸运的降临,要去努力的掌握知识".如此朴实的一句话,此 ...

  2. Android 12(S) 图形显示系统 - BufferQueue/BLASTBufferQueue之初识(六)

    题外话 你有没有听见,心里有一声咆哮,那一声咆哮,它好像在说:我就是要从后面追上去! 写文章真的好痛苦,特别是自己对这方面的知识也一知半解就更加痛苦了.这已经是这个系列的第六篇了,很多次都想放弃了,但 ...

  3. Android 12(S) 图形显示系统 - 初识ANativeWindow/Surface/SurfaceControl(七)

    题外话 "行百里者半九十",是说步行一百里路,走过九十里,只能算是走了一半.因为步行越接近目的地,走起来越困难.借指凡事到了接近成功,往往是最吃力.最艰难的时段.劝人做事贵在坚持, ...

  4. Android 12(S) 图形显示系统 - BufferQueue的工作流程(九)

    题外话 Covid-19疫情的强烈反弹,小区里检测出了无症状感染者.小区封闭管理,我也不得不居家办公了.既然这么大把的时间可以光明正大的宅家里,自然要好好利用,八个字 == 努力工作,好好学习 一.前 ...

  5. Android 12(S) 图形显示系统 - 简单聊聊 SurfaceView 与 BufferQueue的关联(十三)

    必读: Android 12(S) 图形显示系统 - 开篇 一.前言 前面的文章中,讲解的内容基本都是从我们提供的一个 native demo Android 12(S) 图形显示系统 - 示例应用( ...

  6. Android 12(S) 图形显示系统 - Surface 一点补充知识(十二)

    必读: Android 12(S) 图形显示系统 - 开篇 一.前言 因为个人工作主要是Android多媒体播放的内容,在工作中查看源码或设计程序经常会遇到调用API: static inline i ...

  7. Android 12(S) 图形显示系统 - 应用建立和SurfaceFlinger的沟通桥梁(三)

    1 前言 上一篇文章中我们已经创建了一个Native示例应用,从使用者的角度了解了图形显示系统API的基本使用,从这篇文章开始我们将基于这个示例应用深入图形显示系统API的内部实现逻辑,分析运作流程. ...

  8. Android 12(S) 图形显示系统 - BufferQueue的工作流程(八)

    题外话 最近总有一个感觉:在不断学习中,越发的感觉自己的无知,自己是不是要从"愚昧之巅"掉到"绝望之谷"了,哈哈哈 邓宁-克鲁格效应 一.前言 前面的文章中已经 ...

  9. Android 12(S) 图形显示系统 - BufferQueue的工作流程(十)

    题外话 疫情隔离在家,周末还在努力学习的我  ..... 一.前言 上一篇文章中,有基本讲清楚Producer一端的处理逻辑,最后也留下了一个疑问: Consumer是什么时候来消费数据的?他是自己主 ...

随机推荐

  1. 12 - Vue3 UI Framework - 打包发布

    基础组件库先做到这个阶段,后面我会继续新增.完善 接下来,我们对之前做的组件进行打包发布到 npm 返回阅读列表点击 这里 组件库优化 通用层叠样式表 我想大家都注意到了,前面我们在写组件的时候,sc ...

  2. CF710C Magic Odd Square 题解

    Content 构造出一个 \(n\times n\) 的矩阵,使得这个矩阵由 \(1\sim n^2\) 这些数字组成,并且这个矩阵的每行,每列,以及对角线的和都为奇数. 数据范围:\(1\leqs ...

  3. 年度最受欢迎的开源CHROME插件

    又到了年底,时间过得飞快,每到年底就有各种各样的总结各种各样的奖项出来.前几天谷歌就公布了2021年年度最受欢迎Chrome插件名单,名单共有13个. 让很多网友费解的是,其中有很多并不是今年刚出现的 ...

  4. WebRTC本地插入多个转发节点

    网络延迟是一种比较常见的情况.在本地网页上,我们可以建立多个RTCPeerConnection,增加转发次数,来模拟出网络延迟的效果. 建立通话后,再往后面增加本地转发节点. 准备 页面准备,方便我们 ...

  5. python2升级到python3 yum不可用解决方案

    /usr/libexec/urlgrabber-ext-down /usr/bin/yum 这两个文件解释器 写 /usr/bin/python2

  6. 当通过Struts2传值给后台时,如果是外键,传字符串那么会出现错误

    当通过Struts2传值给后台时,如果是外键,传字符串那么会出现错误 如<input type="text" name="user.department" ...

  7. 【九度OJ】题目1185:特殊排序 解题报告

    [九度OJ]题目1185:特殊排序 解题报告 标签(空格分隔): 九度OJ [LeetCode] http://ac.jobdu.com/problem.php?pid=1185 题目描述: 输入一系 ...

  8. 【LeetCode】735. Asteroid Collision 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 栈 日期 题目地址:https://leetcode ...

  9. 使用VUE CLI3.0搭建项目vue2+scss+element简易版

    1.安装Vue CLI 3 //三选一即可cnpm install -g @vue/cli npm install -g @vue/cli yarn global add @vue/cli 注意: 1 ...

  10. matplotlib 进阶之Artist tutorial(如何操作Atrist和定制)

    目录 基本 plt.figure() fig.add_axes() ax.lines set_xlabel 一个完整的例子 定制你的对象 obj.set(alpha=0.5, zorder=2), o ...