深入 Flutter 和 Compose 的 PlatformView 实现对比,它们是如何接入平台控件

在上一篇《深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比》发布之后,收到了大佬的“催稿”,想了解下 Flutter 和 Compose 在 PlatformView 实现上的对比,恰好过去写过不少 Flutter 上对于 PlatformView 的实现,这次恰好可以用来和 Compose 做个简单对比:

Flutter

其实 Flutter 在 Android 上的 PlatformView 实现过去已经聊过好多次了,Flutter 作为完全脱离平台渲染树的独立 UI 库,它在混合开发的 PlatformView 实现可以说是“历经沧桑” 。

既然前面我们讲过很多次,这里主要就是简单介绍下,方便和 Compose 做个对比,感兴趣的可以去看后面的详细链接。

在 Flutter 上是通过 AndroidView 接入平台控件,目前活跃在 Android 平台的 PlatformView 支持主要有以下三种:

  • Virtual Display (VD)
  • Hybrid Composition (HC)
  • Texture Layer Hybrid Composition (TLHC)

为什么会有这么多不同模式支持?因为主要是随着技术推进和适配场景,PlatformView 的适配需求都在更新,但是新来的又不能完全提前之前的方案,所以就导致实现都并存下来。

VD

VD简单来说就是使用 VirtualDisplay 渲染原生控件到内存,然后利用 id 在 Flutter 界面上占用一个相应大小的位置,最后通过 id 关联到 Flutter Texture 里进行渲染。

问题也很明显,因为控件不会真实存在渲染的位置,可以不严谨理解,它只是内存里 UI 的“镜像”显示,或者说“副屏镜像”,所以此时的点击和对原生控件的操作,其实都是需要由 Flutter 这个 View 进行二次转发到原生再回到 Flutter 。

另外因为控件是渲染在内存里,所以和键盘交互需要通过二级代理处理,容易产生各种键盘输入和交互的异常问题,特别是 WebView 场景。

当然,现在的 VD 已经比初始的时候好很多,并且还在兼容“服役”。

HC

1.2 版本开始支持 HC 模式,这个版本就是直接把原生控件「覆盖」在 FlutterView 上进行堆叠,简单来说就是 HC 模式会直接把原生控件通过 addView 添加到 FlutterView 上 。如果出现 Flutter Widget 需要渲染在 Native Widget 上,就采用新的 FlutterImageView 来承载新图层。

比如在 Layout Inspector,HC 模式可以看出来各种原生布局的边界绘制:

而如下图所示,其中蓝色的文本是原生的 TextView ,红色的文本是 Flutter 的 Text 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:

  • 两个蓝色的 TextView 是被添加在 FlutterView 之上,并且把没有背景色的红色 RE 遮挡住了
  • 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 TextView 之上,所以这时候多一个 FlutterImageView ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。

这里的 FlutterImageView ,其实还有一个作用,就是为了解决动画同步和渲染

当然,这样带来了一个问题,因为此时原生控件是直接渲染,所以需要在原生的平台线程上执行,纯在 Flutter 的 UI 线程就存在线程同步问题,所以在此之前一些场景下会有画面闪烁 bug 。

虽然这个问题最后也通过类似线程同步实现解决,但是也带来一定程度的性能开销,另外在 Android 10 之前还会存在 GPU->CPU->GPU的性能损耗,所以 HC 属于会性能开销较大,又需要原生控件特性的场景

TLHC

3.0 版本开始支持 TLHC 模式,最初的目的是取代上面这两种模式,可惜最终共存下来,该模式下控件虽然在还是布局在该有的位置上,但是其实是通过一个 FrameLayout 代理 onDraw 然后替换掉 child 原生控件的 Canvas 来实现混合绘制。

所以看到此时上图 TextView 里没有了内容,因为 TextView 里的 Canvas 被替换成 Flutter 在内存里创建的 Canvas

其实 TLHC 流程上和 VD 基本一样,简单对比 VirtualDisplayTextureLayer 的实现差异,可以看到主要还是在于原生控件纹理的提取方式上

从上图我们可以得知:

  • 从 VD 到 TLHC, Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑

  • 以前 Flutter 中会将 AndroidView 需要渲染的内容绘制到 VirtualDisplays ,然后在 VirtualDisplay 对应的内存中,绘制的画面就可以通过其 Surface 获取得到;现在 AndroidView 需要的内容,会通过 View 的 draw 方法被绘制到 SurfaceTexture 里,然后同样通过 TextureId 获取绘制在内存的纹理

