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

深入解析iOS视频录制(二):自定义UI的实现

深入解析 iOS 视频录制(一):录制管理核心MWRecordingController 类的设计与实现

深入解析iOS视频录制(二):自定义UI的实现​​​​​​​

深入解析 iOS 视频录制(三):完整录制流程的实现与整合​​​​​​​

引言

在上一篇博客中,我们深入解析了 iOS 视频录制功能的核心类 MWRecordingController 的实现,涵盖了如何管理视频会话、设置输入输出、控制摄像头以及录制的启动与停止等重要功能。本篇博客将接着上文,探索自定义 UI 的实现,重点讲解如何通过 MWRecordingPreview 预览视图、MWRcordingControlView 控制视图以及精心设置的自定义按钮来提升视频录制的用户体验。

我们将逐步揭开这些自定义 UI 组件背后的设计与实现,了解如何通过灵活的视图布局与动画效果,让录制过程更加直观与流畅。希望通过本篇博客,能够帮助大家更深入地理解如何在 iOS 中定制出高质量的视频录制界面。

UI实现

在视频录制功能中,UI 设计不仅仅是视觉呈现,更是与用户交互的桥梁。一个直观、流畅且具有良好反馈的界面,能够大大提升用户的使用体验。通过自定义 UI,我们能够精细控制各个交互细节,确保用户能够快速而顺畅地完成录制操作。

本篇博客将从三个核心部分入手,详细讲解自定义 UI 的实现:

  1. 预览视图的实现:如何设计并展示视频录制的预览内容,确保录制时用户能够实时看到画面。
  2. 控制视图的实现:包括录制、暂停、重新录制以及完成按钮等,通过合理布局和状态管理,使得控制操作清晰易懂。
  3. 顶部导航栏视图的实现:导航栏中的返回按钮与切换摄像头按钮如何与视频录制功能进行无缝集成。

接下来,我们将逐一展开讲解这些关键视图的设计与实现,带大家了解如何通过代码构建一个高效且美观的视频录制界面。

MWRecordingPreview:预览视图的实现

MWRecordingPreview 类是整个视频录制界面中关键部分之一,它负责显示来自摄像头的实时视频预览。在本次实现中,我们简化了设计,确保预览层与录制会话紧密配合,并通过简单的配置来实现画面显示。

以下是 MWRecordingPreview 类的实现代码:

import UIKit
import AVFoundationclass MWRecordingPreview: UIView {override init(frame: CGRect) {super.init(frame: frame)previewLayer.videoGravity = .resizeAspectFill}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}override class var layerClass: AnyClass {return AVCaptureVideoPreviewLayer.self}/// 预览图层var previewLayer: AVCaptureVideoPreviewLayer {return layer as! AVCaptureVideoPreviewLayer}/// 设置图层会话var session:AVCaptureSession? {didSet {previewLayer.session = session}}
}
  1. layerClass:通过重写 layerClass 方法,我们指定了该视图的图层类型为 AVCaptureVideoPreviewLayer,这是一个专门用于显示摄像头预览内容的图层类。
  2. previewLayer:该属性返回类型为A VCaptureVideoPreviewLayer 的图层,我们可以通过它来访问与摄像头相关的配置。
  3. session:这个属性用来设置 AVCaptureSession,它是视频录制功能的核心,管理着输入输出设备以及数据流。通过 didSet 方法,当会话对象被设置时,预览层的 session 会自动与之绑定,从而实现实时预览功能。

通过这种简单的实现,我们就能将摄像头的实时图像呈现在自定义视图 MWRecordingPreview 中,并且具备了灵活的图像填充方式,使用 .resizeAspectFill 可以确保预览画面根据视图的大小自适应显示。

MWRecordingControlView:控制视图的实现

MWRecordingControlView 类负责管理视频录制过程汇总的交互空间,包括录制按钮、录制时间显示、重新录制按钮以及完成按钮。通过合理布局与状态管理,确保用户能够清晰地了解录制进度,并灵活地控制录制行为。

以下是 MWRecordingControlView 类的实现代码:

