【23】Android高级知识之Window(四) - ThreadedRenderer
一、概述
在上一篇文章中已经讲了setView整个流程中,最开始的addToDisplay和WMS跨进程通信的整个过程做了什么。继文章Android基础知识之Window(二),这算是另外一个分支了,接着讲分析在performTraversals的三个操作中,最后触发performDraw执行绘制的绘制原理。
二、SurfaceFlinger基础
SurfaceFlinger是Android操作系统中一个关键组件,负责管理和合成显示内容。你说它是显示引擎也可以,说他是Android的显示服务器也可以。
2.1 创建
它属于一个独立的进程,在系统启动过程中,会通过init进程解析init.rc,然后再去加载SurfaceFlinger。最后加载的路径在*/frameworks/native/services/surfaceflinger/main_surfaceflinger.cpp*,执行它的main函数。
//main_surfaceflinger.cpp
int main(int, char**) {signal(SIGPIPE, SIG_IGN);...// start the thread poolsp<ProcessState> ps(ProcessState::self());ps->startThreadPool();...// instantiate surfaceflinger// 实例化SurfaceFlingersp<SurfaceFlinger> flinger = surfaceflinger::createSurfaceFlinger();...
2.2 图形系统概要
这里简单的介绍一下图形系统,应用程序可以借助图形系统在屏幕上显示画面与用户完成交互。把图形系统进行划分,可以分为UI框架、渲染系统(Skia/OpenGL)、窗口系统(X11/Wayland/SurfaceFlinger)、显示系统(DRM/显示驱动等),可以看到讲的SurfaceFlinger属于系统层级中的窗口系统。
- 显示系统:对屏幕的抽象和封装
- 渲染系统:抽象和封装GPU提供的渲染能力
- 窗口系统:把一块屏幕拆分为几个window使得多个应用同时使用屏幕
- UI框架:向应用程序提供与用户交互的能力
纵向分层,从下层至上层分为
GPU -> GPU驱动 -> OpenGL -> 2D图形库(Skia等)-> UI框架(Android原生View /Flutter等)
在来说一下渲染和绘制这两个概念,很多地方经常会互用,但也没有问题,有时候我们说渲染某个画面,或者绘制某个画面也是同一个意思。但是如果需要认真区分,它们就是两个不同的概念了。
- 绘制:View -> 2D几何图形(矩阵/圆/三角形)和文字
- 渲染:点/直线/三角面片/ -> (光栅化/着色)像素(矢量图转变位图)


三、绘制
基本的概念补充了一下,就讲这次的主要内容了,performTraversals执行了测量、布局、和绘制三个操作,前面两个操作都是为最后一个绘制做的准备工作。在应用上层中,常常提到的绘制,我们知道是执行View#onDraw方法,可是怎么执行进来的,在之前文章中只是讲了一个大概,这次就详细分析一下这个流程,perfromDraw中主要的函数draw。
//ViewRootImpl.java
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {...//DEBUG下,可以捕获当前fps值if (DEBUG_FPS) {trackFPS();}...//脏视图的集合是否为空(有没有变化的视图区域)if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {//判断是否开启了硬件加速(是否硬件支持)if (isHardwareEnabled()) {...//硬件绘制(ThreadRenderer进行绘制)mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);} else {...//软件绘制if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,scalingRequired, dirty, surfaceInsets)) {return false;}}}
}
3.1 drawSoftware
先看一下软件绘制drawSoftware做了什么,一般情况没有开启硬件加速,在performDraw执行进来过后,就执行这部分逻辑。
//ViewRootImpl.java
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {// Draw with software renderer.final Canvas canvas;try {//拿到Surface的画布canvas = mSurface.lockCanvas(dirty);canvas.setDensity(mDensity);} catch (Surface.OutOfResourcesException e) {handleOutOfResourcesException(e);return false;} catch (IllegalArgumentException e) {Log.e(mTag, "Could not lock surface", e);mLayoutRequested = true; // ask wm for a new surface next time.return false;}try {if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {canvas.drawColor(0, PorterDuff.Mode.CLEAR);}//清空脏视图缓存dirty.setEmpty();mIsAnimating = false;mView.mPrivateFlags |= View.PFLAG_DRAWN;canvas.translate(-xoff, -yoff);if (mTranslator != null) {mTranslator.translateCanvas(canvas);}canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);//回调到View的onDraw方法mView.draw(canvas);drawAccessibilityFocusedDrawableIfNeeded(canvas);} finally {try {//将后缓冲区提交到前缓冲区显示surface.unlockCanvasAndPost(canvas);} catch (IllegalArgumentException e) {Log.e(mTag, "Could not unlock surface", e);mLayoutRequested = true; // ask wm for a new surface next time.//noinspection ReturnInsideFinallyBlockreturn false;}}return true;
}
mSurface是ViewRootImpl创建的一个Surface对象,也就说明一个windnow对应一个Surface和SurfaceControl对象,这个在之前文章有讲过。Surface涉及的双缓冲机制,分前缓冲区和后缓冲区,前缓冲区用于显示,绘制在后缓冲区,绘制完成通过unlockCanvasAndPost和前缓冲区互换,完成显示,防止闪烁的问题。这里我们看到了mView#draw方法,回调View当中的onDraw,通过Surface拿到的canvas执行绘制代码。
补充:ViewRootImpl 和 SurfaceView 可以看作是一个层级的事物,他们都持有一个 surface,ViewRootImpl 自己把 ViewTree 渲染到 surface 上,SurfaceView 的 surface 供应用自行使用,应用可以把游戏/视频/相机/3D图形库生成数据放到 surface 上
3.2 ThreadedRenderer#draw
然后继续看一下mAttachInfo.mThreadedRenderer.draw这个方法,mThreadedRenderer是我们常说的渲染线程,mAttachInfo属于View类中的一个内部类。在performTraversals中,会判断并执行enableHardwareAcceleration,然后创建renderer对象。
//ViewRootImpl.java@UnsupportedAppUsageprivate void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {...if (ThreadedRenderer.sRendererEnabled || forceHwAccelerated) {if (mAttachInfo.mThreadedRenderer != null) {mAttachInfo.mThreadedRenderer.destroy();}final Rect insets = attrs.surfaceInsets;final boolean hasSurfaceInsets = insets.left != 0 || insets.right != 0|| insets.top != 0 || insets.bottom != 0;final boolean translucent = attrs.format != PixelFormat.OPAQUE || hasSurfaceInsets;final ThreadedRenderer renderer = ThreadedRenderer.create(mContext, translucent,attrs.getTitle().toString());mAttachInfo.mThreadedRenderer = renderer;renderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);updateColorModeIfNeeded(attrs.getColorMode());updateRenderHdrSdrRatio();updateForceDarkMode();mAttachInfo.mHardwareAccelerated = true;mAttachInfo.mHardwareAccelerationRequested = true;if (mHardwareRendererObserver != null) {renderer.addObserver(mHardwareRendererObserver);}}}
}
代码我们可以看到,通过ThreadedRenderer#create的静态方法,创建renderer对象,并赋值给了mAttachInfo.mThreadedRenderer属性。继续看一下renderer#draw方法。
//ThreadedRenderer.java/*** Draws the specified view.** @param view The view to draw.* @param attachInfo AttachInfo tied to the specified view.*/void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {attachInfo.mViewRootImpl.mViewFrameInfo.markDrawStart();updateRootDisplayList(view, callbacks);// register animating rendernodes which started animating prior to renderer// creation, which is typical for animators started prior to first drawif (attachInfo.mPendingAnimatingRenderNodes != null) {final int count = attachInfo.mPendingAnimatingRenderNodes.size();for (int i = 0; i < count; i++) {registerAnimatingRenderNode(attachInfo.mPendingAnimatingRenderNodes.get(i));}attachInfo.mPendingAnimatingRenderNodes.clear();// We don't need this anymore as subsequent calls to// ViewRootImpl#attachRenderNodeAnimator will go directly to us.attachInfo.mPendingAnimatingRenderNodes = null;}final FrameInfo frameInfo = attachInfo.mViewRootImpl.getUpdatedFrameInfo();int syncResult = syncAndDrawFrame(frameInfo);if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {Log.w("OpenGLRenderer", "Surface lost, forcing relayout");// We lost our surface. For a relayout next frame which should give us a new// surface from WindowManager, which hopefully will work.attachInfo.mViewRootImpl.mForceNextWindowRelayout = true;attachInfo.mViewRootImpl.requestLayout();}if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {attachInfo.mViewRootImpl.invalidate();}}
方法注解说明是一个绘制指定View的方法,AttachInfo绑定到指定View上。syncAndDrawFrame是父类HardwareRenderer的一个方法,调用的是native方法。再看一下updateRootDisplayList。
//ThreadedRenderer.javaprivate void updateRootDisplayList(View view, DrawCallbacks callbacks) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");//更新view的一些标志位updateViewTreeDisplayList(view);if (mNextRtFrameCallbacks != null) {final ArrayList<FrameDrawingCallback> frameCallbacks = mNextRtFrameCallbacks;mNextRtFrameCallbacks = null;//设置每帧的绘制回调setFrameCallback(new FrameDrawingCallback() {@Overridepublic void onFrameDraw(long frame) {}@Overridepublic FrameCommitCallback onFrameDraw(int syncResult, long frame) {ArrayList<FrameCommitCallback> frameCommitCallbacks = new ArrayList<>();for (int i = 0; i < frameCallbacks.size(); ++i) {FrameCommitCallback frameCommitCallback = frameCallbacks.get(i).onFrameDraw(syncResult, frame);if (frameCommitCallback != null) {frameCommitCallbacks.add(frameCommitCallback);}}if (frameCommitCallbacks.isEmpty()) {return null;}return didProduceBuffer -> {for (int i = 0; i < frameCommitCallbacks.size(); ++i) {frameCommitCallbacks.get(i).onFrameCommit(didProduceBuffer);}};}});}if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {//拿到RecordingCanvas对象,通过mRootNode获取RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);try {final int saveCount = canvas.save();canvas.translate(mInsetLeft, mInsetTop);callbacks.onPreDraw(canvas);canvas.enableZ();//执行canvas的drawRenderNode,来执行mRootNode绘制canvas.drawRenderNode(view.updateDisplayListIfDirty());canvas.disableZ();callbacks.onPostDraw(canvas);canvas.restoreToCount(saveCount);mRootNodeNeedsUpdate = false;} finally {mRootNode.endRecording();}}Trace.traceEnd(Trace.TRACE_TAG_VIEW);
RecordingCanvas是Canvas的一个子类,而RecordingCanvas#drawRenderNode方法,将绘制任务传递给本地层,调用了nDrawRenderNode是一个native方法。/frameworks/base/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
//SkiaRecordingCavas.app
void SkiaRecordingCanvas::drawRenderNode(uirenderer::RenderNode* renderNode) {// Record the child node. Drawable dtor will be invoked when mChildNodes deque is cleared.mDisplayList->mChildNodes.emplace_back(renderNode, asSkCanvas(), true, mCurrentBarrier);auto& renderNodeDrawable = mDisplayList->mChildNodes.back();if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaVulkan) {// Put Vulkan WebViews with non-rectangular clips in a HW layerrenderNode->mutateStagingProperties().setClipMayBeComplex(mRecorder.isClipMayBeComplex());}drawDrawable(&renderNodeDrawable);// use staging property, since recording on UI threadif (renderNode->stagingProperties().isProjectionReceiver()) {mDisplayList->mProjectionReceiver = &renderNodeDrawable;}
}
SkiaRecordingCanvas是一个用于记录绘制命令的类。renderNode是一个记录了绘制命令的对象。DisplayList用来存储ViewTree中需要绘制的View,所生成的renderNode节点。
1、mDisplayList把RenderNode节点添加到它的mChildNodes列表的尾部
2、然后取出列表尾部这个元素赋值给renderNodeDrawable
3、执行drawDrawable函数,传入renderNodeDrawable地址
4、Drawable#draw会将绘制命令传递给SkCanvas
5、Skia图形库再将绘制命令转换为GPU指令,并通过OpenGL等图形API发送到GPU进行渲染
SkCanvas是Skia图形库的核心类,用于执行具体的绘制操作。
软件绘制,通过Surface.unlockCanvasAndPost把提交绘制结果到SurfaceFlinger。硬件绘制,通过使用GPU进行绘制,并通过OpenGL等图形API与SurfaceFlinger通信。它们最后都实现了SurfaceFlinger的通信过程,并提交了结果,SurfaceFlinger负责合成各个窗口的内容,并将最终的显示结果提交到屏幕上。
这里给出了Activity一帧的绘制流程:

总结
1、performDraw分两个流程软件绘制和硬件绘制
2、软件绘制直接在ViewRootImpl创建的Surface进行绘制并提交给SurfaceFlinger
3、判断启动硬件加速会创建Render对象
4、硬件绘制通过RecordingCanvas提交绘制任务给本地层
5、RenderNode会记录绘制命令并将绘制命令传递给SkCanvas上
6、Skia图形库将命令转换成GPU指令交由GPU进行渲染
之后最后一篇文章,主要围绕整个图形系统,详细讲讲SurfaceFlinger的概念。
相关文章:
【23】Android高级知识之Window(四) - ThreadedRenderer
一、概述 在上一篇文章中已经讲了setView整个流程中,最开始的addToDisplay和WMS跨进程通信的整个过程做了什么。继文章Android基础知识之Window(二),这算是另外一个分支了,接着讲分析在performTraversals的三个操作中,最后触发pe…...
Java-根据前缀-日期-数字-生成流水号(不重复)
🎈边走、边悟🎈迟早会好 小伙伴们在日常开发时可能会遇到的业务-生成流水号,在企业中可以说是比较常见的需求, 可以采用"前缀日期数字"的方式(ps:此方式是需要用到缓存的)前缀:为了…...
跟李沐学AI:卷积层
从全连接层到卷积 多层感知机十分适合处理表格数据,其中行对应样本,列对应特征。但对于图片等数据,全连接层会导致参数过多。卷积神经网络(convolutional neural networks,CNN)是机器学习利用自然图像中一…...
使用RedisTemplate操作executePipelined
前言 RedisTemplate 是 Spring 提供的用于操作 Redis 的模板类,它封装了 Redis 的连接、连接池等管理,并提供了一系列的操作方法来简化 Redis 的使用。其中,executePipelined 方法是 RedisTemplate 中的一个高级特性,用于支持 Re…...
react-native从入门到实战系列教程一环境安装篇
充分阅读官网的环境配置指南,严格按照他的指导作业,不然你一直只能在web或沙箱环境下玩玩 极快的网络和科学上网,必备其中的一个较好的心理忍受能力,因为上面一点就可以让你放弃坚持不懈,努力尝试 成功效果 三大件 …...
【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(下)
【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(下) 大家好 我是寸铁👊 【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(下)✨ 喜欢的小伙伴可以点点关注 💝 前言 本次文章分为上下两部分&…...
国科大作业考试资料-人工智能原理与算法-2024新编-第十二次作业整理
袋子里面有3个有偏差的硬币a、b和c,抛掷硬币正面朝上的概率分别是20%、60%和80%。从袋子里随机取出一个硬币(3个硬币被取出的概率是相等的),并把取出的硬币抛掷3次,得到抛掷结果依次是X1 , X2和 X3。 a. 画出对应的贝叶斯网络并定义必要的CPT表。 b. 如果抛掷结果是2次正…...
《0基础》学习Python——第二十一讲__网络爬虫/<4>爬取豆瓣电影电影信息
爬取网页数据(获取网页信息全过程) 1、爬取豆瓣电影的电影名称、导演、主演、年份、国家、评价 2、首先我们先爬取页面然后再获取信息 1、爬取网页源码 import requests from lxml import etree if __name__ __main__:#UA伪装head{User-Agent:Mozilla/…...
【C++初阶】string类
【C初阶】string类 🥕个人主页:开敲🍉 🔥所属专栏:C🥭 🌼文章目录🌼 1. 为什么学习string类? 1.1 C语言中的字符串 1.2 实际中 2. 标准库中的string类 2.1 string类 2.…...
RAS--APEI 报错解析流程(2)
RAS--APEI 报错解析流程(1) 除了APEI 中除了GHES会记录错误,在Post过程中的错误通常是通过BERT Table汇报 1.BERT Boot Error Record Table is used to report unhandled errors that occurred in a previous boot,it is reported as a ‘one-time polle…...
微软蓝屏事件对企业数字化转型有什么影响?
引言:从北京时间2024年7月19日(周五)下午2点多开始,全球大量Windows用户出现电脑崩溃、蓝屏死机、无法重启等情况。事发后,网络安全公司CrowdStrike称,收到大量关于Windows电脑出现蓝屏报告,公司…...
【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(上)
【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(上) 大家好 我是寸铁👊 【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(上)✨ 喜欢的小伙伴可以点点关注 💝 前言 本次文章分为上下两部分&…...
浅谈Devops
1.什么是Devops DevopsDev(Development)Ops(Operation) DevOps(Development和Operations的混合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”…...
大文件分片上传(前端TS实现)
大文件分片上传 内容 一般情况下,前端上传文件就是new FormData,然后把文件 append 进去,然后post发送给后端就完事了,但是文件越大,上传的文件也就越长,如果在上传过程中,突然网络故障,又或者…...
unity2D游戏开发02添加组件移动玩家
添加组件 给PlayGame和EnemyObject添加组件BoxCollider 2D碰撞器,不用修改参数 给PlayGame添加组件Rigibody 2D 设置数据 添加EnemyObject,属性如下 Edit->project setting->Physics 2D 将 y的值改为0 给playerObject添加标签 新建层 将PlayerObj…...
设计模式 之 —— 单例模式
目录 什么是单例模式? 定义 单例模式的主要特点 单例模式的几种设计模式 1.懒汉式:线程不安全 2.懒汉式:线程安全 3.饿汉式 4.双重校验锁 单例模式的优缺点 优点: 缺点: 适用场景: 什么是单例模…...
深入浅出WebRTC—ULPFEC
FEC 通过在发送端添加额外的冗余信息,使接收端即使在部分数据包丢失的情况下也能恢复原始数据,从而减轻网络丢包的影响。在 WebRTC 中,FEC 主要有两种实现方式:ULPFEC 和 FlexFEC,FlexFEC 是 ULPFEC 的扩展和升级&…...
Python从0到100(四十三):数据库与Django ORM 精讲
前言: 零基础学Python:Python从0到100最新最全教程。 想做这件事情很久了,这次我更新了自己所写过的所有博客,汇集成了Python从0到100,共一百节课,帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…...
Redis-主从模式
目录 前言 一.主从节点介绍 二.配置redis主从结构 二.主从复制 四.拓扑结构 五.数据同步 全量复制(Full Sync Replication) 局部复制(Partial Replication) Redis的学习专栏:http://t.csdnimg.cn/a8cvV 前言 …...
加速决策过程:企业级爬虫平台的实时数据分析
摘要 在当今数据驱动的商业环境中,企业如何才能在海量信息中迅速做出精准决策?本文将探讨企业级爬虫平台如何通过实时数据分析加速决策过程,实现数据到决策的无缝衔接。我们聚焦于技术如何赋能企业,提升数据处理效率,…...
剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
云原生安全实战:API网关Kong的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关(API Gateway) API网关是微服务架构中的核心组件,负责统一管理所有API的流量入口。它像一座…...
uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...
Python网页自动化Selenium中文文档
1. 安装 1.1. 安装 Selenium Python bindings 提供了一个简单的API,让你使用Selenium WebDriver来编写功能/校验测试。 通过Selenium Python的API,你可以非常直观的使用Selenium WebDriver的所有功能。 Selenium Python bindings 使用非常简洁方便的A…...
Python实现简单音频数据压缩与解压算法
Python实现简单音频数据压缩与解压算法 引言 在音频数据处理中,压缩算法是降低存储成本和传输效率的关键技术。Python作为一门灵活且功能强大的编程语言,提供了丰富的库和工具来实现音频数据的压缩与解压。本文将通过一个简单的音频数据压缩与解压算法…...
Linux基础开发工具——vim工具
文章目录 vim工具什么是vimvim的多模式和使用vim的基础模式vim的三种基础模式三种模式的初步了解 常用模式的详细讲解插入模式命令模式模式转化光标的移动文本的编辑 底行模式替换模式视图模式总结 使用vim的小技巧vim的配置(了解) vim工具 本文章仍然是继续讲解Linux系统下的…...
EEG-fNIRS联合成像在跨频率耦合研究中的创新应用
摘要 神经影像技术对医学科学产生了深远的影响,推动了许多神经系统疾病研究的进展并改善了其诊断方法。在此背景下,基于神经血管耦合现象的多模态神经影像方法,通过融合各自优势来提供有关大脑皮层神经活动的互补信息。在这里,本研…...
高保真组件库:开关
一:制作关状态 拖入一个矩形作为关闭的底色:44 x 22,填充灰色CCCCCC,圆角23,边框宽度0,文本为”关“,右对齐,边距2,2,6,2,文本颜色白色FFFFFF。 拖拽一个椭圆,尺寸18 x 18,边框为0。3. 全选转为动态面板状态1命名为”关“。 二:制作开状态 复制关状态并命名为”开…...