从这个简单流程上看,这里面的关键就在于 super.draw(surfaceCanvas); ,给 Android 的 View “模拟” 出来工作环境,然后通过“替换” Canvas 让 View 绘制需要的 Surface 上合成:

那 TLHC 有什么问题?因为它是通过“替换” Canvas 来得到 UI ,但是这种实现天然不支持 SurfaceView等场景,因为 SurfaceView 是自己独立的 Surface 和 Canvas,所以通过 parent 替换 Canvas 的实现并不支持。

所以目前的 PlatformVIew 支持上的结果:

  • 默认会是 TLHC 模式,如果发现接入的 View 是 SurfaceView ,那么就会“降级”使用 VD 来适配
  • 可以通过 initExpensiveAndroidView 接口强行使用 HC

详细链接:

https://blog.csdn.net/ZuoYueLiang/article/details/131800717
https://blog.csdn.net/ZuoYueLiang/article/details/124577097
https://blog.csdn.net/ZuoYueLiang/article/details/124805110

Compose

Compose 的 PlatformView 原理这里可以详细聊聊,这个目前的资料不多,比较有聊的价值。

众所周知,Jetpack Compose 虽然是 Android 平台的全新 UI 开发框架,但是它的 UI 渲染树和「传统 xml View 控件」是“不直接兼容”的,Compose 属于独立的 UI 库,它的 UI 模式更接近 Flutter ,但是 @Composable 函数又不是和 Flutter 一样 return ,在实际工作中,Compose 代码在编译时会给 @Composable 函数添加 Composer 参数 ,而实际的 UI Node Tree 等的创建,都是从“隐藏”的 Composer 开始:

详细可见:https://blog.csdn.net/ZuoYueLiang/article/details/145105060?spm=1001.2014.3001.5501

所以,一旦你需要在 Jetpack Compose 里接入一个原生控件,你就需要用到 PlatformView 的相关实现,PlatformView 本质上就是把「传统 xml View 控件」渲染进 Compose 渲染树里,而在 Compose 在 Android 平台,使用的就是 AndroidView

@Composable
fun CustomView() {var selectedItem by remember { mutableStateOf("Hello from View") }// Adds view to ComposeAndroidView(modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI treefactory = { context ->// Creates viewTextView(context).apply {text = "Hello from View"textSize = 30ftextAlignment = TextView.TEXT_ALIGNMENT_CENTER}}, update = { view ->// View's been inflated or state read in this block has been updated// Add logic here if necessary// As selectedItem is read here, AndroidView will recompose// whenever the state changes// Example of Compose -> View communicationview.text = selectedItem})
}

如上代码所示,通过 AndroidView 我们可以把一个 Android 传统的 TextView 添加到 Compose 里,当然这没什么实际意义,只是作为一个简单例子。

渲染之后,我们可以看到在 Layout Inspector 的 Component tree 里并没有 TextView ,因为它只是被渲染到 Compose 里,但是它其实并不是 “直接” 存在于 Compose 的 LayoutNode ,它只是“依附”在 AndroidView

想知道 AndroidView 的工作原理,我们需要看它的 factory 实现,从源码我们可以看到,它主要是通过 ViewFactoryHolder 创建了一个代理 layoutNode 来“进入” Compose 渲染树:

而 Android 上 ViewFactoryHolder 的实现主要在它基类 AndroidViewHolder ,这里可以看到 AndroidViewHolder 那可是一个“实实在在”的传统 ViewGroup 实现

我们可以假设,我们前面的传统 TextView ,在 AndroidView 内部实际上就是被添加到 AndroidViewHolder 这个 ViewGroup 里 ,而且这里还有一个 Owner ,从命名上也很“关键”。

带着这两个问题,我们继续看,首先我们在 AndroidViewHolder 里可以看到有 layoutNode 的实现,也就是其实这个 Holder ,它既是传统 ViewGroup ,又具备 Compose 里的 layoutNode 实现