import UIKitenum MWRecordingControlState {/// 正常case normal/// 录制中case recording/// 录制完成case finish
}class MWRecordingControlView: UIView {/// 录制按钮private let recordButton = MWRecordingButton()/// 录制时间private let recordTimeLabel = UILabel()/// 重新录制按钮private let reRecordButton = MWRecordingItemButton()/// 完成按钮private let finishButton = MWRecordingItemButton()/// 录制按钮点击事件回调var recordButtonClickBlock: (() -> Void)?/// 重新录制点击回调var reRecordButtonClickBlock: (() -> Void)?/// 完成点击回调var finishButtonClickBlock: (() -> Void)?override init(frame: CGRect) {super.init(frame: frame)setupView()setLayout()setEvent()setRecordingState(state: .normal)}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}private func setupView() {// 录制按钮addSubview(recordButton)// 录制时间addSubview(recordTimeLabel)recordTimeLabel.textColor = .whiterecordTimeLabel.font = MWFontHelper.font(name: .nunito, size: 14, weight: .bold)// 重新录制按钮addSubview(reRecordButton)reRecordButton.setImage(UIImage(named: "recording_retake"), for: .normal)reRecordButton.setTitle(MWLocaleStringHelper.getString("Retake"), for: .normal)reRecordButton.setTitleColor(.white, for: .normal)reRecordButton.titleLabel?.font = MWFontHelper.font(name: .nunito, size: 14, weight: .bold)reRecordButton.titleLabel?.textAlignment = .center// 完成按钮addSubview(finishButton)finishButton.setImage(UIImage(named: "recording_complete"), for: .normal)finishButton.setTitle(MWLocaleStringHelper.getString("Complete"), for: .normal)finishButton.setTitleColor(.white, for: .normal)finishButton.titleLabel?.font = MWFontHelper.font(name: .nunito, size: 14, weight: .bold)finishButton.titleLabel?.textAlignment = .center}private func setLayout() {// 录制按钮recordButton.snp.makeConstraints { make inmake.centerX.equalToSuperview()make.top.equalToSuperview()make.width.height.equalTo(72.0)}// 录制时间recordTimeLabel.snp.makeConstraints { make inmake.centerX.equalToSuperview()make.top.equalTo(recordButton.snp.bottom).offset(12.0)make.height.equalTo(19.0)}// 重新录制按钮reRecordButton.snp.makeConstraints { make inmake.trailing.equalTo(recordButton.snp.leading).offset(-44.0)make.width.equalTo(64.0)make.height.equalTo(63.0)make.centerY.equalTo(recordButton)}// 完成按钮finishButton.snp.makeConstraints { make inmake.leading.equalTo(recordButton.snp.trailing).offset(44.0)make.width.equalTo(64.0)make.height.equalTo(63.0)make.centerY.equalTo(recordButton)}}private func setEvent() {// 录制按钮recordButton.addTarget(self, action: #selector(recordButtonClick), for: .touchUpInside)// 重新录制按钮reRecordButton.addTarget(self, action: #selector(reRecordButtonClick), for: .touchUpInside)// 完成按钮finishButton.addTarget(self, action: #selector(finishButtonClick), for: .touchUpInside)}/// 录制事件@objc private func recordButtonClick() {recordButtonClickBlock?()}/// 重新录制事件@objc private func reRecordButtonClick() {reRecordButtonClickBlock?()}/// 完成事件@objc private func finishButtonClick() {finishButtonClickBlock?()}/// 设置录制状态/// - Parameter state: 状态func setRecordingState(state: MWRecordingControlState) {switch state {case .normal:recordButton.isSelected = truerecordTimeLabel.isHidden = truereRecordButton.isHidden = truefinishButton.isHidden = truecase .recording:recordButton.isSelected = falserecordTimeLabel.isHidden = falsereRecordButton.isHidden = truefinishButton.isHidden = truecase .finish:recordButton.isSelected = truerecordTimeLabel.isHidden = falsereRecordButton.isHidden = falsefinishButton.isHidden = false}}/// 更新时间/// - Parameter time: 时间func updateTime(time: TimeInterval) {recordTimeLabel.text = time.formattedMS}
}

MWRecordingControlView 提供了以下几个核心功能:

  1. 录制按钮(recordButton):用于控制录制的开始和暂停。根据状态变化,按钮的样式会自动调整。
  2. 录制时间(recordTimeLabel):显示当前录制的时长,动态更新。
  3. 重新录制按钮(reRecordButton):在录制完成后,允许用户重新开始录制。
  4. 完成按钮(finishButton):会在点击完成时,将录制好的音频文件传递到需要的地方。

通过设置不同的录制状态(normal、recording和finish),控制视图中的各个按钮和标签的显示与隐藏,确保用户能够清晰地看到当前操作的状态。

MWRecordingButton:录制按钮

MWRecordingButton 是自定义的录制按钮,通过多层图层效果展现录制状态,并使用CALayer和CAGradientLayer创建了独特的视觉效果。其设计目标是提供一个既美观又符合录制功能需求的按钮,用户可以通过点击它来启动或停止视频录制。

