当前位置: 首页 > news >正文

【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整个流程中&#xff0c;最开始的addToDisplay和WMS跨进程通信的整个过程做了什么。继文章Android基础知识之Window(二)&#xff0c;这算是另外一个分支了&#xff0c;接着讲分析在performTraversals的三个操作中&#xff0c;最后触发pe…...

Java-根据前缀-日期-数字-生成流水号(不重复)

&#x1f388;边走、边悟&#x1f388;迟早会好 小伙伴们在日常开发时可能会遇到的业务-生成流水号&#xff0c;在企业中可以说是比较常见的需求&#xff0c; 可以采用"前缀日期数字"的方式&#xff08;ps:此方式是需要用到缓存的&#xff09;前缀&#xff1a;为了…...

跟李沐学AI:卷积层

从全连接层到卷积 多层感知机十分适合处理表格数据&#xff0c;其中行对应样本&#xff0c;列对应特征。但对于图片等数据&#xff0c;全连接层会导致参数过多。卷积神经网络&#xff08;convolutional neural networks&#xff0c;CNN&#xff09;是机器学习利用自然图像中一…...

使用RedisTemplate操作executePipelined

前言 RedisTemplate 是 Spring 提供的用于操作 Redis 的模板类&#xff0c;它封装了 Redis 的连接、连接池等管理&#xff0c;并提供了一系列的操作方法来简化 Redis 的使用。其中&#xff0c;executePipelined 方法是 RedisTemplate 中的一个高级特性&#xff0c;用于支持 Re…...

react-native从入门到实战系列教程一环境安装篇

充分阅读官网的环境配置指南&#xff0c;严格按照他的指导作业&#xff0c;不然你一直只能在web或沙箱环境下玩玩 极快的网络和科学上网&#xff0c;必备其中的一个较好的心理忍受能力&#xff0c;因为上面一点就可以让你放弃坚持不懈&#xff0c;努力尝试 成功效果 三大件 …...

【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(下)

【Gin】精准应用&#xff1a;Gin框架中工厂模式的现代软件开发策略与实施技巧(下) 大家好 我是寸铁&#x1f44a; 【Gin】精准应用&#xff1a;Gin框架中工厂模式的现代软件开发策略与实施技巧(下)✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 前言 本次文章分为上下两部分&…...

国科大作业考试资料-人工智能原理与算法-2024新编-第十二次作业整理

袋子里面有3个有偏差的硬币a、b和c,抛掷硬币正面朝上的概率分别是20%、60%和80%。从袋子里随机取出一个硬币(3个硬币被取出的概率是相等的),并把取出的硬币抛掷3次,得到抛掷结果依次是X1 , X2和 X3。 a. 画出对应的贝叶斯网络并定义必要的CPT表。 b. 如果抛掷结果是2次正…...

《0基础》学习Python——第二十一讲__网络爬虫/<4>爬取豆瓣电影电影信息

爬取网页数据&#xff08;获取网页信息全过程&#xff09; 1、爬取豆瓣电影的电影名称、导演、主演、年份、国家、评价 2、首先我们先爬取页面然后再获取信息 1、爬取网页源码 import requests from lxml import etree if __name__ __main__:#UA伪装head{User-Agent:Mozilla/…...

【C++初阶】string类

【C初阶】string类 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;C&#x1f96d; &#x1f33c;文章目录&#x1f33c; 1. 为什么学习string类&#xff1f; 1.1 C语言中的字符串 1.2 实际中 2. 标准库中的string类 2.1 string类 2.…...

RAS--APEI 报错解析流程(2)

RAS--APEI 报错解析流程(1) 除了APEI 中除了GHES会记录错误&#xff0c;在Post过程中的错误通常是通过BERT Table汇报 1.BERT Boot Error Record Table is used to report unhandled errors that occurred in a previous boot&#xff0c;it is reported as a ‘one-time polle…...

微软蓝屏事件对企业数字化转型有什么影响?

引言&#xff1a;从北京时间2024年7月19日&#xff08;周五&#xff09;下午2点多开始&#xff0c;全球大量Windows用户出现电脑崩溃、蓝屏死机、无法重启等情况。事发后&#xff0c;网络安全公司CrowdStrike称&#xff0c;收到大量关于Windows电脑出现蓝屏报告&#xff0c;公司…...

【Gin】精准应用:Gin框架中工厂模式的现代软件开发策略与实施技巧(上)

【Gin】精准应用&#xff1a;Gin框架中工厂模式的现代软件开发策略与实施技巧(上) 大家好 我是寸铁&#x1f44a; 【Gin】精准应用&#xff1a;Gin框架中工厂模式的现代软件开发策略与实施技巧(上)✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 前言 本次文章分为上下两部分&…...

浅谈Devops

1.什么是Devops DevopsDev&#xff08;Development&#xff09;Ops&#xff08;Operation&#xff09; DevOps&#xff08;Development和Operations的混合词&#xff09;是一种重视“软件开发人员&#xff08;Dev&#xff09;”和“IT运维技术人员&#xff08;Ops&#xff09;”…...

大文件分片上传(前端TS实现)