通过查看 layoutNode 的实现,我们可以看到:

  • layoutNodeonAttach 到 Compose 布局里的时候,会执行 addAndroidView ,其实这里的 addAndroidView 内部,就是一个 ViewGroupaddView 操作
  • 另外,在 layoutNode 被 onDetach 时执行 removeAndroidView ,内部也就是 ViewGroupremoveViewInLayout
  • 另外还有通过 MeasurePolicy 处理布局,简单说就是将 Compose 的布局状态同步到 AndroidViewHolder 这个 ViewGroup 去布局,给 「传统 XML View」“模拟” 布局环境。

所以我们可以看到,AndroidViewHolder 类似一个“中转站”,它将 Compose UI 的生命周期和测绘布局状态同步到传统 ViewGroup 控件,从而给添加进来的 TextView “模拟” 出布局和绘制环境,大概可以总结:

AndroidViewHolder 类似于 Compose 代理 Node,它 Compose 中的 UI 环境“模拟”到 ViewGroup 中,通过控制 ViewGroup 的绘制与布局来控制我们的「传统 xml View 控件」

那么 AndroidViewHolder 肯定就是从 onAttach 开始进入 Compose 的 LayoutNode 体系工作,这里关键在于 AndroidComposeView 的这个操作:

(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)

这里又冒出来一个新对象 AndroidComposeView ,它就是我们前面所说的 owner ,那它又是什么?

我们看 AndroidComposeView 的源码,可以看到 AndroidComposeView 同样是一个 ViewGroup ,它的内部主要是有一个 AndroidViewsHandlerViewGroup 在处理 AndroidViewHolder ,比如前面的 addAndroidView 就是将 Holder 添加到 Handler

那到这里就有三个东西:

  • AndroidComposeView
  • AndroidViewsHandler
  • AndroidViewHolder

它们都是传统 ViewGroup 的实现,且关系大概如下所示:

那么到这里,流程上我们应该就清晰了,我们只需要搞清楚 AndroidComposeView 是什么,来自哪里,然后往下,大概就可以理清它的实现。

我们通过 AndroidComposeView 内部有个 root 节点的实现,可以猜测它应该是一个顶层节点,所以我们直接从顶部开始找:

我们从 Activity 开始往下找,经过几个简单调整,就可以在 AbstractComposeView.setContent 找到创建 AndroidComposeView 的地方:

因为 AbstractComposeView 的实现是 ComposeView ,所以可以看到:

AndroidComposeView 是在初始时被 ComposeView 创建并 addView ,然后 Composition 里 UiApplier 的 root 节点就是 AndroidComposeView

所以这就是为什么前面我们那个 owner 为什么是 AndroidComposeView 的来源,然后往下就是 AndroidViewsHandler ,它主要就是持有所有 Holder ,然后根据调用给它的 children 执行各种布局和绘制操作:

所以我们就知道了:

  • 在初始化的时候,Compose 就会创建一个顶层 ViewGroup 节点 AndroidComposeView ,它是一个 root LayoutNode
  • AndroidComposeView 内部的 AndroidViewsHandler 会通过一个 hashMap 去触发和管理 children Holder 的布局和重绘
  • AndroidViewHolder 是一个代理 LayoutNode ,同时它将 Compose UI 的生命周期和测绘布局状态同步到传统 ViewGroup 控件

大概会是下面这样的结构,但是它虽然被 addViewViewGroup 里,但是它并不会直接渲染在 ViewGroup 里 ,而是「被代理渲染」到 LayoutNode 对应的 Scope 里

比如我们接入了两个 SurfaceView 到 Compose ,如果我们打印传统布局结构,大概可以看到这样的一个结果,:

这里举例的 SurfaceView 后面会顺便聊聊 。

最后就是绘制,知道流程后,我们直接看回 AndroidViewHolder 里的 layoutNode 实现,在这里有一个来自 drawBehindcanvas

一般情况下,drawBehind 修饰符可以想任何可组合函数后面绘制内容时,例如:

Text("Hello Compose!",modifier = Modifier.drawBehind {drawRoundRect(Color(0xFFBBAAEE),cornerRadius = CornerRadius(10.dp.toPx()))}.padding(4.dp)
)

而这里的 Canvas 是来自 DrawScopeDrawScope 属于一个针对 Canvas 接口的高级封装,内部 Canvas 的底层支持还是原生平台的 Canvas ,因为 Compose 有多平台支持,而 Android 平台对应的就是 AndroidCanvas 对象,这里是通过 canvas.nativeCanvas 获取到的,就是 android.graphics.Canvas 对象,也就是传入了一个 Android 原生 Canvas