以下是 MWRecordingButton 类的实现代码:

import UIKitclass MWRecordingButton: UIButton {/// 大圆图层private let bigCircleLayer = CALayer()/// 小圆图层private let smallCircleLayer = CALayer()/// 方形图层private let squareLayer = CAGradientLayer()override init(frame: CGRect) {super.init(frame: frame)setupLayer()}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}private func setupLayer() {// 大圆bigCircleLayer.backgroundColor = UIColor.white.withAlphaComponent(0.6).cgColorbigCircleLayer.cornerRadius = 36.0layer.addSublayer(bigCircleLayer)// 小圆smallCircleLayer.backgroundColor = UIColor.white.withAlphaComponent(1.0).cgColorsmallCircleLayer.cornerRadius = 30layer.addSublayer(smallCircleLayer)// 方形squareLayer.colors = [UIColor.wm_hex("FF3498").cgColor, UIColor.wm_hex("FF4545").cgColor]squareLayer.cornerRadius = 6.0layer.addSublayer(squareLayer)}override var isSelected: Bool {didSet {squareLayer.isHidden = isSelected}}override func layoutSubviews() {super.layoutSubviews()let width = bounds.widthlet height = bounds.height// 大圆bigCircleLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)// 小圆let smallWidth = 60.0smallCircleLayer.frame = CGRect(x: (width - smallWidth) / 2, y: (height - smallWidth) / 2, width: smallWidth, height: smallWidth)// 方形let squareWidth = 25.0squareLayer.frame = CGRect(x: (width - squareWidth) / 2, y: (height - squareWidth) / 2, width: squareWidth, height: squareWidth)}
}
  1. 大圆图层(bigCircleLayer):这个圆形图层充当按钮的背景,使用 CALayer 创建,设置了半透明白色背景,使其看起来既简洁又具有层次感。
  2. 小圆图层 (smallCircleLayer):这个圆形图层在大圆内部显示,颜色为纯白,模拟了录制按钮中的核心圆形。
  3. 方形图层(squareLayer):这个图层使用 CAGradientLayer 绘制了一个渐变色的矩形,位于小圆的中心。它的显示与按钮的选择状(isSelected)态相关,状态为selected时,方形会被隐藏。

通过这种设计,MWRecordingButton在录制和暂停状态之间切换时,能够清晰地反馈状态,用户一眼就能判断当前按钮的作用,同时通过动态的图层切换,使得界面更加生动。

MWRecordingNavigationView:导航栏

MWRecordingNavigationView 是自定义的视频录制界面顶部导航栏视图,它包含了三个主要组件:返回按钮、切换摄像头按钮和一个可定制的中间文案标签。这个视图旨在为用户提供流畅的操作体验,在录制过程中可以方便地进行控制和反馈。

以下是  MWRecordingNavigationView 类的实现代码:

import UIKitclass MWRecordingNavgationView: UIView {/// 返回按钮private let backButton = UIButton()/// 切换摄像头按钮private let switchCameraButton = UIButton()/// 返回按钮点击事件var backButtonClickBlock: (() -> Void)?/// 切换摄像头按钮点击事件var switchCameraButtonClickBlock: (() -> Void)?/// 隐藏显示切换按钮var isHiddenSwitchCameraButton: Bool = false {didSet {switchCameraButton.isHidden = isHiddenSwitchCameraButton}}override init(frame: CGRect) {super.init(frame: frame)setupView()setLayout()setEvent()}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}private func setupView() {// 返回按钮addSubview(backButton)backButton.setImage(UIImage(named: "navigation_back_white"), for: .normal)// 切换摄像头按钮addSubview(switchCameraButton)switchCameraButton.setImage(UIImage(named: "navigation_switch_camera"), for: .normal)}private func setLayout() {// 设置返回按钮的布局backButton.snp.makeConstraints { make inmake.leading.equalToSuperview().offset(16.0)make.centerY.equalToSuperview()make.width.height.equalTo(32.0)}// 设置切换摄像头按钮的布局switchCameraButton.snp.makeConstraints { make inmake.trailing.equalToSuperview().offset(-16.0)make.centerY.equalToSuperview()make.width.height.equalTo(32.0)}}private func setEvent() {// 返回按钮点击事件backButton.addTarget(self, action: #selector(backButtonClick), for: .touchUpInside)// 切换摄像头按钮点击事件switchCameraButton.addTarget(self, action: #selector(switchCameraButtonClick), for: .touchUpInside)}@objc private func backButtonClick() {backButtonClickBlock?()}@objc private func switchCameraButtonClick() {switchCameraButtonClickBlock?()}
}
  1. 返回按钮(backButton):通过UIButton实现,提供一个简单的返回操作,用户可以随时退出当前的录制界面。图标为白色箭头,易于辨识。
  2. 切换摄像头(switchCameraButton):也是一个UIButton,用于切换前后摄像头。在某些应用场景下,用户可能需要切换摄像头,这个按钮就提供了这个功能。

