【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是一个用于记录绘制命令的类。它的drawRenderNode方法会将RenderNode添加到显示列表中,并记录相关的绘制命令。drawable方法会将Drawable对象绘制到当前SkCanvas上。这个方法会调用drawable#draw,会将绘制命令传递给SkCanvas。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的概念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1487168.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Ansible的脚本-----playbook剧本【上】

目录 1.playbook剧本组成 2.playbook剧本实战演练 2.1 实战演练一&#xff1a;给被管理主机安装httpd服务 2.2 实战演练二&#xff1a;定义、引用变量 2.3 实战演练三&#xff1a;指定远程主机sudo切换用户 2.4 实战演练四&#xff1a;when条件判断 2.5 实战演练五&…

一家银行数据库的六年攻坚战

前沿科技&#xff0c;数智经济 文&#xff5c;白 鸽 编&#xff5c;王一粟 从传统的商业数据库Oracle&#xff0c;到后来加入的MySQL数据库&#xff0c;再到现如今的分布式数据库&#xff0c;中国金融行业数据库的转型升级走过了多年时间。 “2018年&#xff0c;我们提出…

《你敢不学习?》numpy库——细细学<2>

续接上集: 1、reshape函数&#xff1a;重塑数组的形状 改变数组的维度 其语法为 numpy.reshape(arr, newshape, orderC) 如下图所示 首先生成一个1到17不包括17的16个元素的数组&#xff0c;然后对这个数组进行重塑&#xff0c;使其成为4行4列的二维数组&#xff0c;注意&…

【Micropython入门】Thoony安装并烧录固件到ESP32

文章目录 前言Thonny IDE 介绍Thoony的下载烧录固件到ESP32下载固件烧录固件烧录时的小问题 总结 前言 MicroPython 是一款为微控制器设计的精简版 Python 解释器&#xff0c;它以其简洁和强大的特性赢得了众多嵌入式开发者的青睐。ESP32 是一款功能强大且价格低廉的微控制器&…

React开发者并不存在

根本就没有所谓的React开发者 — 永远不要这样称呼自己。 这是许多软件开发者犯的一个巨大错误&#xff0c;浪费了你大量时间。 专注于工具而非概念。忽视了大局。 React只是一个JavaScript工具。JavaScript只是一个计算工具。计算只是一个解决问题的工具。 当我刚开始编码时&a…

hugging face 使用教程———快速入门

概述 本篇存在的意义是快速介绍hugging face使用&#xff0c;梳理主要部件&#xff0c;梳理易混淆概念。原因是&#xff1a;目前hugging face的使用&#xff0c;官方放在了3个地方&#xff08;参考链接部分&#xff09;&#xff1a;使用文档、NLP教程、Transformers git的readm…

PDF转Word后不能修改怎么办?是什么原因呢?

平时在生活中&#xff0c;很多朋友都会有将PDF转换成Word文档的需求&#xff0c;因为一般情况下PDF文件是不能直接编辑修改的&#xff0c;所以只能通过这种方式来解决问题。但是近期&#xff0c;有部分用户在后台反馈说PDF转Word后不能修改怎么办呢&#xff1f;其实这个问题也是…

前端页面:用户交互持续时间跟踪(duration)user-interaction-tracker

引言 在用户至上的时代&#xff0c;精准把握用户行为已成为产品优化的关键。本文将详细介绍 user-interaction-tracker 库&#xff0c;它提供了一种高效的解决方案&#xff0c;用于跟踪用户交互的持续时间&#xff0c;并提升项目埋点的效率。通过本文&#xff0c;你将了解到如…

云仓如何改变传统仓储模式?

云仓&#xff0c;即云仓储&#xff0c;是一种基于互联网技术的现代仓储模式&#xff0c;与传统的仓储模式相比&#xff0c;它在多个方面进行了创新和优化&#xff0c;包括&#xff1a; ———————————————————— 1、数据管理与实时监控&#xff1a; 云仓储利…

每日一题 LeetCode03 无重复字符的最长字串

1.题目描述 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的最长字串的长度。 2 思路 可以用两个指针, 滑动窗口的思想来做这道题,即定义两个指针.一个left和一个right 并且用一个set容器,一个length , 一个maxlength来记录, 让right往右走,并且用一个set容器来…

【数据结构】链表(单链表实现 + 详解 + 原码)

&#x1f387;&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳&#xff0c;欢迎大佬指点&#xff01; 人生格言: 当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友…

Spring Boot配置文件的语法规则

主要介绍两种配置文件的语法和格式&#xff0c;properties和yml 目录 1.配置文件的作用 2.创建配置文件 3.properties语法 4.yml语法 5.配置文件格式 1.配置文件的作用 对于配置文件&#xff0c;也有独立的文件夹去存放&#xff0c;主要用来存放一些需要经过变动的数据&a…

IEDA怎么把springboot项目 启动多个

利用Idea提供的Edit Configurations配置应用参数。 点击Modify Options进行添加应用参数&#xff1a; 确保这里勾选

如何避免蓝屏?轻量部署,安全和业务连续性才能两不误

自19日起&#xff0c;因CrowdStrike软件更新的错误配置而导致的“微软全球蓝屏”&#xff0c;影响依然在持续。这场被称为“史上最大规模的IT故障”&#xff0c;由于所涉全球企业太多&#xff0c;专家估计“蓝屏”电脑全部恢复正常仍需时日。 尽管 CEO 乔治 库尔茨&#xff08…

C#入门与精通

C#精通 本文章主要是对于学习C#基础难点进行学习以及与java语言的不同点&#xff0c;详细学习可见官网&#xff1a;https://dotnet.microsoft.com/en-us/learn 文章目录 C#精通VSVS基本设置 C#是什么C#程序控制台输出变量内插占位符C#foreach循环类型转换操作数组内置方法格式设…

OpenAI发布GPT-4 Mini的深度分析及中国大模型的弯道超车机会

引言 在OpenAI封禁中国IP访问其API后&#xff0c;紧接着推出了GPT-4 Mini&#xff0c;这是一个引发广泛关注和讨论的新举措。此举不仅让人们质疑OpenAI的战略方向&#xff0c;更引发了对中国大模型是否能弯道超车的讨论。本文将详细分析GPT-4 Mini的特点、市场影响及中国大模型…

广东工程职业技术学院财经学院领导一行莅临泰迪智能科技参观交流

7月19日&#xff0c;广东工程职业技术学院财经学院市场调查与统计分析专业主任苏志鹏、专业老师余乐莅临广东泰迪智能科技股份有限公司产教融合实训基地参观交流。泰迪智能科技董事长张良均、副总经理施兴、高校业务部经理孙学镂、校企合作经理吴桂锋进行接待。 仪式伊始&#…

leetcode-79. 单词搜索

题目描述 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水平相…

美食地图开发

调用地图接口展示数据库录入的不同类别地址信息&#xff0c;提供导航服务&#xff0c;手机端电脑端自适应。 语音介绍使用微软的tts接口可选不同语音性别生成

Mysql数据库和Sql语句

数据库管理&#xff1a; sql语句&#xff1a;数据库用来增删改查的语句&#xff08;重要&#xff09; 备份&#xff1a;数据库的数据进行备份 主从复制、读写分离、高可用&#xff08;重要&#xff09; Mysql数据库和Sql语句 一、Mysql数据库 1、数据库&#xff1a;组织、…