大文件分片上传 内容 一般情况下&#xff0c;前端上传文件就是new FormData,然后把文件 append 进去&#xff0c;然后post发送给后端就完事了&#xff0c;但是文件越大&#xff0c;上传的文件也就越长&#xff0c;如果在上传过程中&#xff0c;突然网络故障&#xff0c;又或者…...

unity2D游戏开发02添加组件移动玩家

添加组件 给PlayGame和EnemyObject添加组件BoxCollider 2D碰撞器&#xff0c;不用修改参数 给PlayGame添加组件Rigibody 2D 设置数据 添加EnemyObject&#xff0c;属性如下 Edit->project setting->Physics 2D 将 y的值改为0 给playerObject添加标签 新建层 将PlayerObj…...

设计模式 之 —— 单例模式

目录 什么是单例模式&#xff1f; 定义 单例模式的主要特点 单例模式的几种设计模式 1.懒汉式&#xff1a;线程不安全 2.懒汉式&#xff1a;线程安全 3.饿汉式 4.双重校验锁 单例模式的优缺点 优点&#xff1a; 缺点&#xff1a; 适用场景&#xff1a; 什么是单例模…...

深入浅出WebRTC—ULPFEC

FEC 通过在发送端添加额外的冗余信息&#xff0c;使接收端即使在部分数据包丢失的情况下也能恢复原始数据&#xff0c;从而减轻网络丢包的影响。在 WebRTC 中&#xff0c;FEC 主要有两种实现方式&#xff1a;ULPFEC 和 FlexFEC&#xff0c;FlexFEC 是 ULPFEC 的扩展和升级&…...

Python从0到100(四十三):数据库与Django ORM 精讲

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…...

Redis-主从模式

目录 前言 一.主从节点介绍 二.配置redis主从结构 二.主从复制 四.拓扑结构 五.数据同步 全量复制&#xff08;Full Sync Replication&#xff09; 局部复制&#xff08;Partial Replication&#xff09; Redis的学习专栏&#xff1a;http://t.csdnimg.cn/a8cvV 前言 …...

加速决策过程:企业级爬虫平台的实时数据分析

摘要 在当今数据驱动的商业环境中&#xff0c;企业如何才能在海量信息中迅速做出精准决策&#xff1f;本文将探讨企业级爬虫平台如何通过实时数据分析加速决策过程&#xff0c;实现数据到决策的无缝衔接。我们聚焦于技术如何赋能企业&#xff0c;提升数据处理效率&#xff0c;…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中&#xff0c;结构体可以嵌套使用&#xff0c;形成更复杂的数据结构。例如&#xff0c;可以通过嵌套结构体描述多层级数据关系&#xff1a; struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

Docker 离线安装指南

参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性&#xff0c;不同版本的Docker对内核版本有不同要求。例如&#xff0c;Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本&#xff0c;Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

cf2117E

原题链接&#xff1a;https://codeforces.com/contest/2117/problem/E 题目背景&#xff1a; 给定两个数组a,b&#xff0c;可以执行多次以下操作&#xff1a;选择 i (1 < i < n - 1)&#xff0c;并设置 或&#xff0c;也可以在执行上述操作前执行一次删除任意 和 。求…...

CMake控制VS2022项目文件分组

我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

高抗扰度汽车光耦合器的特性

晶台光电推出的125℃光耦合器系列产品&#xff08;包括KL357NU、KL3H7U和KL817U&#xff09;&#xff0c;专为高温环境下的汽车应用设计&#xff0c;具备以下核心优势和技术特点&#xff1a; 一、技术特性分析 高温稳定性 采用先进的LED技术和优化的IC设计&#xff0c;确保在…...

HTML中各种标签的作用

一、HTML文件主要标签结构及说明 1. <&#xff01;DOCTYPE html> 作用&#xff1a;声明文档类型&#xff0c;告知浏览器这是 HTML5 文档。 必须&#xff1a;是。 2. <html lang“zh”>. </html> 作用&#xff1a;包裹整个网页内容&#xff0c;lang"z…...

基于谷歌ADK的 智能产品推荐系统(2): 模块功能详解

在我的上一篇博客&#xff1a;基于谷歌ADK的 智能产品推荐系统(1): 功能简介-CSDN博客 中我们介绍了个性化购物 Agent 项目&#xff0c;该项目展示了一个强大的框架&#xff0c;旨在模拟和实现在线购物环境中的智能导购。它不仅仅是一个简单的聊天机器人&#xff0c;更是一个集…...

SFTrack:面向警务无人机的自适应多目标跟踪算法——突破小尺度高速运动目标的追踪瓶颈

【导读】 本文针对无人机&#xff08;UAV&#xff09;视频中目标尺寸小、运动快导致的多目标跟踪难题&#xff0c;提出一种更简单高效的方法。核心创新在于从低置信度检测启动跟踪&#xff08;贴合无人机场景特性&#xff09;&#xff0c;并改进传统外观匹配算法以关联此类检测…...

八、【ESP32开发全栈指南:UDP客户端】

1. 环境准备 安装ESP-IDF v4.4 (官方指南)确保Python 3.7 和Git已安装 2. 创建项目 idf.py create-project udp_client cd udp_client3. 完整优化代码 (main/main.c) #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h&…...