结语

在本文中,我们详细探讨了 iOS 视频录制功能中的自定义 UI 实现,包括预览视图、控制视图和导航栏的设计与实现。这些自定义组件不仅提升了用户体验,还确保了操作的流畅性和可控性。通过自定义 MWRecordingPreview 视图,我们为录制过程提供了实时的视频预览;通过设计 MWRecordingControlView 和 MWRecordingButton,我们实现了清晰直观的录制控制;而 MWRecordingNavigationView 则为用户提供了便捷的导航和摄像头切换功能。

这些 UI 组件的灵活性和可定制性,使得在开发过程中可以根据需求对界面进行调整与扩展。在未来的版本中,您可以继续根据产品需求对这些视图进行优化和改进,带给用户更加丰富和优质的录制体验。

通过对这些自定义 UI 组件的解析,我们希望能够帮助开发者们更好地理解和实现 iOS 视频录制功能,并为其应用增添更多个性化的设计。感谢您的阅读,期待您的探索和创新!

相关文章:

深入解析iOS视频录制(二):自定义UI的实现

深入解析 iOS 视频录制(一):录制管理核心MWRecordingController 类的设计与实现 深入解析iOS视频录制(二):自定义UI的实现​​​​​​​ 深入解析 iOS 视频录制(三):完…...

跳表的C语言实现

跳表(Skip List)是一种基于链表的动态数据结构,用于实现高效的查找、插入和删除操作。它通过引入多级索引来加速查找过程,类似于多级索引的有序链表。跳表的平均时间复杂度为 O(logn),在某些场景下可以替代平衡树。 以…...

Java Web开发实战与项目——Spring Security与权限管理实现

Web应用中,权限管理是系统安全的核心部分,确保用户只能访问他们被授权的资源。Spring Security是Spring框架中的一个安全框架,它提供了强大的认证和授权功能,用于实现用户认证和权限控制。本章节将详细讲解如何使用Spring Securit…...

单元测试方法的使用