流程上如下图所示,这里的核心其实就是:将 Compose 里 drawBehind 的 Canvas 传递给「传统 XML View」,这样在绘制时用的就是来自 Compose 体系 drawBehind 的 Canvas 链条

所以这里可以看到,在绘制的时候,采用的其实就是通过 AndroidViewHolder 这个 ViewGroup 作为 Parent 来 “替换” 掉作为 child 的传统 View 的 Canvas ,让 View 的内容通过 Compose 的 Canvas 绘制到它所在的 LayoutNode 上

另外, pointerInteropFilter 也会处理手势事件,用户在当前 LayoutNode 交互的手势,会被发送到 AndroidViewHolder 这个 ViewGroup ,从而触发传统 Androd 控件的点击等效果。

最后,在 navigate 切换的时候, AndroidViewHolder 也会相对应的被 add/remove 。

从这角度看,Compose 的 PlatformView 实现和 Flutter 的 TextureLayer 理念很接近,都是通过“替换” Canvas 和“模拟”布局环境来实现 View 接入,但是,它们又有本质不同,这个不同就体现在 SurfaceView

因为 SurfaceView 是有自己独立的 Surface 和 Canvas ,所以它是无法被 Parent 的 Canvas “替换” ,这也是 Flutter 里 TLHC 的问题,但是在 Compose 里,你会发现 SurfaceViewAndroidView 里可以正常工作:

@Composable
fun ContentExample() {Box() {ComposableSurfaceView(Modifier.size(100.dp))Text("Compose", modifier = Modifier.drawBehind {drawRoundRect(color = Color(0x9000FFFF), cornerRadius = CornerRadius(10.dp.toPx()))}.padding(vertical = 30.dp))}
}@Composable
fun ComposableSurfaceView(modifier: Modifier = Modifier) {AndroidView(factory = { context ->SurfaceView(context).apply {layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)holder.addCallback(MySurfaceCallback())//添加回调}}, modifier = modifier)
}class MySurfaceCallback : SurfaceHolder.Callback {private var _canvas: Canvas? = nulloverride fun surfaceCreated(p0: SurfaceHolder) {_canvas = p0.lockCanvas()_canvas?.drawColor(android.graphics.Color.GRAY)//设置背景颜色_canvas?.drawCircle(100f, 100f, 50f, Paint().apply {color = android.graphics.Color.YELLOW})//绘制一个红色的图像p0.unlockCanvasAndPost(_canvas)}
}

可以看到,上面代码的 SurfaceView 灰色的背景和黄色的圆都被渲染出来,另外 TextCompose 文本也正常带着背景色覆盖显示在 SurfaceView 上:

有没有觉得奇怪,为什么 SurfaceViewCanvas 没有被替换,但是 SurfaceView 的内容和层级却又正常渲染在了 Compose UI 树里?

其实道理很简单,虽然 Compose 和「传统 XML View」 是两套 UI 框架,但是 Compose 的本质还是 Android 里面的 View ,也就是它依旧在 View 体系的范畴内

依赖 Android 的 Surface、Window、SurfaceFlinger 体系去渲染。

我们简单回忆下 SurfaceView 是怎么工作的?

  • Android 里控件基本都是以 View 为基类,所有可见 View 对象都会渲染到一个 Surface ,这个 Surface 来自 SurfaceFlinger ,也就是当前 Window 下。

  • 尽管 SurfaceView 继承自类View,但是它有自己独立的 Surface,是直接提交到 SurfaceFlinger

这也是 SurfaceView 会有自己独立 Canvas 的原因,简单说它是一个可以绘制到 Surface 并直接输出到 SurfaceFlinger 的视图。

一般情况下, SurfaceView 在其 Window 层上始终是一个透明的 Rect,类似于**SurfaceView 在其窗口中打了一个洞**, 并且默认情况下,SurfaceView 的 Z 顺序始终低于其附加的 Window 层,也就是 SurfaceView 的 Surface 是在默认 Surface 的下面。

而最终渲染时,SurfaceFlinger 会将 SurfaceView 的图像层和 Window 的图像层叠加在一起

那么回到 Compose,Compose 的底层还是一个传统的 View ,所以它还是依赖 View 的 Surface 和 SurfaceFlinger,也就是:

Compose 和「传统 View」 共用同一个 Window 和 DecorViewAndroidView 作为一个桥接节点,将「传统 View」 “插入” 到 Compose 的布局树中,虽然 SurfaceView 绘制内容是独立的,但在屏幕上是共享一个 WindowSurfaceFlinger 依然会统一管理窗口合成。

如给上方 SurfaceView 的代码加上 setZOrderOnTop(true),就会看到 Compose 的 Text 看不到了,因为此时的 Z 层面发生了变化:

这就是 Compose 和 Flutter 在 AndroidView 上最大的区别:

Flutter 是完全脱离了渲染体系,但是 Compose 还是在 View 体系内,所以 SurfaceView 不会是问题,甚至官方还推出了 SurfaceView 对应的 Compose 封装 AndroidExternalSurfaceScope

只是说,在 「传统 XML View」 体系中,每个 View 会有一个 RenderNode,而 Compose 中“一般”只有 ComposeView 一个 RenderNode,也就是传说的单页面状态,而 Compose 内部最终就是将自己的 LayoutNode 通过 Composer 组合完成后塞到 RenderNode 里面。

最后

可以看到,在 Android 平台上, Flutter 和 Compose 在最终实现思路很接近,大家都叫 AndroidView理念都是“模拟”环境和“替换” Canvas ,但是在 Android 平台上 Compose 有着原生 View 体系的优势,所以它对 SurfaceView 的支持更友好。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小游戏源码开发搭建技术栈和服务器配置流程

近些年各种场景小游戏开发搭建版本层出不穷,山东布谷科技拥有多年海内外小游戏源码开发经验,现为从事小游戏源码开发或游戏运营的朋友们详细介绍小游戏开发及服务器配置流程。 一、可以对接到app的小游戏是如何开发的 1、小游戏源码开发的需求分析: 明…

【Linux网络编程】传输层协议

目录 一,传输层的介绍 二,UDP协议 2-1,UDP的特点 2-2,UDP协议端格式 三,TCP协议 3-1,TCP报文格式 3-2,TCP三次握手 3-3,TCP四次挥手 3-4,滑动窗口 3-5&#xf…

五、华为 RSTP

RSTP(Rapid Spanning Tree Protocol,快速生成树协议)是 STP 的优化版本,能实现网络拓扑的快速收敛。 一、RSTP 原理 快速收敛机制:RSTP 通过引入边缘端口、P/A(Proposal/Agreement)机制等&…

期权帮|在股指期货中超过持仓限额怎么办?

锦鲤三三每日分享期权知识,帮助期权新手及时有效地掌握即市趋势与新资讯! 在股指期货中超过持仓限额怎么办? 一、立即平仓或减仓: (1)最直接且有效的方法是立即平仓或减仓,以降低持仓量至限额…

【FFmpeg】FLV 格式分析 ③ ( Tag Body 数据块体结构 - Vedio Data 视频数据 )

文章目录 一、Tag Body 数据块体结构 - Video Data 视频数据1、Vedio Data 视频数据 类型标识2、Vedio Data 视频数据 结构分析3、Composition Time Offset 字段涉及的时间计算4、AVC Packet Type 字段说明① AVC Sequence Header 类型② AVC NALU 类型③ AVC End of Sequence …

【React】 react路由

这一篇文章的重点在于将React关于路由的问题都给搞清楚。 一个路由就是一个映射关系,key:value。key是路径,value 可能是function或者component。 安装react-router-dom包使用路由服务,我这里想要用的是6版本的包,因此后面加”6&q…

vue3+uniapp开发鸿蒙初体验

去年7月20号,uniapp官网就已经开始支持鸿蒙应用开发了,话不多说,按照现有规则进行配置实现一下鸿蒙开发效果; 本文基于macOS Monterey 版本 12.6.5实现 开发鸿蒙的前置准备 这里就直接说我的版本: DevEco Studio 5.…

Git代码管理工具 — 5 GitHub远程仓库

目录 1 常用的代码托管平台 2 GitHub平台详解 2.1 github简介 2.2 Github基本功能介绍 3 GitHub创建远程仓库 3.1 创建远程仓库 3.2 创建远程仓库别名 4 推送本地分支到远程仓库 5 拉取远程库到本地 6 克隆远程库到本地 1 常用的代码托管平台 前面我们已经知道了Git…

UDP 广播组播点播的区别及联系

1、网络IP地址的分类 组播地址是分类编址的IPv4地址中的D类地址,又叫多播地址,他的前四位必须是1110,所以网络地址的二进制取值范围是11100000~11101111对应的十进制为 224~~239。所以以224~239开头的网络地址都是组播地址。 组播地址的功能…

mysql 学习2 MYSQL数据模型,mysql内部可以创建多个数据库,一个数据库中有多个表;表是真正放数据的地方,关系型数据库 。

在第一章中安装 ,启动mysql80 服务后,连接上了mysql,那么就要 使用 SQL语句来 操作mysql数据库了。那么在学习 SQL语言操作 mysql 数据库 之前,要对于 mysql数据模型有一个了解。 MYSQL数据模型 在下图中 客户端 将 SQL语言&…

10个非常基础的 Javascript 问题

Javascript是一种用于Web开发的编程语言。JavaScript在网络的客户端上运行。 根据MDN,JavaScript(通常缩写为JS)是一种轻量级的,解释性的,面向对象的语言,具有一流的功能,并且最著名的是Web页面…

把 PVE 下的机械硬盘(非SSD系统盘)分配给虚拟机使用

PVE 挂在硬盘 参考 Ubuntu 24.04 LTS 空闲硬盘挂载到 文件管理器的 other locations。 在 PVE shell 中根据上面教程挂在硬盘 新建分享目录 参考 Proxmox VE(PVE)添加硬盘做存储 虚拟机新增硬盘 虚拟机 关机,按下图新增硬盘 新增硬盘…

制作动态菜单

动态菜单的说明 什么叫做动态菜单?动态菜单就是根据用户属于不同的角色,每个角色还有不同的菜单,左侧菜单栏会有不同的显示 前端加载流程 menu.js 修改/src/api/menu.js中的请求地址,如下所示: // 获取菜单 export…

C语言数组详解:从基础到进阶的全面解析

在C语言中,数组是一种基本的数据结构,用于存储多个相同类型的数据。数组的引入使得C语言能够高效地存储和操作大量数据。在任何一个C语言程序中,数组都发挥着极其重要的作用。无论是在算法实现、数据存储、还是在复杂程序的设计中&#xff0c…

什么是COLLATE排序规则?

在当今数字化世界中,数据的整理、比较和排序是至关重要的。在数据库管理和编程语言中,我们经常需要对字符串进行排序,以展示或处理信息。为了实现这一点,各种系统和工具提供了排序规则,其中COLLATE排序规则就是其中的一…

【数据结构进阶】红黑树超详解 + 实现(附源码)

🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:数据结构 目录 前言 一、红黑树介绍 二、红黑树原理详解 三、红黑树的实现 1. 节点定义 2. 红黑树类型定义及接口声明 3. 红黑树的插入(重点&a…

计算机网络 (57)改进“尽最大努力交付”的服务

前言 计算机网络中的“尽最大努力交付”服务是网络层的一种数据传输方式。这种服务的特点是网络层只负责尽力将数据报从源端传输到目的端,而不保证数据传输的可靠性。 一、标记与分类 为数据分组打上标记: 给不同性质的分组打上不同的标记&#x…

ThinkPHP 8模型与数据的插入、更新、删除

【图书介绍】《ThinkPHP 8高效构建Web应用》-CSDN博客 《2025新书 ThinkPHP 8高效构建Web应用 编程与应用开发丛书 夏磊 清华大学出版社教材书籍 9787302678236 ThinkPHP 8高效构建Web应用》【摘要 书评 试读】- 京东图书 使用VS Code开发ThinkPHP项目-CSDN博客 编程与应用开…

【数据可视化】全国星巴克门店可视化

🧑 博主简介:曾任某智慧城市类企业算法总监,目前在美国市场的物流公司从事高级算法工程师一职,深耕人工智能领域,精通python数据挖掘、可视化、机器学习等,发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

密码无关认证:金融机构如何解决密码问题

密码安全问题,依然是金融行业面临的重大挑战。尽管密码简单易用,但许多金融机构仍然依赖这种方式进行身份认证。幸运的是,随着技术的发展,密码无关认证已经成为一种更加安全、便捷的选择,它能够为数字银行带来更好的用…