import java.util.Date; import org.junit.Test; /** java中的JUnit单元测试* * 步骤:* 1.选中当前项目工程 --》 右键:build path --》 add libraries --》 JUnit 4 --》 下一步* 2.创建一个Java类进行单元测试。* 此时的Java类要求:①此类是公共的 ②此类提供一个公共的无参…...

VScode内接入deepseek包过程(本地部署版包会)

目录 1. 首先得有vscode软件 2. 在我们的电脑本地已经部署了ollama,我将以qwen作为实验例子 3. 在vscode上的扩展商店下载continue 4. 下载完成后,依次点击添加模型 5. 在这里可以添加,各种各样的模型,选择我们的ollama 6. 选…...

flink写入hdfs数据如何保证幂等的?

在 Flink 中使用 HDFS Connector 将数据写入 HDFS 时,保证幂等性是一个重要的需求,尤其是在数据可靠性要求较高的场景下。以下是详细介绍如何通过 Flink 和 HDFS 的特性以及一些设计上的优化来实现幂等性。 一、Flink 的 Checkpoint 机制 Flink 的 Chec…...

newgrp docker需要每次刷新问题

每次都需要运行 newgrp docker 的原因: 当用户被添加到 docker 组后,当前会话并不会立即更新组信息,因此需要通过 newgrp docker 切换到新的用户组以使权限生效 如果不想每次都手动运行 newgrp docker,可以在终端中配置一个自动刷新的脚本。…...

LM_Funny-2-01 递推算法:从数学基础到跨学科应用

目录 第一章 递推算法的数学本质 1.1 形式化定义与公理化体系 定理1.1 (完备性条件) 1.2 高阶递推的特征分析 案例:Gauss同余递推4 第二章 工程实现优化技术 2.1 内存压缩的革新方法 滚动窗口策略 分块存储技术 2.2 异构计算加速方案 GPU并行递推 量子计…...

WDM_OTN_基础知识_波分站点与组网类型

为了便于理解,我们用高铁来打个比方,这是郑州与武汉的高铁,中间经过了许昌孝感等很多个站点,郑州武汉作为始发站和终点站,所有人员都是上车或下车,而许昌等中间站点,既有人员上下车,…...

机器视觉--索贝尔滤波

引言 在图像处理领域,边缘检测是一项至关重要的任务,它能够帮助我们识别图像中不同区域的边界,为后续的目标识别、图像分割等操作奠定基础。索贝尔滤波(Sobel Filter)作为一种经典的边缘检测算法,因其简单…...

网络分析仪E5071C的回波损耗测量

回波损耗(Return Loss)是评估射频/微波元件(如滤波器、天线、电缆等)信号反射特性的关键参数,反映端口阻抗匹配性能。E5071C矢量网络分析仪(VNA)通过以下步骤实现高精度回波损耗测量&#xff1a…...

力扣-二叉树-98 验证二叉搜索树

思路 第一个特性,二叉搜索树的中序遍历是有序的,第二个特性,利用两个指针判断大小关系 代码 class Solution { public:TreeNode* pre NULL;bool isValidBST(TreeNode* root) {if(root NULL) return true;bool left isValidBST(root->…...

【动态规划】详解 0-1背包问题

文章目录 1. 问题引入2. 从 dfs 到动态规划3. 动态规划过程分析4. 二维 dp 的遍历顺序5. 从二维数组到一维数组6. 一维数组的遍历次序7. 背包的遍历顺序8. 代码总结9. 总结 1. 问题引入 0-1 背包是比较经典的动态规划问题,这里以代码随想录里面的例子来介绍下。总的…...

【Java线程池与线程状态】线程池分类与最佳实践

解析Java线程池与线程状态变化,结合运行机制与业务场景对照,帮助形成系统性知识。 一、线程池核心要素(五维模型) 采用「参数配置→处理流程→工作模式」三层递进结构 核心参数(线程池DNA) corePoolSiz…...

【小白学AI系列】NLP 核心知识点(八)多头自注意力机制

文章目录 **多头自注意力机制(Multi-Head Self-Attention)****核心概念** **1. 自注意力机制(Self-Attention)****2. 多头机制(Multi-Head Attention)****3. 为什么要用多头注意力机制?****4. 公…...

学习笔记——word中图目录、表目录 标题引用

目标1: 建立——图1-1 引用——图1-1 1在word文档中的引用——>插入题注 新建标签,然后命名为“图1-“。 点击确认,即可插入如图所示 图1- 1 春天 需要把图1-和后面那个1中间的空格删除,即 图1-1 春天 2怎么去引用这个“…...

3.3 Hugging Face Transformers核心功能模块深度解析

Hugging Face Transformers核心功能模块深度解析 一、模块化架构总览 #mermaid-svg-wxTV5vrEo7Y57IlW {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-wxTV5vrEo7Y57IlW .error-icon{fill:#552222;}#mermaid-svg-wxT…...

linux中设置脚本定时执行ntp命令同步时间

目录 一、背景二、过程1.到系统目录2.安装ntp3.创建文件夹4.创建脚本文件5.提升脚本文件权限6.设置执行时间:7.检查是否设置了执行器(执行后输出的内容为执行器中的定时执行内容)8.执行脚本文件9.查看日志文件,是否执行成功 三、总…...

map的使用(c++)

在了解map之前,我们先看看两个场景,通过这两个场景的对比,让我们知道为什么要存在存储双关键字的容器 场景一:判断一堆字符串中,某一个字符串是否出现过 在没学set容器之前,我们只能想到把这一堆字符串存到…...

毕业设计—基于Spring Boot的社区居民健康管理平台的设计与实现

🎓 毕业设计大揭秘!想要源码和文章?快来私信我吧! Hey小伙伴们~ 👋 毕业季又来啦!是不是都在为毕业设计忙得团团转呢?🤔 别担心,我这里有个小小的福利要分享给你们哦&…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

Nuxt.js 中的路由配置详解

Nuxt.js 通过其内置的路由系统简化了应用的路由配置&#xff0c;使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)

引言&#xff1a;为什么 Eureka 依然是存量系统的核心&#xff1f; 尽管 Nacos 等新注册中心崛起&#xff0c;但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制&#xff0c;是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

拉力测试cuda pytorch 把 4070显卡拉满

import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试&#xff0c;通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小&#xff0c;增大可提高计算复杂度duration: 测试持续时间&#xff08;秒&…...

算法笔记2

1.字符串拼接最好用StringBuilder&#xff0c;不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

2025季度云服务器排行榜

在全球云服务器市场&#xff0c;各厂商的排名和地位并非一成不变&#xff0c;而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势&#xff0c;对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析&#xff1a; 一、全球“三巨头”…...